Published on

Next.js 14 이미지 최적화로 LCP 1초 줄이기

Authors

서버 컴포넌트와 스트리밍이 기본이 된 Next.js 14에서도, 실제 LCP를 잡아먹는 1순위는 여전히 hero 이미지인 경우가 많습니다. 특히 커머스/콘텐츠 사이트처럼 첫 화면에 큰 배너가 있고, 그 위에 텍스트가 얹히는 레이아웃이라면 이미지 로딩 전략 하나로 LCP가 1초 이상 흔들립니다.

이 글은 “이미지 최적화”를 추상적으로 나열하지 않고, LCP를 실제로 1초 줄이는 데 기여하는 순서대로 점검 포인트와 튜닝 방법을 제시합니다. 기준은 Chrome Lighthouse/CrUX가 보는 LCP 정의(뷰포트 내 가장 큰 콘텐츠 렌더 완료)이며, Next.js 14 App Router 환경을 가정합니다.

관련해서 RSC/SEO 이슈를 함께 다뤄야 하는 경우는 Next.js 14 RSC로 SEO 깨짐? 원인·해결 7가지도 같이 보면 좋습니다. 프론트 전체 체감 지표(INP)까지 묶어 최적화하려면 React/Next.js 프론트 최적화로 INP 200ms 달성도 참고하세요.

1) LCP가 이미지에서 느려지는 대표 패턴

LCP가 이미지에서 느려질 때, 원인은 보통 아래 중 하나(혹은 복합)입니다.

1-1. LCP 이미지 요청이 늦게 시작됨

  • HTML 파싱 후에야 이미지 URL이 결정됨(클라이언트 렌더링 의존)
  • loading="lazy"가 LCP 이미지에 걸려 있음
  • next/image를 쓰지만 priority가 빠져 프리로드가 안 됨

1-2. 다운로드가 느림

  • 원본이 너무 큼(불필요한 해상도)
  • 포맷이 비효율적(JPEG/PNG 고집)
  • CDN 캐시 미스/TTFB 지연

1-3. 디코드/렌더가 느림

  • 이미지 자체가 과도하게 큼(예: 4000px)
  • 메인 스레드가 바쁨(롱태스크)으로 디코드 타이밍이 밀림

이 글은 1-1과 1-2를 중심으로, Next.js 14에서 가장 재현성 있게 성능을 당기는 방법을 다룹니다.

2) 먼저 측정: LCP 이미지가 “무엇인지”부터 고정

LCP 개선은 “가장 큰 요소”가 바뀌면 성과가 사라지기도 합니다. 먼저 LCP 후보를 고정해야 합니다.

2-1. Chrome DevTools로 LCP 요소 확인

  1. DevTools Performance 탭에서 기록
  2. Timings 섹션에서 LCP 이벤트 클릭
  3. Related node에서 실제 LCP DOM 확인

LCP가 이미지라면 보통 img 또는 next/image가 렌더한 img가 잡힙니다.

2-2. 네트워크에서 “요청 시작 시점” 확인

DevTools Network에서 해당 이미지 요청을 찾고,

  • Start Time이 늦는지
  • Queueing/Stalled가 긴지
  • TTFB가 큰지
  • Content Download가 큰지 를 분리해서 봅니다.

이 분리만 제대로 해도, 다음 단계에서 무엇을 건드려야 할지 명확해집니다.

3) Next.js 14에서 LCP 이미지에 반드시 적용할 3가지

3-1. next/image + priority로 프리로드 강제

LCP 이미지에는 priority를 주는 게 기본입니다. priority는 내부적으로 fetchpriority="high" 및 프리로드 힌트를 유도해 요청 시작 자체를 앞당깁니다.

import Image from 'next/image'

export function Hero() {
  return (
    <section>
      <Image
        src="/images/hero.webp"
        alt="메인 배너"
        width={1440}
        height={720}
        priority
        sizes="(max-width: 768px) 100vw, 1440px"
      />
      <h1>Next.js 14 이미지 최적화</h1>
    </section>
  )
}

주의할 점은 두 가지입니다.

  • priority는 남발하면 역효과입니다. LCP 후보 1개(또는 상단 1~2개) 정도로 제한하세요.
  • sizes를 정확히 주지 않으면, 브라우저가 더 큰 리소스를 고르는 경우가 많아 다운로드가 커집니다.

