- Published on
Next.js LCP 개선 - 이미지·폰트 최적화 체크리스트
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버·라우팅·데이터 패칭을 아무리 최적화해도 LCP가 잘 안 내려가는 경우가 있습니다. 대부분의 원인은 LCP 후보 요소가 히어로 이미지 또는 큰 텍스트(웹폰트 적용)인데, 이 둘이 늦게 도착하거나(네트워크), 늦게 그려지거나(디코드·렌더링), 도착 후 레이아웃이 흔들리기 때문입니다.
이 글은 Next.js 기준으로 이미지와 폰트에 집중해 LCP를 개선하는 체크리스트를 제공합니다. 각 항목은 "왜 LCP에 영향을 주는지"와 "Next.js에서 어떻게 적용하는지"를 함께 다룹니다.
LCP부터 정확히 짚기: 무엇이 LCP 후보인가
LCP(Largest Contentful Paint)는 뷰포트 내에서 가장 큰 콘텐츠 요소(이미지, 비디오 포스터, 블록 텍스트 등)가 처음으로 렌더링 완료되는 시점을 뜻합니다. 실전에서는 다음 둘이 LCP 후보가 되는 일이 많습니다.
- 상단 히어로 이미지(배너, 썸네일, 제품 이미지)
- 상단 H1 같은 큰 텍스트(웹폰트 적용으로 렌더링 지연 가능)
따라서 LCP 개선은 LCP 후보 리소스를 가장 먼저 가져오고, 가져온 뒤 즉시 그릴 수 있게 만드는 작업입니다.
체크리스트 1: 히어로 이미지는 무조건 우선 로딩
1) next/image에서 priority와 fetchPriority를 올바르게
히어로 이미지가 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 개선은 다음 글이 도움이 됩니다.
실전 적용 순서: 가장 효과 큰 것부터
아래 순서로 적용하면 시행착오가 줄어듭니다.
- LCP 후보가 이미지인지 텍스트인지부터 확정(DevTools로 확인)
- 이미지가 LCP면
next/image로 전환하고priority와sizes설정 - 히어로 컨테이너의 크기 확정(특히
fill사용 시) - 폰트가 LCP면
next/font로 전환하고display: 'swap', 서브셋/weight 최소화 - 배포 환경에서 이미지 변환/캐시 정책 점검
- Web Vitals 수집으로 회귀 감시
자주 발생하는 안티패턴 요약
- LCP 이미지에
priority를 안 줌 sizes누락으로 모바일에서도 큰 이미지 다운로드- 히어로를 CSS
background-image로 처리 - 클라이언트 상태 이후에야 히어로가 렌더링됨
- 한글 폰트를 여러 weight로 한 번에 로드
- 폰트
display기본값으로 FOIT 발생
마무리: LCP는 "가장 큰 것" 하나만 제대로 다루면 내려간다
LCP 최적화는 범위를 넓게 잡기보다, 실제 LCP 후보 리소스 1개를 정확히 겨냥해 우선순위와 바이트, 레이아웃 안정성을 정리하는 것이 가장 빠른 길입니다. 히어로 이미지에는 priority와 정확한 sizes, 큰 텍스트에는 next/font와 최소한의 폰트 파일만 적용해도 체감과 지표가 동시에 개선되는 경우가 많습니다.
다음 단계로는 상호작용 지연(INP)까지 함께 최적화해 전체 Core Web Vitals를 안정화하는 흐름을 추천합니다.