반응형 이미지와 타이포그래피: srcset, next/image, 유동 타이포그래피

Frontend

이 글은 React로 반응형 웹 구성하기 시리즈 7편입니다.

레이아웃이 완벽하게 반응해도, 모바일 사용자에게 2000px짜리 원본 이미지를 그대로 내려주면 반응형은 절반만 완성된 것입니다. 데이터를 낭비하고, 로딩이 느리고, 이미지가 늦게 떠서 레이아웃이 밀립니다. 글자도 마찬가지입니다. 데스크톱에 맞춘 폰트 크기를 모바일에서 그대로 쓰면 너무 크거나, 한 줄이 너무 길어 읽기 불편합니다.

이 글에서는 반응형에서 자주 뒤로 미뤄지는 두 가지, 이미지와 타이포그래피를 다룹니다. 눈에 덜 띄지만 성능과 가독성에 직접 영향을 주는 부분입니다.

한눈에 보면

  • 반응형 이미지의 목표는 화면과 화면 밀도(DPR)에 맞는 크기의 이미지만 내려받게 하는 것입니다.
  • srcset + sizes는 "같은 그림의 여러 해상도" 중 브라우저가 알아서 고르게 합니다.
  • <picture>는 "화면에 따라 아예 다른 그림"(아트 디렉션)이 필요할 때 씁니다.
  • next/image는 이 srcset·지연 로딩·크기 예약을 자동화하지만, sizes를 제대로 넘겨야 제값을 합니다.
  • 이미지에 width/height 또는 aspect-ratio 를 주는 것만으로 CLS(레이아웃 이동)를 크게 줄입니다.
  • 타이포그래피는 clamp()로 크기를, ch로 줄 길이를 통제하면 화면 전반에서 읽기 좋아집니다.

반응형 이미지가 푸는 두 가지 문제

반응형 이미지는 서로 다른 두 문제를 다룹니다. 이 둘을 구분해야 srcsetpicture 중 무엇을 쓸지 정할 수 있습니다.

flowchart TD
    q["반응형 이미지가 필요하다"]
    q --> kind{"화면에 따라..."}
    kind -->|"같은 그림을 크기만 다르게"| res["해상도 전환<br/>srcset + sizes"]
    kind -->|"아예 다른 그림/비율/크롭"| art["아트 디렉션<br/>picture + source"]
 
    res --> resex["예: 상품 사진을<br/>모바일 400px / 데스크톱 800px"]
    art --> artex["예: 데스크톱은 와이드 배너,<br/>모바일은 세로 크롭 배너"]
 
    style res fill:#dbeafe,stroke:#3b82f6,color:#111
    style art fill:#dcfce7,stroke:#22c55e,color:#111
  • 해상도 전환: 그림은 같고 크기만 다르면 됩니다 → srcset + sizes
  • 아트 디렉션: 화면에 따라 구도나 비율이 달라져야 합니다 → <picture>

srcset과 sizes: 브라우저에게 선택을 맡기기

srcset은 "이 이미지의 여러 버전과 각 버전의 실제 폭"을, sizes는 "이 이미지가 화면에서 대략 얼마의 폭으로 표시될지"를 알려줍니다. 이 둘을 보고 브라우저가 DPR까지 고려해 가장 알맞은 파일을 고릅니다.

export function ProductImage() {
  return (
    <img
      src="/product-800.jpg" // srcset 미지원 브라우저용 폴백
      srcSet="/product-400.jpg 400w, /product-800.jpg 800w, /product-1200.jpg 1200w"
      sizes="(min-width: 768px) 50vw, 100vw"
      alt="상품 사진"
      width={800}
      height={800}
    />
  );
}

sizes="(min-width: 768px) 50vw, 100vw"를 읽으면 이렇습니다. "768px 이상 화면에서는 이 이미지가 뷰포트 폭의 50%로, 그보다 좁으면 100%로 표시된다." 브라우저는 여기에 화면 폭과 DPR을 곱해, srcset 중 딱 맞는 해상도를 내려받습니다. sizes를 틀리게 적으면(예: 항상 100vw) 필요보다 큰 파일을 받으므로, 실제 레이아웃 폭과 맞추는 것이 중요합니다.

picture: 화면에 따라 다른 그림

구도 자체가 달라져야 하면 srcset로는 부족합니다. 이때 <picture>로 조건별 소스를 지정합니다.

export function HeroBanner() {
  return (
    <picture>
      {/* 좁은 화면: 세로로 크롭된 배너 */}
      <source media="(max-width: 767px)" srcSet="/hero-mobile.jpg" />
      {/* 넓은 화면: 와이드 배너 */}
      <source media="(min-width: 768px)" srcSet="/hero-wide.jpg" />
      <img src="/hero-wide.jpg" alt="이벤트 배너" width={1200} height={480} />
    </picture>
  );
}
● 모바일 (max-width: 767px) — 세로 크롭
+----------------+
|                |
|     인물       |
|     중심       |
|     세로       |
+----------------+
 
● 데스크톱 (min-width: 768px) — 와이드
+------------------------------------------+
|   배경 넓게 + 인물 + 카피 공간 확보      |
+------------------------------------------+

srcset이 "브라우저가 알아서 고르는" 방식이라면, picture는 "개발자가 조건을 명시해 강제하는" 방식입니다.

next/image: 자동화와 그 대가

Next.js를 쓴다면 next/image가 srcset 생성, 지연 로딩, 크기 예약을 자동으로 해 줍니다. 다만 반응형에서 제값을 하려면 sizes를 직접 넘겨야 합니다.

import Image from 'next/image';
 
