- Published on
Next.js LCP 4초→1초 - 이미지·폰트 최적화
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 빠른데도 LCP가 4초대에서 내려오지 않는 경우, 범인은 대체로 LCP 후보(히어로 이미지/타이틀 영역)의 다운로드 지연 또는 렌더링 차단 리소스(폰트/CSS) 입니다. Next.js는 next/image, next/font 같은 도구를 제공하지만, 기본값만으로는 “가장 중요한 한 장”과 “가장 중요한 폰트”를 제대로 우선순위로 올리지 못하는 경우가 많습니다.
이 글은 LCP를 4초에서 1초대로 줄일 때 실제로 효과가 큰 이미지·폰트 최적화를 중심으로, 측정 방법부터 코드 적용, 운영 팁까지 한 번에 정리합니다.
참고: CLS가 같이 튀는 경우가 많습니다. 폰트/이미지로 레이아웃이 흔들린다면 Chrome CLS 급증 - 폰트·이미지·광고 시프트 해결도 함께 보세요.
1) 먼저 “진짜 LCP 요소”부터 확정하기
LCP 최적화는 감으로 하면 실패합니다. 먼저 크롬에서 LCP가 무엇인지 확정하세요.
Chrome DevTools로 LCP 후보 확인
- DevTools
Performance탭에서 기록 Timings또는Experience섹션에서Largest Contentful Paint클릭- 우측 패널에서 LCP element가 어떤 노드인지 확인
대부분 아래 둘 중 하나입니다.
- 히어로 이미지:
img또는next/image가 렌더한img - 히어로 텍스트: 큰
h1/p가 웹폰트 로딩 때문에 늦게 그려짐
Web Vitals로 실사용 LCP 수집
실험실(로컬)에서 좋아도 실사용이 나쁘면 의미가 없습니다. Next.js App Router 기준으로 web-vitals를 연결해 LCP를 수집하세요.
// app/_web-vitals.ts (예시) 또는 instrumentation/analytics 레이어
import type { Metric } from 'web-vitals';
export function reportWebVitals(metric: Metric) {
// metric.name: 'LCP', 'CLS', 'INP'...
// metric.value: ms
if (metric.name === 'LCP') {
// 예: analytics endpoint로 전송
navigator.sendBeacon(
'/api/vitals',
JSON.stringify({ name: metric.name, value: metric.value, id: metric.id })
);
}
}
이제부터는 “LCP 요소가 이미지인지 폰트인지”에 따라 처방이 갈립니다.
2) 이미지 최적화: LCP의 80%는 히어로 이미지에서 나온다
히어로 이미지가 LCP인 경우, 목표는 단순합니다.
- LCP 이미지 요청을 최대한 빨리 시작한다
- 전송 바이트를 줄인다
- 디코드/렌더 비용을 줄인다
2-1) next/image에서 LCP 이미지는 반드시 priority
next/image는 기본적으로 lazy 로딩을 사용합니다. 히어로 이미지를 lazy로 두면 LCP가 늦어지는 건 당연합니다.
// app/(home)/page.tsx
import Image from 'next/image';
export default function Home() {
return (
<main>
<section>
<Image
src="/images/hero.webp"
alt="서비스 소개"
width={1200}
height={630}
priority
sizes="(max-width: 768px) 100vw, 1200px"
/>
<h1>가장 중요한 메시지</h1>
</section>
</main>
);
}
핵심 포인트:
priority: LCP 후보라면 거의 무조건width/height: CLS 방지 + 레이아웃 확정으로 렌더링 안정화sizes: 모바일에서 과도한 이미지 다운로드 방지
2-2) fill을 쓸 때는 컨테이너 크기 확정
fill은 편하지만, 컨테이너가 늦게 확정되면 레이아웃 계산이 흔들리고 이미지 로딩 타이밍도 꼬일 수 있습니다.
import Image from 'next/image';
export function Hero() {
return (
<div style={{ position: 'relative', width: '100%', height: '420px' }}>
<Image
src="/images/hero.webp"
alt="히어로"
fill
priority
sizes="100vw"
style={{ objectFit: 'cover' }}
/>
</div>
);
}
컨테이너에 height 또는 aspect-ratio로 초기부터 크기를 확정하는 게 중요합니다.
2-3) 포맷: AVIF 또는 WebP 우선, JPEG/PNG는 최소화
LCP 이미지가 JPEG/PNG면 전송 바이트가 커서 RTT가 조금만 늘어도 바로 4초대로 갑니다.
- 가능하면
AVIF(지원 브라우저 확대 중) - 그 다음
WebP - 투명 필요 없으면 PNG 지양
Next.js 기본 이미지 최적화 파이프라인이 포맷 변환을 도와주지만, 원본이 너무 크면 변환해도 손해가 남습니다. 원본 리사이즈/크롭을 먼저 해두면 효과가 큽니다.
2-4) sizes 미설정은 “모바일 LCP 악화” 지름길
sizes가 없으면 브라우저는 큰 후보를 선택하기 쉬워져 모바일에서 과한 다운로드가 발생합니다.
<Image
src="/images/hero.webp"
alt="히어로"
width={1600}
height={900}
priority
sizes="(max-width: 480px) 100vw, (max-width: 1024px) 90vw, 1200px"
/>
현장에서 자주 보는 패턴:
- 데스크톱은 괜찮은데 모바일 LCP만 나쁨
- 원인:
sizes누락으로 모바일이 1600px 이미지를 받아버림
2-5) 원격 이미지라면 next.config와 캐시 전략 점검
원격 이미지(https://...)를 next/image로 최적화할 때는 도메인 허용과 캐시가 중요합니다.
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/**'
}
],
formats: ['image/avif', 'image/webp']
}
};
추가 팁:
- 이미지가 자주 바뀌지 않으면 CDN에서
Cache-Control을 길게 - 사용자별로 달라지는 이미지(예: 개인화 배너)는 LCP 후보에서 제외하거나 크기를 과감히 줄이기
2-6) preload는 최후의 수단, 하지만 “정확히” 쓰면 강력
priority로도 부족하고, LCP 이미지가 초기 HTML 파싱 시점에 확실히 필요하다면 <link rel="preload">를 고려할 수 있습니다. 다만 잘못 쓰면 다른 리소스를 밀어내 역효과가 납니다.
App Router에서는 next/image의 priority가 일반적으로 충분합니다. 정말 필요할 때만 제한적으로 적용하세요.
3) 폰트 최적화: 텍스트가 LCP면 next/font가 정답에 가깝다
LCP 요소가 큰 헤딩 텍스트인데, 웹폰트 로딩 때문에 늦게 페인트되면 LCP가 크게 늘어납니다. 이때 핵심은 아래 3가지입니다.
- 폰트를 렌더링 차단 리소스로 만들지 않기
- 필요한 글리프만 내려받기(서브셋)
- 폰트 로딩 중 레이아웃 변화를 최소화
3-1) next/font로 로컬/구글 폰트 최적화
next/font는 폰트를 빌드 타임에 최적화하고, 필요한 CSS를 자동으로 주입하며, font-display 전략도 포함합니다.
// app/layout.tsx
import './globals.css';
import { Noto_Sans_KR } from 'next/font/google';
const noto = Noto_Sans_KR({
subsets: ['latin'],
weight: ['400', '700'],
display: 'swap',
preload: true
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body className={noto.className}>{children}</body>
</html>
);
}
주의할 점:
- 한글 폰트는 용량이 큽니다. 가능한 경우
subsets를 현실적으로 설계하거나, 아예 로컬 서브셋 폰트를 만드는 편이 더 큽니다. weight를 여러 개 열면 그만큼 요청 수와 용량이 늘어납니다. 실제로 쓰는 굵기만 남기세요.
3-2) 한글 폰트는 “서브셋 제작”이 LCP를 바꾼다
한글 폰트 전체를 내려받으면 수백 KB에서 수 MB까지도 갑니다. 히어로 텍스트가 웹폰트를 기다리면 LCP는 쉽게 4초가 됩니다.
실무에서는 다음 중 하나를 선택합니다.
- 첫 화면에는 시스템 폰트 스택을 쓰고, 본문에서만 웹폰트 적용
- 히어로에 필요한 글자만 서브셋 폰트로 제공
예: fonttools로 서브셋(개념 예시)
# 예시: 필요한 글자만 포함한 woff2 생성
pyftsubset "NotoSansKR-Regular.otf" \
--text="서비스 소개 지금 시작" \
--flavor=woff2 \
--output-file="NotoSansKR-subset.woff2"
그리고 next/font/local로 로드합니다.
import localFont from 'next/font/local';
const heroFont = localFont({
src: './fonts/NotoSansKR-subset.woff2',
display: 'swap',
preload: true
});
export function HeroTitle() {
return <h1 className={heroFont.className}>서비스 소개 지금 시작</h1>;
}
서브셋의 장점:
- 전송 바이트 급감
- 폰트 로딩 시간이 짧아져 텍스트 LCP가 크게 개선
3-3) font-display: swap만으로는 부족할 때: 메트릭 오버라이드
swap은 FOIT를 줄이지만, 폴백 폰트와 웹폰트의 폭/높이가 달라 CLS가 생길 수 있습니다. next/font는 폰트 메트릭 오버라이드로 이를 완화할 수 있습니다.
- 결과적으로 LCP뿐 아니라 CLS도 안정화
CLS까지 같이 튜닝하려면 위에서 언급한 내부 글을 함께 참고하세요.
4) “이미지 + 폰트” 조합에서 LCP를 1초대로 만드는 체크리스트
4-1) 히어로 섹션은 최대한 단순하게
- 히어로 영역에 대형 이미지 1장 + 텍스트 정도로 제한
- 초기 렌더에 불필요한 슬라이더/비디오/애니메이션 제거
- 3rd-party 스크립트(채팅/태그)는 가능한 지연 로드
4-2) CSS가 LCP를 막고 있지 않은지 확인
- 거대한 CSS 번들이 초기 렌더를 지연시키기도 합니다.
- App Router에서 컴포넌트 단위 CSS를 쓰더라도, 전역 CSS에 모든 것을 몰아넣으면 역효과가 납니다.
4-3) 서버/캐시가 느리면 이미지·폰트 최적화가 반쪽
LCP는 네트워크 왕복에 민감합니다.
- 정적 자산은 CDN 캐시
Cache-Control설정 점검- TTFB가 나쁘면 LCP도 같이 나빠짐
운영 환경에서 인증/리다이렉트가 얽혀 초기 HTML이 늦어지는 경우도 있습니다. 인증 콜백이나 상태 검증 문제로 불필요한 왕복이 생기면 성능에도 악영향이 나니, 관련 이슈가 있다면 Next.js 14 OAuth 콜백 400 invalid_state 해결법처럼 “초기 진입 플로우”도 함께 점검하세요.
5) 실전 예시: LCP 4초에서 1초대로 떨어지는 전형적인 변경
아래는 현장에서 자주 나오는 “전/후”입니다.
변경 전(문제 패턴)
- 히어로 이미지가
img태그로 삽입, lazy 로딩 - 모바일에서도 큰 이미지 다운로드
- 한글 웹폰트 전체를 첫 화면에서 로드
export function HeroBad() {
return (
<section>
<img src="/images/hero.jpg" alt="hero" />
<h1 style={{ fontFamily: 'MyKoreanWebFont' }}>서비스 소개</h1>
</section>
);
}
변경 후(개선 패턴)
next/image+priority+sizeswebp또는avif- 히어로 텍스트는 서브셋 폰트 또는 시스템 폰트 우선
import Image from 'next/image';
import localFont from 'next/font/local';
const heroFont = localFont({
src: './fonts/HeroSubset.woff2',
display: 'swap',
preload: true
});
export function HeroGood() {
return (
<section>
<Image
src="/images/hero.webp"
alt="hero"
width={1200}
height={630}
priority
sizes="(max-width: 768px) 100vw, 1200px"
/>
<h1 className={heroFont.className}>서비스 소개</h1>
</section>
);
}
이 조합은 특히 모바일에서 효과가 큽니다. LCP가 4초대였던 페이지가, 이미지 전송량 감소 + 요청 우선순위 상승 + 폰트 대기 감소로 1초대까지 내려오는 케이스가 많습니다.
6) 검증: “좋아진 것처럼 보이는” 함정을 피하는 법
최적화 후에는 다음을 꼭 확인하세요.
- DevTools
Network에서 LCP 이미지가 초기에 요청되는지(워터폴 상 앞쪽) - LCP 이미지 응답 헤더에 캐시가 제대로 설정됐는지
- 모바일 시뮬레이션(느린 4G)에서 LCP가 유지되는지
- 실사용 데이터(필드 데이터)에서 LCP 분포가 개선되는지
특히 priority를 남발하면 다른 리소스가 밀려 전체 UX가 나빠질 수 있습니다. LCP 후보 1개(필요하면 2개) 정도만 엄격히 우선순위를 주는 게 안전합니다.
마무리
Next.js에서 LCP를 4초에서 1초대로 줄이는 가장 확실한 지름길은 “LCP 요소를 빨리 시작하고, 가볍게 만들고, 기다리지 않게 만드는 것”입니다.
- 히어로 이미지라면
next/image의priority와sizes, 그리고 포맷/원본 용량 최적화 - 텍스트라면
next/font기반 폰트 로딩 전략과 한글 서브셋
이 두 축만 제대로 잡아도 LCP는 체감될 정도로 내려가고, 부수적으로 CLS까지 안정화되는 경우가 많습니다.