DevInsight

개발의 판단과 맥락을 기록하는 곳

Frontend
조회 0약 5분 읽기

next/image sizes 한 줄이 LCP를 0.5초 당긴다

LCP 개선을 위해 무작정 이미지를 압축하고 CDN을 도입하기 전에, next/image의 sizes 속성과 priority 플래그가 실제로 어떤 영향을 미치는지 정량적으로 이해해야 한다. 이 글은 next/image 설정값이 LCP에 미치는 영향을 실제 코드 레벨에서 분석하고, 이미지 CDN이 진짜 필요한 상황과 불필요하게 최적화를 도입했다가 역효과를 보는 사례까지 함께 다룬다.

DevInsight 편집팀 발행

AI 보조 초안과 편집 검수를 거쳐 발행했습니다.

#LCP#이미지최적화#next-image#CoreWebVitals#성능최적화#ImageCDN#WebP#프론트엔드성능

LCP 하면 가장 먼저 떠오르는 최적화는 이미지 압축과 CDN 도입이다. WebP로 변환하고, quality를 80으로 낮추고, CloudFront나 Cloudflare를 앞에 세우면 문제없을 거라고 생각한다. 하지만 실제로는 next/imagesizes 속성 한 줄과 priority 플래그를 제대로 썼는지 여부가 LCP에 더 직접적인 영향을 주는 경우가 훨씬 많다. CDN 도입 전에 먼저 확인해야 할 것들이다.

LCP 후보가 <Image>인데 왜 5초나 걸리나

Next.js에서 <Image> 컴포넌트는 lazy loading이 기본값이다. 브라우저가 뷰포트 안 이미지를 발견하면 로딩을 시작하는데, 이 시점이 LCP 타이밍을 결정한다. 문제는 next/image가 생성하는 <img> 태그에 loading="lazy"가 기본 포함된다는 점이다. 뷰포트에 들어오기 직전까지 브라우저는 해당 이미지 다운로드를 시작하지 않는다.

스크롤 없이 첫 화면에 보이는 히어로 이미지도 마찬가지다. Lazy loading이 걸리면 브라우저의 preload scanner가 이 이미지를 우선순위 리소스로 인식하지 못한다. 결과적으로 스타일시트나 폰트가 로드된 후에야 이미지 요청이 시작된다. 수백 밀리초에서 많게는 1~2초가 여기서 허비된다.

priority 플래그를 명시하면 이 문제가 해결된다. prioritytrue로 설정된 <Image><link rel="preload"> 태그를 <head>에 삽입하고, loading="eager"로 렌더링된다. LCP 이미지가 1.5MB WebP라면 preload 유무에 따라 LCP가 2.1초에서 1.4초로 줄어드는 식의 차이가 실제 프로젝트에서 자주 관측된다.

// LCP에 이 한 줄이 없으면 브라우저가 이미지를 늦게 발견한다 <Image src="/hero.webp" width={1200} height={675} priority alt="" />

priority 하나 주면 끝이라고 생각할 수 있지만, 반드시 LCP 후보 이미지에만 붙여야 한다. 페이지당 하나, 많아야 두 개를 넘기면 preload 경합이 발생해 오히려 LCP가 느려진다. 모든 이미지에 priority를 붙이는 건 가장 흔한 역최적화 중 하나다.

sizes가 없으면 Next.js는 매번 1080w를 내려받는다

next/image의 디폴트 동작을 제대로 아는 사람이 별로 없다. sizes 속성을 생략하면 Next.js는 sizes="100vw"를 가정한다. 모바일에서도 데스크톱에서도 브라우저는 가장 큰 후보 이미지(보통 1080w나 1200w)를 선택한다. 뷰포트가 390px인 아이폰에서도 1080px 너비 이미지를 다운로드하고, 4K 모니터에서도 같은 이미지를 받는다. 차이는 없다. 어차피 최대サイズ 하나만 내려받으니까.

