Published on

Next.js LCP 개선 - 이미지·폰트 최적화 체크리스트

Authors

서버·라우팅·데이터 패칭을 아무리 최적화해도 LCP가 잘 안 내려가는 경우가 있습니다. 대부분의 원인은 LCP 후보 요소가 히어로 이미지 또는 큰 텍스트(웹폰트 적용)인데, 이 둘이 늦게 도착하거나(네트워크), 늦게 그려지거나(디코드·렌더링), 도착 후 레이아웃이 흔들리기 때문입니다.

이 글은 Next.js 기준으로 이미지폰트에 집중해 LCP를 개선하는 체크리스트를 제공합니다. 각 항목은 "왜 LCP에 영향을 주는지"와 "Next.js에서 어떻게 적용하는지"를 함께 다룹니다.

LCP부터 정확히 짚기: 무엇이 LCP 후보인가

LCP(Largest Contentful Paint)는 뷰포트 내에서 가장 큰 콘텐츠 요소(이미지, 비디오 포스터, 블록 텍스트 등)가 처음으로 렌더링 완료되는 시점을 뜻합니다. 실전에서는 다음 둘이 LCP 후보가 되는 일이 많습니다.

  • 상단 히어로 이미지(배너, 썸네일, 제품 이미지)
  • 상단 H1 같은 큰 텍스트(웹폰트 적용으로 렌더링 지연 가능)

따라서 LCP 개선은 LCP 후보 리소스를 가장 먼저 가져오고, 가져온 뒤 즉시 그릴 수 있게 만드는 작업입니다.

체크리스트 1: 히어로 이미지는 무조건 우선 로딩

1) next/image에서 priorityfetchPriority를 올바르게

히어로 이미지가 LCP 후보라면 priority를 켜서 preload 힌트를 주는 것이 기본입니다. Next.js 최신 브라우저에서는 fetchPriority도 함께 고려할 수 있습니다.

import Image from 'next/image'

export function Hero() {
  return (
    <section>
      <Image
        src="/images/hero.jpg"
        alt="메인 배너"
        width={1600}
        height={900}
        priority
        fetchPriority="high"
        sizes="(max-width: 768px) 100vw, 1200px"
        style={{ width: '100%', height: 'auto' }}
      />
    </section>
  )
}
  • priority: LCP 후보에만 제한적으로 사용하세요. 너무 많이 켜면 네트워크가 혼잡해져 역효과가 납니다.
  • sizes: 반응형에서 실제 필요한 리소스 크기를 정확히 알려 불필요한 큰 이미지를 피합니다.

2) fill을 쓰더라도 레이아웃 안정성 확보

fill은 편하지만 부모 컨테이너의 크기가 확정되지 않으면 CLS와 함께 LCP도 흔들립니다. 최소 높이를 확보하거나 aspect ratio를 고정하세요.

<div style={{ position: 'relative', width: '100%', aspectRatio: '16 / 9' }}>
  <Image
    src="/images/hero.jpg"
    alt="메인"
    fill
    priority
    sizes="100vw"
    style={{ objectFit: 'cover' }}
  />
</div>

3) LCP 이미지에 블러 플레이스홀더를 과신하지 않기

placeholder="blur"는 체감 로딩을 좋게 만들지만 LCP 자체를 줄여주지는 않습니다. 오히려 base64 블러 데이터가 커지면 초기 HTML/JS 바이트가 늘 수 있습니다. LCP가 목표라면 priority, sizes, 포맷 최적화가 우선입니다.

체크리스트 2: 이미지 바이트 줄이기(포맷·품질·리사이즈)

1) AVIF/WebP 우선, 원본은 필요할 때만

Next.js의 이미지 최적화 파이프라인을 쓰면 브라우저에 맞는 포맷으로 자동 변환됩니다. 단, 원본이 지나치게 크면 변환 비용과 전송량이 같이 커집니다.

  • 원본 업로드 단계에서 가로 2배수 같은 과도한 해상도를 피하기
  • 가능한 경우 사진은 JPEG 대신 WebP/AVIF 소스로 관리