3-2. sizes는 “디자인 기준”으로 실제 픽셀을 반영

sizes는 “이 이미지가 뷰포트에서 얼마나 차지하는지”를 브라우저에 알려주는 힌트입니다. 이게 부정확하면 srcset 중에서 큰 파일이 선택되어 LCP가 악화됩니다.

예를 들어 데스크톱에서 히어로가 컨테이너 1200px로 고정이라면:

<Image
  src="/images/hero.webp"
  alt=""
  fill
  priority
  sizes="(max-width: 768px) 100vw, 1200px"
  style={{ objectFit: 'cover' }}
/>

핵심은 fill을 쓰더라도 sizes를 생략하지 않는 것입니다.

3-3. LCP 이미지는 절대 loading="lazy" 금지

next/image는 기본이 lazy이므로, LCP 후보에는 priority 또는 loading="eager"를 명시해야 합니다.

priority를 쓰면 대부분 해결되므로, 특별한 이유가 없다면 priority를 우선 선택하세요.

4) “원본이 크다” 문제를 끊는 실전 전략

LCP를 1초 줄일 때 가장 큰 폭으로 듣는 건 결국 전송 바이트 절감입니다.

4-1. 포맷: WebP/AVIF 우선

Next.js의 이미지 최적화는 브라우저가 지원하면 WebP/AVIF로 변환해 전달할 수 있습니다. 다만 원본이 이미 WebP/AVIF라도, 리사이즈/품질 정책이 잘못되면 여전히 큽니다.

next.config에서 포맷 우선순위를 명시합니다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
  },
}

module.exports = nextConfig

AVIF는 용량이 더 줄어드는 경우가 많지만, 인코딩 비용이 커질 수 있습니다. 트래픽이 크고 이미지 변환이 자주 일어나는 환경이라면, 변환을 런타임에서 매번 하지 않도록 캐시 전략을 같이 봐야 합니다.

4-2. 품질(quality) 튜닝: LCP는 60~75부터 실험

quality는 체감 대비 용량 영향이 큽니다. 히어로 배너는 텍스트가 얹히는 경우가 많아, 과도한 품질이 낭비가 되기도 합니다.

<Image
  src="/images/hero.jpg"
  alt=""
  width={1440}
  height={720}
  priority
  quality={70}
  sizes="(max-width: 768px) 100vw, 1440px"
/>

실무 팁:

  • 먼저 quality={75}로 시작
  • Lighthouse에서 Transfer size와 LCP 변화를 보고
  • 70, 65로 내려가며 밴딩/블러가 티 나는지 QA

4-3. “필요한 해상도만” 제공: sizes + 적절한 레이아웃

히어로가 모바일에서 390px 폭인데 2000px짜리를 받으면, 네트워크가 좋은 환경에서도 LCP가 흔들립니다.

가장 흔한 실수는 sizes="100vw"를 모든 구간에 적용하는 것입니다. 데스크톱에서 실제로는 1200px 컨테이너인데 1920px 이상을 고르는 상황이 나옵니다.

5) App Router에서 LCP 이미지 요청을 더 앞당기는 방법

5-1. 가능한 한 서버에서 URL이 확정되게 만들기

클라이언트에서 상태를 받아서 이미지 URL이 바뀌면, 그 순간부터 요청이 시작되어 LCP가 늦어집니다.

  • 마케팅 배너/대표 이미지는 서버 컴포넌트에서 확정
  • 사용자별 개인화가 필요하면, LCP 후보는 고정 이미지로 두고 개인화는 2순위 영역에서 처리

5-2. metadata만 믿지 말고 실제 프리로드를 확인

priority를 주면 프리로드 힌트가 생성되는 경우가 많지만, 실제 HTML에 어떻게 반영되는지는 환경마다 다를 수 있습니다.

확인 방법:

  • View Source 또는 서버 응답 HTML에서 preload 관련 힌트가 있는지 확인
  • DevTools Network에서 해당 이미지가 초기에 High 우선순위를 받는지 확인

6) 원격 이미지(CDN)에서 자주 터지는 함정 4가지

6-1. images.remotePatterns 미설정으로 최적화 우회

원격 호스트를 허용하지 않으면 next/image 최적화가 비활성화되거나 기대와 다르게 동작할 수 있습니다.

