- Published on
Next.js Image 최적화로 LCP 1초 줄이기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
웹 성능을 튜닝할 때 LCP(Largest Contentful Paint)가 잘 안 내려가는 프로젝트를 보면, 원인의 상당수가 “히어로 이미지(첫 화면에서 가장 큰 이미지)”에 걸려 있습니다. 이미지 자체가 크거나, 레이아웃이 늦게 확정되어 CLS를 유발하거나, 네트워크 우선순위가 밀려서 다운로드가 늦게 시작되는 패턴이 반복됩니다.
Next.js는 기본적으로 next/image를 통해 리사이징, 포맷 변환(WebP/AVIF), lazy loading, 최적화된 srcset 생성을 제공하지만, “기본값만으로는” LCP를 1초 단위로 줄이기 어려운 케이스가 자주 있습니다. 이 글에서는 실제로 LCP를 크게 줄이는 데 기여하는 옵션과 설계 포인트를, 측정-원인-해결 순서로 정리합니다.
성능 문제는 재현과 관측이 핵심입니다. 원인 분석 자체가 막히면 다른 레이어(예: CDN, 캐시, 403 오류 등)에서 병목이 생겼을 수도 있으니, 이미지가 CloudFront 뒤에 있다면 CloudFront 403 The request could not be satisfied 해결 같은 운영 이슈도 함께 점검하는 것을 권합니다.
1) LCP가 이미지일 때, 무엇을 먼저 확인해야 하나
LCP가 이미지로 잡히는 상황에서 가장 먼저 확인할 것은 “다운로드 시작 시점”과 “최종 디코드/페인트까지의 시간”입니다. Chrome DevTools의 Performance 패널 또는 Lighthouse/CrUX에서 LCP 후보 요소를 확인하고, Network 워터폴에서 해당 이미지 요청이 언제 시작되는지 봅니다.
대표적인 병목은 아래 4가지입니다.
- 요청이 늦게 시작됨: lazy loading, 낮은 우선순위, 렌더 블로킹 리소스 때문에 이미지 요청이 늦게 발사
- 너무 큰 파일: 원본이 과도하게 크거나, 리사이즈가 적용되지 않음
- 레이아웃이 늦게 확정됨:
width/height또는sizes가 부정확해 브라우저가 적절한 리소스를 늦게 선택 - 서버/캐시 미스: 이미지 최적화 서버가 느리거나, 매 요청마다 변환이 발생
이 중 1번과 3번은 next/image 옵션과 마크업만으로도 큰 폭으로 개선되는 경우가 많습니다.
2) 히어로 이미지는 priority와 fetchPriority로 “즉시” 받게 만들기
LCP 대상이 되는 히어로 이미지는 lazy loading이면 거의 항상 손해입니다. next/image에서 priority를 주면 preload가 걸리고, LCP 후보가 빨리 내려옵니다.
기본 패턴: priority + 정확한 크기
import Image from 'next/image'
export function Hero() {
return (
<section>
<Image
src="/images/hero.jpg"
alt="제품 메인 히어로 이미지"
width={1600}
height={900}
priority
/>
<h1>메인 카피</h1>
</section>
)
}
priority는 해당 이미지를 preload 대상으로 만들어 “다운로드 시작 시점”을 앞당깁니다.width/height는 레이아웃 박스를 즉시 확정해 CLS를 줄이고, 브라우저가 렌더링 계획을 더 빨리 세울 수 있게 돕습니다.
더 공격적인 우선순위: fetchPriority
Next.js/브라우저 조합에 따라 fetchPriority를 명시하면 네트워크 우선순위를 더 분명히 줄 수 있습니다.
<Image
src="/images/hero.jpg"
alt="히어로"
width={1600}
height={900}
priority
fetchPriority="high"
/>
주의할 점은 “모든 이미지를 high로” 주면 오히려 경쟁이 생겨 LCP가 악화될 수 있다는 것입니다. 일반적으로 high는 LCP 1개, 많아도 2개 수준으로 제한하세요.
3) fill을 쓸 때 LCP가 느려지는 흔한 이유: sizes 누락
fill은 반응형 레이아웃에 편리하지만, sizes를 제대로 주지 않으면 브라우저가 큰 이미지를 선택하거나(과다운로드), 반대로 적절한 후보를 늦게 고르는 문제가 생길 수 있습니다.
나쁜 예: fill만 있고 sizes가 없음
<div style={{ position: 'relative', width: '100%', height: 420 }}>
<Image
src="/images/hero.jpg"
alt="히어로"
fill
priority
/>
</div>
이 경우 브라우저는 뷰포트 조건에 따라 어떤 크기를 받아야 하는지 힌트가 부족합니다.
좋은 예: 레이아웃에 맞는 sizes 제공
<div style={{ position: 'relative', width: '100%', height: 420 }}>
<Image
src="/images/hero.jpg"
alt="히어로"
fill
priority
sizes="(max-width: 768px) 100vw, 1200px"
/>
</div>
- 모바일에서는 전체 폭(
100vw)을, 데스크톱에서는 최대 1200px만 사용한다는 정보를 주면 - Next.js가 생성하는
srcset중 “가장 맞는 후보”를 브라우저가 빠르게 선택하고 - 불필요하게 큰 리소스를 받는 것을 줄여 LCP가 내려갑니다.
실무에서는 sizes를 “디자인 시스템의 브레이크포인트”와 함께 관리하는 것이 가장 효과적입니다.
4) 이미지 포맷과 품질: AVIF/WebP는 LCP에 직결된다
Next.js는 기본적으로 브라우저가 지원하면 WebP/AVIF로 서빙할 수 있습니다. 다만 프로젝트 설정과 배포 환경에 따라 기대만큼 줄지 않을 수 있으니 next.config.js에서 명시적으로 관리하는 편이 안전합니다.
// next.config.js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [360, 640, 768, 1024, 1280, 1600],
imageSizes: [16, 32, 48, 64, 96, 128, 256],
},
}
formats우선순위를 주면 최신 포맷을 적극 활용합니다.deviceSizes/imageSizes는srcset후보를 구성하는 기준입니다. 후보가 너무 촘촘하면 캐시 파편화가 생기고, 너무 듬성하면 과다운로드가 생깁니다.
또한 quality를 무작정 낮추기보다 “히어로 이미지 1장만” 목표 용량을 정해 튜닝하는 게 효과적입니다.
<Image
src="/images/hero.jpg"
alt="히어로"
width={1600}
height={900}
priority
quality={75}
/>
경험적으로 LCP에 가장 큰 영향을 주는 건 “용량”과 “다운로드 시작 시점”입니다. 포맷 전환으로 30%만 줄어도 4G 환경에서 체감 차이가 크게 납니다.
5) next/image를 쓰고도 느린 경우: 최적화 서버/캐시 병목 제거
next/image의 기본 로더는 요청 시점에 리사이즈/변환이 일어날 수 있습니다. 트래픽이 많거나, 서버리스 콜드스타트가 있거나, 캐시가 제대로 붙지 않으면 LCP가 들쭉날쭉해집니다.
체크리스트
- 동일한 이미지 URL(같은
w파라미터)에서 캐시 히트가 나는가 - CDN이
/_next/image경로를 제대로 캐시하는가 - 원본 이미지가 외부 스토리지에 있을 때 TTFB가 튀지 않는가
운영에서 캐시/네트워크 이슈가 반복된다면, 애플리케이션 레벨 최적화뿐 아니라 CDN/배포 파이프라인 상태도 함께 봐야 합니다. 배포 후 갑자기 성능이 나빠졌다면 동기화/상태 불일치 같은 배포 이슈도 원인이 될 수 있으니 Argo CD Sync 실패 - OutOfSync·Degraded 해결법처럼 인프라 이벤트를 함께 추적하는 습관이 도움이 됩니다.
대안 1: 원본을 미리 리사이즈해서 제공
히어로처럼 항상 동일한 크기로 쓰는 이미지는 빌드 타임에 리사이즈한 정적 파일을 두고, unoptimized를 고려할 수 있습니다.
<Image
src="/images/hero-1600w.webp"
alt="히어로"
width={1600}
height={900}
priority
unoptimized
/>
- 장점: 런타임 변환 제거, CDN 캐시 단순화
- 단점: 다양한 디바이스에 대한 적응형 최적화는 줄어듭니다
대안 2: 커스텀 로더 또는 이미지 CDN 사용
이미지 변환을 전담하는 CDN(예: 변환 파라미터 기반)을 쓰면 TTFB와 캐시 전략이 단순해지고, 대규모 트래픽에서 안정적인 LCP를 만들기 쉽습니다.
import Image from 'next/image'
const cdnLoader = ({ src, width, quality }) => {
const q = quality || 75
return `https://img.example-cdn.com${src}?w=${width}&q=${q}&auto=format`
}
export function Hero() {
return (
<Image
loader={cdnLoader}
src="/hero.jpg"
alt="히어로"
width={1600}
height={900}
priority
quality={75}
/>
)
}
이 방식은 “변환 비용을 애플리케이션에서 떼어낸다”는 점에서 LCP 안정화에 특히 유리합니다.
6) LCP를 악화시키는 흔한 실수 7가지
- 히어로 이미지에
priority를 주지 않음 fill사용 시sizes누락- CSS로만 크기를 잡고
width/height를 비워 CLS 유발 - 원본 이미지를 과도하게 큰 해상도로 업로드(예: 6000px)
- 페이지 상단에 슬라이더를 두고 모든 슬라이드 이미지를 동시에 high priority로 다운로드
- 외부 이미지 도메인을 허용만 해두고, 실제로는 원본 서버가 느려 TTFB가 튐
srcset후보가 과도하게 많아 캐시 파편화(특히 트래픽이 큰 서비스에서)
이 중 1, 2, 3은 마크업 수정만으로도 즉시 개선되는 경우가 많고, 4, 6은 운영/자산 관리 체계 개선이 필요합니다.
7) 실전: LCP 1초 줄이는 적용 순서(추천)
아래 순서대로 적용하면 “가장 큰 효과를 가장 적은 변경으로” 얻기 쉽습니다.
- LCP 후보가 되는 이미지 1장을 특정한다(라우트별로 다를 수 있음)
- 해당 이미지에
priority를 적용하고, 필요하면fetchPriority를high로 설정 width/height또는fill+sizes로 레이아웃/리소스 선택 힌트를 정확히 준다- 포맷을 AVIF/WebP 우선으로 하고, 히어로는 목표 용량(예: 150KB 이하)을 정해
quality를 튜닝 /_next/image캐시 히트 여부를 확인하고, 불안정하면 정적 리사이즈 또는 이미지 CDN으로 변환 책임을 분리
이 과정을 거치면, 특히 모바일 4G/저사양 기기에서 LCP가 1초 이상 줄어드는 케이스가 흔합니다. 중요한 건 “모든 이미지를 최적화”가 아니라 “LCP를 만드는 단 하나의 이미지”를 먼저 확실히 최적화하는 것입니다.
8) 마무리: 측정 가능한 목표로 튜닝하자
이미지 최적화는 감으로 하면 끝이 없습니다. 라우트별로 LCP 후보를 고정하고(히어로), 그 이미지의 다운로드 시작 시점과 전송량, 캐시 히트 여부를 지표로 삼으면 개선이 빠릅니다.
- 목표 예시: 모바일 기준 히어로 이미지 전송량 150KB 이하, 요청 시작 시점 0.5초 이내
- 적용 예시:
priority+sizes+ AVIF + 캐시 안정화
성능은 결국 운영 품질과도 연결됩니다. 이미지 요청이 간헐적으로 실패하거나, CDN에서 차단/오류가 나면 LCP는 물론 사용자 경험 전체가 무너집니다. 성능 튜닝과 함께 네트워크/배포 레이어의 안정성도 같이 점검해두면, “한 번 줄인 1초”를 지속적으로 유지할 수 있습니다.