2) sizes 미설정은 사실상 "항상 큰 이미지" 요청

반응형에서 sizes가 없으면 브라우저는 보수적으로 큰 리소스를 선택할 수 있습니다. 특히 히어로 영역은 LCP에 직결되므로 sizes를 반드시 작성하세요.

<Image
  src="/images/product.jpg"
  alt="상품"
  width={1200}
  height={800}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 90vw, 800px"
/>

3) CDN 캐시와 변환 캐시 확인

이미지 최적화는 서버에서 변환이 발생할 수 있습니다. 변환이 매번 일어나면 첫 요청의 TTFB가 늘고, LCP도 밀립니다.

  • Cache-Control이 적절한지 점검
  • 배포 환경에서 이미지 변환 결과가 캐시되는지 확인
  • 외부 이미지라면 remotePatterns 설정과 함께 원본 호스트의 캐시 정책도 확인

next.config.js 예시는 다음과 같습니다.

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

module.exports = nextConfig

체크리스트 3: LCP 이미지를 "늦게" 만드는 숨은 원인 제거

1) CSS background-image 히어로는 LCP에 불리할 수 있음

히어로를 background-image로 넣으면 preload/우선순위 제어가 어려워지고, 이미지가 LCP 후보로 잡히지 않거나 더 늦게 잡히는 경우가 있습니다. 가능하면 실제 img 요소(Next.js에서는 next/image)로 렌더링하세요.

2) 초기 렌더에서 조건부로 이미지가 등장하지 않게

예를 들어 클라이언트 상태가 준비된 뒤에만 히어로 이미지를 렌더링하면, LCP 후보 자체가 늦게 생성됩니다.

  • LCP 후보 요소는 SSR/정적 렌더에서 바로 포함
  • 스켈레톤 전환이 필요하면 이미지 컨테이너는 먼저 렌더하고 소스만 채우는 방식 고려

3) 과도한 애니메이션·필터는 디코드/페인트 비용 증가

히어로 이미지에 filter: blur()나 큰 그림자, 복잡한 애니메이션을 주면 페인트가 느려져 LCP가 밀릴 수 있습니다. LCP 영역은 가능한 단순하게 유지하세요.

체크리스트 4: 웹폰트는 LCP 텍스트 렌더링을 막는다

LCP 후보가 큰 텍스트일 때, 웹폰트 로딩 정책이 LCP를 좌우합니다. 핵심은 FOIT(폰트가 올 때까지 텍스트 숨김)를 피하고, 필요한 폰트만 빠르게 가져오는 것입니다.

1) next/font로 로컬/구글 폰트 최적화

Next.js의 next/font는 서브셋팅, preload, CSS 주입을 자동화해 폰트 최적화의 기본값을 끌어올립니다.

import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  preload: true,
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}
  • display: 'swap': 폰트가 늦어도 시스템 폰트로 먼저 그려 LCP를 앞당길 수 있습니다.
  • 서브셋을 최소화: 한글 폰트는 용량이 커서 특히 중요합니다.

2) 한글 폰트는 "전부"가 아니라 "필요한 굵기만"

한글 가변 폰트 또는 다수의 weight를 한 번에 로드하면 바이트가 급증합니다.

  • LCP 영역(H1 등)에 필요한 weight만 먼저 로드
  • 나머지 weight는 이후 상호작용 단계에서 로드하거나, 아예 제거

로컬 폰트 예시:

import localFont from 'next/font/local'

const pretendard = localFont({
  src: [
    { path: './fonts/Pretendard-Regular.woff2', weight: '400', style: 'normal' },
    { path: './fonts/Pretendard-Bold.woff2', weight: '700', style: 'normal' },
  ],
  display: 'swap',
  preload: true,
})

3) 폰트 프리로드를 쓸 때는 크로스오리진과 포맷 확인