export function ProductImage() {
  return (
    <Image
      src="/product.jpg"
      alt="상품 사진"
      width={800}
      height={800}
      sizes="(min-width: 768px) 50vw, 100vw" // 이걸 빠뜨리면 과한 크기를 받음
      className="h-auto w-full"
    />
  );
}

컨테이너를 꽉 채우는 이미지라면 fill과 부모의 position: relative, object-fit을 함께 씁니다.

<div className="relative aspect-video w-full">
  <Image src="/cover.jpg" alt="" fill sizes="100vw" className="object-cover" />
</div>

next/image의 트레이드오프도 분명합니다. 자동 최적화·지연 로딩·CLS 방지는 강력하지만, 정적 익스포트(output: export) 환경에서는 기본 이미지 최적화 서버가 없어 별도 로더 설정이 필요할 수 있습니다. 프로젝트의 배포 방식에 맞는 설정을 확인해야 합니다.

aspect-ratio로 CLS 막기

반응형 이미지에서 가장 흔한 성능 실수는 크기를 예약하지 않는 것입니다. 이미지가 로드되기 전 높이가 0이었다가, 로드되며 갑자기 자리를 차지해 아래 콘텐츠를 밀어냅니다. 이게 CLS입니다.

해결은 간단합니다. width/height 속성을 주거나, CSS aspect-ratio로 비율을 예약합니다.

.thumb {
  width: 100%;
  aspect-ratio: 16 / 9; /* 로드 전에도 자리를 미리 확보 */
  object-fit: cover;
}

aspect-ratio는 반응형과 특히 잘 맞습니다. 폭이 유동적으로 변해도 비율이 유지되므로, 폭만 100%로 두면 높이는 알아서 따라옵니다.

유동 타이포그래피

2편에서 clamp()를 소개했는데, 타이포그래피야말로 clamp()가 가장 빛나는 영역입니다. 두 가지를 통제합니다. 글자 크기줄 길이입니다.

크기: clamp로 부드럽게

제목처럼 화면에 따라 크기 차이가 큰 요소는 유동 타이포그래피로 브레이크포인트 없이 부드럽게 조정합니다.

.h1 {
  font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3.5rem);
  line-height: 1.15;
}
.body {
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.125rem);
  line-height: 1.6;
}

선호값에 rem을 섞은 형태(1.2rem + 2.5vw)를 쓴 이유는 2편에서 본 대로입니다. vw 단독은 사용자의 브라우저 폰트 확대를 무시하는데, rem을 섞으면 확대에도 어느 정도 반응하게 됩니다.

줄 길이: ch로 가독성 지키기

읽기 좋은 본문 줄 길이는 대략 45~75자라고 알려져 있습니다. 화면이 아무리 넓어도 한 줄이 화면 끝까지 늘어나면 눈이 다음 줄을 찾기 어렵습니다. ch 단위(문자 0의 폭)로 상한을 겁니다.

한 가지 주의할 점은, ch는 숫자 0 한 글자의 폭을 기준으로 하므로 폭이 넓은 한글(전각)에는 정확한 글자 수가 아니라 근사치로 동작한다는 것입니다. 대략 70ch는 한글로는 35자 안팎에 해당하니, 한글 본문이라면 이 점을 감안해 실제 화면에서 값을 조정하는 게 좋습니다.

.prose {
  max-width: 70ch; /* 대략 70자에서 줄바꿈되어 읽기 편함 */
}
● max-width 없음 — 한 줄이 화면 끝까지 늘어남 (읽기 불편)
 
  반응형 웹에서 이미지와 타이포그래피는 자주 뒤로 밀리지만 실제로는 성능과 가독성에 크게 ...
  → 한 줄이 너무 길어 눈이 다음 줄 시작점을 놓치기 쉽다
 
● max-width: 70ch — 읽기 좋은 폭에서 줄바꿈
 
  반응형 웹에서 이미지와 타이포그래피는
  자주 뒤로 밀리지만 실제로는 성능과
  가독성에 크게 영향을 준다.
  → 약 45~75자 폭이라 시선 이동이 편하다

트레이드오프 정리

상황 우선 고려 이유
같은 그림, 크기만 다름 srcset + sizes 브라우저가 DPR까지 고려해 최적 선택
화면별로 다른 구도/크롭 <picture> 조건별 소스를 개발자가 명시
Next.js 프로젝트 next/image (+ sizes) srcset·지연 로딩·CLS 방지 자동화
로딩 중 레이아웃 밀림 aspect-ratio 또는 width/height 로드 전 자리 예약으로 CLS 감소
화면에 따라 큰 제목 크기 clamp() 유동 타이포 브레이크포인트 없이 부드럽게
넓은 화면에서 긴 본문 max-width: ...ch 읽기 좋은 줄 길이 유지

정리하면

반응형은 레이아웃에서 끝나지 않습니다. 이미지와 글자까지 화면에 맞춰야 성능과 가독성이 함께 따라옵니다.

  • 해상도만 다르면 srcset+sizes, 구도가 다르면 <picture> 로 구분합니다.
  • next/imagesizes를 제대로 넘겨야 자동 최적화가 제값을 합니다.
  • aspect-ratio로 자리를 예약해 이미지 로딩 중 레이아웃 밀림을 막습니다.
  • 글자 크기는 clamp(), 줄 길이는 ch 로 화면 전반에서 읽기 좋게 만듭니다.

다음 글은 시리즈의 마지막으로, 지금까지의 반응형(하나의 코드로 유동 대응)과 대비되는 적응형(Adaptive) 개념을 정리하고, React에서 반응형 컴포넌트를 구성하는 패턴과 테스트 방법까지 묶어 마무리합니다.

같이 보면 좋은 글