sizes를 올바르게 설정하면 이야기가 달라진다. 반응형 레이아웃에서 뷰포트 너비별로 실제로 화면에 표시될 이미지 크기를 브라우저에 알려준다. 예를 들어 모바일에서 100vw, 데스크톱에서 50vw를 차지한다면 sizes="(max-width: 768px) 100vw, 50vw"로 설정한다. 브라우저는 이 정보를 바탕으로 뷰포트에 맞는 최적의 srcset 후보를 선택한다.

아이폰 SE(375px 뷰포트)에서 1080w 대신 640w 이미지를 받으면 용량이 대략 6070% 감소한다. WebP 기준으로 180KB에서 60KB로 줄어드는 셈이다. 이 차이는 3G나 저속 4G에서 LCP 0.30.5초 차이로 바로 이어진다. sizes가 없으면 아무리 이미지 압축을 잘해도 불필요한 바이트가 LCP 경로에 포함된다.

반응형 그리드나 스테이지 레이아웃이 아니면 sizes가 덜 중요할 거라고 생각할 수 있다. 하지만 next/image는 정적 import로 가져온 이미지 외에는 항상 srcset을 생성하고, sizes 기본값이 100vw이기 때문에 거의 모든 경우에 적용된다. 풀스크린 배너가 아니더라도 확인해볼 가치가 있다.

format 선택: WebP vs AVIF, 트레이드오프를 알아야 하는 이유

WebP가 무조건 정답이라는 인식이 널리 퍼져 있다. AVIF는 더 높은 압축률을 제공하지만 디코딩 비용이 WebP의 2배에 가깝다. LCP 이미지는 다운로드 시간만 문제가 아니라 디코딩까지 paint 타임에 포함된다. 저사양 모바일 기기에서 AVIF 500KB를 디코딩하는 데 30~50ms가 소요될 수 있고, 이게 메인 스레드를 블로킹한다.

next/imageaccept 헤더를 보고 브라우저가 지원하는 포맷으로 자동 변환해준다. format="avif"를 명시하면 AVIF를 우선 제공하고, 미지원 브라우저에는 WebP나 PNG로 폴백한다. 문제는 이 자동 변환이 서버리스 환경에서 CPU를 많이 잡아먹는다는 점이다. Vercel Functions나 Edge Functions에서 첫 번째 요청이 콜드 스타트와 이미지 변환을 동시에 처리하면 TTFB가 급증한다.

실험 결과: AVIF 변환을 추가하면 평균 이미지 용량은 30% 더 줄지만, 서버리스 환경에서 첫 방문자의 TTFB가 200400ms 증가한 사례가 있다. 전체 LCP 관점에서 AVIF로 줄어든 다운로드 시간보다 TTFB 증가분이 더 클 수 있다. WebP로도 충분히 4050% 압축이 가능하고, AVIF는 리소스가 충분한 환경이나 정적 빌드에서만 사용하는 편이 안전하다.

이미지 CDN, 진짜 필요한 상황과 불필요한 상황

이미지 CDN 도입은 보통 "트래픽이 많으니까"라는 막연한 이유로 결정된다. 하지만 실제로 LCP 개선에 CDN이 효과적인 조건은 생각보다 좁다. 첫 방문자가 전 세계에 분산되어 있고, 원본 이미지 서버가 단일 리전에 있으며, 이미지 크기가 200KB를 넘는 경우가 대표적이다. 반대로 한국 트래픽이 90% 이상이고 AWS 서울 리전에 S3 + CloudFront 조합을 이미 쓰고 있다면, 추가 CDN 최적화로 얻는 LCP 이득은 50ms 미만일 가능성이 높다.

CDN을 도입했다가 오히려 LCP가 악화되는 패턴도 있다. 대표적인 게 불필요한 이미지 변환 파이프라인 추가다. CDN 레벨에서 WebP 변환, 리사이징, quality 최적화를 동시에 활성화하면 오리진 요청마다 변환 시간이 추가된다. 캐시 히트율이 90%를 넘지 않는 상황에서는 첫 방문자 LCP가 순수 오리진 서빙보다 느려질 수 있다.

