본문으로 건너뛰기
DEVKBS
개발 노트
성능읽는 데 약 5

빠른 것과 빠르게 느껴지는 것은 다르다

이미지가 수십 장씩 들어가는 관광 사이트에서 로딩 시간을 줄이지 않고도 체감 품질을 바꾼 방법들.

#성능#이미지#CLS#next/image

관광지 홍보 사이트는 이미지가 전부입니다. 숙소 목록에는 카드마다 사진이 들어가고, 축제 페이지에는 갤러리가 붙습니다. 한 페이지에 수십 장이 예사입니다. 이런 사이트에서 성능 이야기를 하면 대개 '용량을 줄이자'로 흘러가는데, 실제로 사용자 경험을 바꾼 것은 그것만이 아니었습니다.

느리다는 말의 두 가지 의미

"사이트가 느려요"라는 피드백은 사실 두 가지 서로 다른 문제를 가리킵니다.

실제 속도체감 속도
측정LCP, TTFB, 전송 바이트사용자가 말하는 인상
개선 방법용량 축소, 캐싱, 코드 분할레이아웃 안정, 진행 표시, 순서 조정
효과실제로 빨라진다같은 시간이 짧게 느껴진다
둘 다 필요한가그렇다그렇다

이 사이트에서 제보로 들어온 '느림'의 상당 부분은 두 번째였습니다. 페이지가 뜨긴 하는데 덜컹거리고, 뭐가 되고 있는지 알 수 없으니 오래 기다린 것처럼 느껴지는 것입니다.

1. 레이아웃이 밀리지 않게 만든다

가장 큰 원인은 이미지가 로드되면서 자리를 차지하는 순간 아래 콘텐츠가 밀리는 것이었습니다. 사용자가 읽으려던 글이 갑자기 아래로 내려가고, 누르려던 버튼이 옮겨갑니다. 실제 로딩 시간과 무관하게 이건 '망가진 사이트'로 느껴집니다.

tsx이미지가 들어올 자리를 미리 확보한다
// 비율 컨테이너가 자리를 먼저 잡는다 — 이미지가 늦게 와도 밀리지 않는다
<div className="relative aspect-[4/3] overflow-hidden bg-muted">
  <Image
    src={stay.image}
    alt={stay.name}
    fill
    sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
    className="object-cover"
  />
</div>

2. 빈 공간 대신 형태를 보여준다

데이터를 기다리는 동안 화면을 비워 두면 사용자는 '아무 일도 일어나지 않는다'고 느낍니다. 같은 시간이라도 뼈대가 먼저 보이면 '오고 있다'고 느낍니다.

중요한 건 스켈레톤의 형태가 실제 결과와 닮아야 한다는 점입니다. 회색 사각형 하나를 띄우는 것보다, 카드 세 개가 들어올 자리에 카드 모양 뼈대 세 개를 그리는 편이 훨씬 낫습니다. 결과가 나타날 때 화면이 튀지 않기 때문입니다.

tsxSuspense 경계를 콘텐츠 단위로 쪼갠다
<Suspense fallback={<StayListSkeleton count={6} />}>
  <StayList region={region} />
</Suspense>

// 지도는 별도 경계 — 목록이 먼저 뜨고 지도가 나중에 채워진다
<Suspense fallback={<MapSkeleton />}>
  <RegionMap />
</Suspense>

Suspense 경계를 페이지 전체에 하나만 두면 가장 느린 데이터가 전부를 붙잡습니다. 콘텐츠 단위로 쪼개면 준비된 것부터 순서대로 나타납니다.

3. 이미지는 목록용과 상세용을 나눈다

트레이딩 카드 플랫폼에서도 같은 문제를 만났습니다. 카드 이미지는 원본이 큰데, 목록에서 원본을 그대로 쓰면 스크롤이 버벅입니다. 그렇다고 목록용 저해상도만 쓰면 상세에서 카드 상태를 확인할 수 없습니다.

  1. 목록에서는 저해상도 썸네일 + blur placeholder를 씁니다.
  2. 상세로 진입하면 이미 받아 둔 썸네일을 먼저 확대해 띄웁니다. 화면이 비는 순간이 없습니다.
  3. 그 위에 원본을 로드해 조용히 교체합니다.

전체 전송량은 오히려 늘었지만, 사용자가 느끼는 반응 속도는 훨씬 좋아졌습니다. 상세 화면에 들어갔는데 회색 사각형을 2초 보는 것과, 흐릿한 이미지가 곧바로 뜬 뒤 선명해지는 것은 완전히 다른 경험입니다.

4. 무거운 작업은 진행 상태를 노출한다

백오피스에서 FFmpeg로 영상을 브라우저에서 변환하는 기능을 만든 적이 있습니다. 수백 MB짜리 영상을 규격에 맞게 줄이는 작업이라 시간이 걸립니다. 처음에는 스피너 하나만 띄웠는데, 사용자들이 '멈춘 것 같다'며 창을 닫아 버렸습니다.

변환 진행률을 퍼센트로 노출하는 것만으로 이 문제가 사라졌습니다. 걸리는 시간은 똑같았습니다. 바뀐 것은 사용자가 '진행되고 있다'는 사실을 알게 된 것뿐입니다.

무거운 작업일수록, 지금 무엇이 되고 있는지 보여주는 것이 실제 속도만큼 중요합니다.

그렇다고 실제 성능을 미루면 안 된다

이 사이트에서 실제로 한 것들도 같이 적어 둡니다.

  • sizes를 화면 폭별로 지정해 모바일에서 데스크톱용 이미지를 받지 않게 했습니다.
  • 첫 화면에 보이는 이미지에만 priority를 주고 나머지는 지연 로드했습니다.
  • ISR로 페이지를 미리 구워 두어 요청 시점의 렌더 비용을 없앴습니다.
  • 폰트는 next/font로 셀프 호스팅하고 display: swap으로 텍스트가 먼저 보이게 했습니다.

정리

  • 이미지가 들어올 자리를 먼저 확보합니다. 레이아웃이 밀리면 속도와 무관하게 망가진 사이트로 느껴집니다.
  • 빈 화면 대신 결과와 닮은 뼈대를 보여줍니다.
  • Suspense 경계는 콘텐츠 단위로 쪼갭니다. 가장 느린 것이 전부를 붙잡지 않게.
  • 목록용과 상세용 이미지를 나누고, 상세에서는 이미 받은 것을 먼저 띄웁니다.
  • 오래 걸리는 작업은 진행률을 노출합니다. 시간은 그대로여도 경험이 달라집니다.
  • 체감 개선은 실제 개선과 함께 갑니다. 둘 중 하나만 하면 안 됩니다.

이 글이 나온 작업