- Published on
Next.js 이미지 LCP 개선 - next/image 최적화
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버/클라이언트 렌더링이 아무리 빨라도, 히어로 이미지 한 장이 늦게 뜨면 LCP(Largest Contentful Paint)는 그대로 무너집니다. 특히 Next.js에서 next/image를 “그냥 Image로 바꿔 끼우는 수준”에서 끝내면, 오히려 잘못된 sizes나 레이아웃 선택 때문에 다운로드 바이트가 늘거나 프리로드가 실패해 LCP가 악화되는 경우도 흔합니다.
이 글은 Next.js에서 LCP에 영향을 주는 이미지 경로를 분해하고, next/image의 옵션을 어떤 순서로 점검해야 실제로 LCP가 내려가는지(그리고 왜 내려가는지)를 코드 중심으로 정리합니다.
LCP와 이미지: 어디서 시간이 새는가
이미지 LCP는 대체로 아래 구간의 합으로 결정됩니다.
- HTML 도착 및 파싱
- LCP 후보(대개 히어로 이미지) 리소스 발견
- 네트워크 요청 시작(프리로드 여부가 큼)
- 다운로드(바이트 수, CDN, 캐시)
- 디코딩 및 렌더(레이아웃 안정성 포함)
next/image는 2~5를 건드립니다. 하지만 “자동 최적화”라는 말만 믿고 기본값을 쓰면, 3번(요청 시작)이 늦어지거나 4번(다운로드)이 커질 수 있습니다. LCP 개선은 결국 다음 3가지로 귀결됩니다.
- 요청을 더 빨리 시작:
priority와 프리로드 - 필요한 만큼만 다운로드: 올바른
sizes, 적절한quality, 최신 포맷 - 렌더링 비용과 레이아웃 흔들림 제거:
fill/width/height선택, CSS
1) 히어로 이미지는 반드시 우선 로딩으로 고정하기
히어로 이미지가 LCP라면, 해당 이미지는 지연 로딩(lazy)되면 안 됩니다. next/image에서 핵심은 priority입니다.
App Router 예시: 히어로 이미지에 priority 적용
// app/page.tsx
import Image from 'next/image'
export default function Page() {
return (
<main>
<section style={{ position: 'relative', height: 420 }}>
<Image
src="/images/hero.jpg"
alt="Product hero"
fill
priority
sizes="(max-width: 768px) 100vw, 1200px"
style={{ objectFit: 'cover' }}
/>
</section>
</main>
)
}
priority는 내부적으로 프리로드 힌트를 생성해 “리소스 발견 및 요청 시작”을 앞당깁니다.- LCP 후보가 명확한 경우
priority는 거의 필수입니다.
주의: priority는 1개(또는 아주 소수)만
priority를 여러 이미지에 남발하면 프리로드 경쟁이 생겨 오히려 LCP가 느려질 수 있습니다. 원칙은 “LCP 후보 1개”에만 적용하고, 나머지는 기본 lazy로 두는 것입니다.
2) sizes를 틀리게 쓰면 LCP가 악화된다
next/image는 srcset을 만들어 다양한 해상도 이미지를 제공하지만, 브라우저가 “어떤 크기를 받을지”를 결정하는 열쇠는 sizes입니다.
sizes가 없거나 부정확하면 브라우저가 큰 이미지를 선택할 가능성이 커집니다.- 특히
fill을 쓰면서sizes="100vw"로 고정해버리면, 데스크톱에서도 뷰포트 전체 폭으로 계산되어 불필요하게 큰 파일을 받는 일이 잦습니다.
흔한 실수: 무조건 100vw
<Image
src="/images/hero.jpg"
alt="Hero"
fill
priority
sizes="100vw"
/>
히어로가 실제로는 컨테이너 최대 폭 1200px인데 100vw로 선언하면, 1920px급 리소스를 고르는 식의 과다운로드가 발생할 수 있습니다.
권장 패턴: 브레이크포인트와 최대 폭을 함께 선언
<Image
src="/images/hero.jpg"
alt="Hero"
fill
priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 90vw, 1200px"
/>
- 모바일: 뷰포트 폭만큼
- 태블릿/중간: 90vw
- 데스크톱: 최대 1200px로 캡
이 한 줄이 LCP 바이트를 수십 퍼센트 줄이는 경우가 많습니다.
3) 레이아웃 선택: fill vs width/height 그리고 CLS
LCP는 “언제 그려지느냐”뿐 아니라 “그릴 때 레이아웃이 안정적인가”도 영향을 받습니다. 이미지가 그려지는 순간 레이아웃이 튀면, 브라우저는 추가 작업을 하게 됩니다.
원칙
- 고정 비율 카드/썸네일:
width/height로 명시하는 편이 단순하고 안정적 - 배경형 히어로(컨테이너에 꽉 차는):
fill+ 부모에position: relative와 명확한 높이
// 카드 썸네일 예시
<Image
src="/images/thumb.jpg"
alt="Thumbnail"
width={360}
height={240}
sizes="(max-width: 768px) 50vw, 360px"
/>
width/height를 주면 브라우저가 공간을 미리 확보하므로 CLS가 줄고, 렌더링 파이프라인이 단순해집니다.
렌더링 영역 최적화 관점에서는 이미지 주변 레이아웃도 중요합니다. 이미지 외 컨텐츠가 많아 메인 스레드가 바쁘면 LCP가 늦어질 수 있으니, 필요하다면 CSS 렌더링 튜닝도 함께 보세요: CSS contain·content-visibility로 렌더링 튜닝
4) 포맷과 품질: “가볍게”가 아니라 “맞게”
quality는 기본값에만 맡기지 말고 측정 기반으로
히어로 이미지는 품질을 너무 낮추면 밴딩/블러가 눈에 띄지만, 너무 높이면 LCP 바이트가 늘어납니다. 보통은 다음처럼 시작해 측정으로 조정합니다.
<Image
src="/images/hero.jpg"
alt="Hero"
fill
priority
quality={75}
sizes="(max-width: 768px) 100vw, 1200px"
/>
- 사진:
quality70~80에서 타협점이 자주 나옵니다. - 일러스트/텍스트 포함 이미지: 품질을 더 높이거나(혹은 SVG/벡터로 대체) 접근해야 합니다.
최신 포맷: AVIF/WebP를 제대로 쓰고 있는지
Next.js의 이미지 최적화는 보통 자동으로 WebP/AVIF를 협상합니다. 다만, 원본이 지나치게 큰 PNG이거나, CDN/프록시가 헤더를 망가뜨리면 협상이 실패하기도 합니다.
운영에서 캐시/재검증이 꼬이면 “이미지가 계속 새로 최적화되거나 캐시가 안 먹는” 형태로 LCP가 튈 수 있습니다. App Router의 캐시 동작을 함께 점검하려면 이 글도 도움이 됩니다: Next.js App Router 캐시 꼬임·재검증 버그 해결
5) next.config의 images 설정: 원격 이미지와 캐시를 통제
외부 이미지(예: CMS, S3, 이미지 CDN)를 쓸 때는 remotePatterns를 명시하고, 가능하면 CDN을 통해 지연과 변동성을 줄입니다.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
],
formats: ['image/avif', 'image/webp'],
},
}
module.exports = nextConfig
추가로, 원격 서버의 응답 헤더가 캐시에 불리하면(짧은 cache-control, vary 문제 등) LCP가 흔들립니다. CDN에서 적절한 TTL과 캐시 키 정책을 잡는 것이 중요합니다.
6) 블러 플레이스홀더는 “체감”엔 좋지만 LCP엔 독이 될 수 있다
placeholder="blur"는 초기 페인트를 부드럽게 해주지만, 히어로 이미지에 남발하면 다음 문제가 생길 수 있습니다.
- 블러 데이터 생성/전송 비용
- 메인 이미지 로딩 자체가 빨라지는 것은 아님
히어로(LCP 후보)에는 보통 priority와 правиль한 sizes가 우선이고, blur는 체감 품질이 필요할 때만 제한적으로 사용합니다.
<Image
src="/images/thumb.jpg"
alt="Thumb"
width={360}
height={240}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
7) 실전 점검 체크리스트: LCP가 안 내려갈 때
(1) LCP 후보가 정말 그 이미지가 맞는지
Chrome DevTools의 Performance/웹 바이탈 오버레이로 LCP 엘리먼트를 확인합니다. 텍스트 블록이 LCP인 경우도 많습니다.
(2) priority가 적용되었는지
- LCP 이미지에
priority가 있는가 - 동시에
priority이미지가 여러 개는 아닌가
(3) sizes가 실제 레이아웃과 일치하는지
- 컨테이너 최대 폭을 반영했는가
- 모바일/태블릿/데스크톱 브레이크포인트가 합리적인가
(4) 과다운로드 여부
- 네트워크 탭에서 실제로 내려받는 이미지 픽셀 폭이 과한지 확인
- 필요하면
sizes를 더 공격적으로 캡
(5) 서버/캐시 문제로 이미지가 매번 느린지
- 원격 이미지 응답이 느린가
- CDN 캐시 히트가 나는가
- App Router 캐시/재검증이 이미지 경로에 영향을 주는가
8) 예시: 히어로와 리스트를 분리 최적화하기
히어로 1장은 LCP를 위해 최우선, 아래 리스트 썸네일은 지연 로딩과 정확한 sizes로 바이트를 줄입니다.
import Image from 'next/image'
export function Hero() {
return (
<header style={{ position: 'relative', height: 460 }}>
<Image
src="https://cdn.example.com/images/hero-2026.jpg"
alt="Main hero"
fill
priority
sizes="(max-width: 768px) 100vw, 1200px"
quality={78}
style={{ objectFit: 'cover' }}
/>
<h1 style={{ position: 'relative' }}>Next.js 성능 튜닝</h1>
</header>
)
}
export function Card({ src, title }: { src: string; title: string }) {
return (
<article>
<Image
src={src}
alt={title}
width={320}
height={200}
sizes="(max-width: 768px) 50vw, 320px"
/>
<h2>{title}</h2>
</article>
)
}
- 히어로:
fill+priority+ 최대 폭1200px캡 - 카드: 고정 크기 + lazy(기본) + 모바일에서만 50vw
마무리: LCP는 “옵션 몇 개”가 아니라 경로 최적화다
next/image는 LCP 개선의 강력한 도구지만, 핵심은 자동 최적화가 아니라 “브라우저가 올바른 순간에, 올바른 크기의 이미지를 받게 만드는 것”입니다. 실전에서 가장 효과가 큰 순서는 대개 다음과 같습니다.
- LCP 이미지에
priority sizes를 실제 레이아웃에 맞게 수정(최대 폭 캡)- 레이아웃 안정화(
fill사용 시 부모 높이/포지션 명확화) - 품질/포맷/캐시 점검
이 4가지만 제대로 맞춰도, 히어로 이미지가 LCP인 페이지는 체감과 지표가 함께 개선되는 경우가 많습니다.