직접 preload 링크를 다루는 경우 다음을 점검하세요.

  • as="font" 지정
  • type="font/woff2" 등 정확한 타입
  • 외부 도메인이면 crossorigin 필요

주의: 본문에서 부등호를 그대로 쓰면 MDX 빌드 에러가 날 수 있으니, 아래처럼 코드는 코드 블록으로만 다루세요.

<link
  rel="preload"
  href="/fonts/Pretendard-Regular.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

4) 폰트로 인한 CLS도 LCP 체감에 악영향

swap은 LCP를 당길 수 있지만 폰트 교체로 레이아웃이 흔들리면 사용자 체감이 나빠집니다.

  • 폰트 메트릭이 비슷한 시스템 폰트 스택 지정
  • 가능하면 size-adjust 같은 메트릭 보정(지원 범위 확인)
  • 제목 영역은 줄바꿈이 바뀌지 않도록 컨테이너 폭/라인 수를 설계

체크리스트 5: 측정과 디버깅 루틴(실전)

1) Lighthouse만 보지 말고 실제 LCP 후보를 확인

  • Chrome DevTools Performance에서 LCP 마커 확인
  • Network에서 LCP 이미지/폰트가 언제 다운로드 시작되는지 확인
  • Timing 탭에서 TTFB, download, decode 시간을 분해

2) Web Vitals를 앱에서 수집해 회귀를 막기

Next.js에서는 Web Vitals를 수집해 배포 후 회귀를 감지하는 것이 좋습니다. App Router 기준으로는 별도 계측 로직을 두고 분석 도구로 전송합니다.

개념적으로는 다음처럼 LCP 값을 받아 전송합니다.

// 예시: web-vitals 패키지 등을 활용해 LCP를 수집한 뒤 전송
export function reportWebVitals(metric: { name: string; value: number; id: string }) {
  if (metric.name === 'LCP') {
    navigator.sendBeacon(
      '/api/vitals',
      JSON.stringify({ id: metric.id, name: metric.name, value: metric.value })
    )
  }
}

LCP만 보지 말고 INP도 함께 관리하면 "빠르게 뜨지만 느리게 반응하는" 상태를 피할 수 있습니다. INP 개선은 다음 글이 도움이 됩니다.

실전 적용 순서: 가장 효과 큰 것부터

아래 순서로 적용하면 시행착오가 줄어듭니다.

  1. LCP 후보가 이미지인지 텍스트인지부터 확정(DevTools로 확인)
  2. 이미지가 LCP면 next/image로 전환하고 prioritysizes 설정
  3. 히어로 컨테이너의 크기 확정(특히 fill 사용 시)
  4. 폰트가 LCP면 next/font로 전환하고 display: 'swap', 서브셋/weight 최소화
  5. 배포 환경에서 이미지 변환/캐시 정책 점검
  6. Web Vitals 수집으로 회귀 감시

자주 발생하는 안티패턴 요약

  • LCP 이미지에 priority를 안 줌
  • sizes 누락으로 모바일에서도 큰 이미지 다운로드
  • 히어로를 CSS background-image로 처리
  • 클라이언트 상태 이후에야 히어로가 렌더링됨
  • 한글 폰트를 여러 weight로 한 번에 로드
  • 폰트 display 기본값으로 FOIT 발생

마무리: LCP는 "가장 큰 것" 하나만 제대로 다루면 내려간다

LCP 최적화는 범위를 넓게 잡기보다, 실제 LCP 후보 리소스 1개를 정확히 겨냥해 우선순위바이트, 레이아웃 안정성을 정리하는 것이 가장 빠른 길입니다. 히어로 이미지에는 priority와 정확한 sizes, 큰 텍스트에는 next/font와 최소한의 폰트 파일만 적용해도 체감과 지표가 동시에 개선되는 경우가 많습니다.

다음 단계로는 상호작용 지연(INP)까지 함께 최적화해 전체 Core Web Vitals를 안정화하는 흐름을 추천합니다.