검증 방법은 간단하다. 실제 트래픽 패턴과 동일한 조건으로 CDN 경유 vs 직접 서빙의 LCP를 비교한다. WebPageTest의 멀티 위치 테스트나 RUM 데이터를 1주일 이상 수집해서 p75 LCP를 비교해야 한다. CDN 도입 후 p75 LCP가 100ms 이상 개선되지 않는다면, 그 예산을 이미지 포맷 최적화나 priority/sizes 설정에 쓰는 편이 낫다.

최적화가 과하면 생기는 세 가지 부작용

첫 번째는 앞서 언급한 preload 경합이다. 두 번째는 과도한 이미지 변환导致的 캐시 미스 증가다. next/imagesizesquality 값을 매 요청마다 다르게 주면 동일한 원본 이미지여도 여러 버전의 캐시 키가 생성된다. CDN 캐시는 파편화되고, 오리진 히트율이 떨어진다.

세 번째는 리소스 힌트 과잉이다. priority가 preload를 생성한다는 사실을 알고 나서 모든 이미지에 priority를 붙이거나, 수동으로 <link rel="preload">를 추가하는 경우가 있다. 그러면 브라우저의 preload queue가 꽉 차서 중요한 리소스(폰트, CSS, 주요 스크립트)가 밀려난다. LCP 이미지를 로드하는 동안 폰트가 블로킹되어 FOUT(Flash of Unstyled Text)가 발생하거나, FCP가 지연된다.

이 최적화들을 적용할 때마다 "이 변경이 LCP 경로에서 어떤 리소스를 앞당기고, 어떤 리소스를 뒤로 미는가"를 질문해야 한다. 하나의 지표를 개선하려다 다른 지표를 희생하는지 확인하는 과정 없이 최적화를 쌓아가면 성능은 더 나빠진다.

결국 sizes 한 줄의 가치

Lighthouse가 "Properly size images" 진단을 통과했다고 sizes가 올바르게 설정된 건 아니다. Lighthouse는 뷰포트 크기에서 표시된 이미지만 평가한다. 반응형 레이아웃에서 sizes가 누락된 경우, 모바일 실 기기에서는 여전히 과도한 크기의 이미지를 다운로드한다. 필드 데이터(Chrome User Experience Report)에서 LCP가 좋지 않은 페이지 중 상당수가 이 문제를 갖고 있다.

실제 운영 중인 서비스에서 sizes를 추가했더니 모바일 LCP p75가 3.2초에서 2.7초로 개선된 사례가 있다. priority는 이미 설정되어 있었고, 이미지는 WebP로 제공 중이었다. CDN도 사용 중이었다. 추가로 할 수 있는 건 sizes 속성 하나뿐이었고, 그 한 줄이 LCP를 0.5초 당겼다.

이미지 최적화를 논할 때 CDN, 포맷 변환, quality 조정보다 먼저 해야 할 일은 priority가 LCP 이미지에 걸려 있는지 확인하고, sizes가 뷰포트별 실제 표시 크기를 반영하고 있는지 검증하는 것이다. 이 두 가지만 제대로 맞춰도 대부분의 LCP 문제는 80% 해결된다. 나머지 20%는 CDN 도입이나 AVIF 전환 같은 추가 작업이 필요할 수 있지만, 그 판단은 반드시 실측 데이터를 기반으로 해야 한다.

댓글

댓글을 읽어오는 중입니다.

같이 읽으면 좋은 글

방금 읽은 주제와 이어지는 글을 골랐습니다.

Frontend 전체 보기

이전 글

DB 폴링과 작별할 시간을 아는 기술

다음 글

나의 첫 JWT 도입은 참사였다

DevInsight Digest

새 글이 쌓이면, 피드에서 바로 이어 읽으세요.

과장된 알림 대신 발행한 글 전체를 RSS로 제공합니다.

RSS 구독