const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        pathname: '/images/**',
      },
    ],
  },
}

module.exports = nextConfig

6-2. CDN 캐시 미스로 TTFB가 LCP를 잡아먹음

LCP 이미지가 최적화 서버(또는 CDN)에서 매번 변환되면 첫 요청 TTFB가 커집니다.

대응:

  • 변환 결과 캐시(서버/엣지) TTL 확보
  • 히어로 이미지처럼 트래픽이 큰 리소스는 빌드 시점에 고정 리사이즈된 에셋을 준비

6-3. Cache-Control이 잘못되어 재방문에도 느림

정적 히어로 이미지는 보통 public, max-age=31536000, immutable에 가깝게 가져가야 재방문 LCP가 안정됩니다.

6-4. 리다이렉트로 인해 요청이 1번 더 감

https://example.com/herohttps://cdn.example.com/hero.webp로 302를 타면, LCP는 그만큼 밀립니다. LCP 후보는 최종 URL로 바로 때리세요.

7) CSS/레이아웃이 LCP를 늦추는 경우: CLS와 동시 점검

이미지 최적화로 LCP를 줄이려다, 레이아웃 시프트가 생기면 오히려 사용자 체감이 나빠집니다.

체크리스트:

  • width/height 또는 fill + 부모의 고정 비율 확보
  • 히어로 영역에 동적 컨텐츠 삽입(광고, 개인화 위젯)으로 높이가 바뀌지 않게

예시: 비율 박스 확보

<div style={{ position: 'relative', width: '100%', aspectRatio: '16 / 9' }}>
  <Image
    src="/images/hero.webp"
    alt=""
    fill
    priority
    sizes="(max-width: 768px) 100vw, 1200px"
    style={{ objectFit: 'cover' }}
  />
</div>

8) “LCP 1초 단축”을 만들었던 실전 튜닝 순서

아래 순서는 작업 대비 효과가 큰 순서입니다.

8-1. LCP 이미지 1개를 확정하고 priority 적용

  • 요청 시작 시점을 앞당김
  • 체감 개선이 즉시 보임

8-2. sizes를 실제 레이아웃 기준으로 재작성

  • 불필요한 고해상도 선택을 차단
  • 모바일에서 특히 효과 큼

8-3. quality를 70 전후로 낮추고 포맷을 AVIF/WebP 우선

  • 전송 바이트를 크게 절감
  • 네트워크가 느린 사용자군에서 LCP 분산이 줄어듦

8-4. 원격 이미지라면 캐시/리다이렉트/TTFB를 함께 제거

  • “가끔만 느린” 꼬리 지연을 줄여 p75 LCP가 내려감

9) 코드로 마무리: 점검 가능한 템플릿

9-1. 히어로 컴포넌트 템플릿

import Image from 'next/image'

type Props = {
  src: string
  alt: string
}

export function HeroImage({ src, alt }: Props) {
  return (
    <div style={{ position: 'relative', width: '100%', aspectRatio: '21 / 9' }}>
      <Image
        src={src}
        alt={alt}
        fill
        priority
        quality={70}
        sizes="(max-width: 768px) 100vw, 1200px"
        style={{ objectFit: 'cover' }}
      />
    </div>
  )
}

9-2. next.config.js 템플릿

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        pathname: '/images/**',
      },
    ],
  },
}

module.exports = nextConfig

10) 체크리스트: 배포 전 10분 검증

  • LCP 요소가 이미지로 고정되어 있는가(DevTools Performance)
  • LCP 이미지에 priority가 적용되어 있는가
  • sizes가 레이아웃과 일치하는가(모바일/데스크톱 각각)
  • 이미지 포맷이 AVIF/WebP로 협상되는가(Network의 Content-Type)
  • 전송 바이트가 과도하지 않은가(히어로 기준 수백 KB를 넘기면 의심)
  • 원격 이미지라면 리다이렉트 없이 최종 URL로 받는가
  • 캐시 정책으로 재방문 LCP가 안정적인가

이미지 최적화는 “한 번에 끝”이 아니라, LCP 후보가 바뀌는 순간 다시 흔들립니다. 하지만 위 순서대로 적용하면, Next.js 14에서도 LCP를 가장 예측 가능하게 1초 가까이 당길 수 있습니다.