Next.js Core Web Vitals 개선: LCP·INP·CLS 원인별 점검 순서
현장 데이터로 문제 지표 찾기, next/image preload, 폰트·이미지 크기 고정, 긴 작업 줄이기
핵심 요약
- LCP 2.5초, INP 200ms, CLS 0.1이 좋음 기준이며 75번째 백분위를 모바일·데스크톱별로 본다.
- 실험실 점수보다 Search Console·PageSpeed Insights의 현장 데이터로 문제 페이지를 먼저 찾는다.
- Next.js 16에서 Image의 priority는 deprecated이며 LCP 이미지는 fetchPriority="high"나 loading="eager"를 우선 검토한다.
- CLS는 크기 미지정 요소와 폰트 교체에서, INP는 클릭 핸들러의 긴 작업에서 주로 생긴다.
Lighthouse 점수가 좋아도 실제 사용자 지표가 나쁜 경우가 많다. 실험실 측정은 정해진 기기와 네트워크에서 한 번 재는 값이고, 검색 결과와 사용자 경험에 반영되는 것은 실제 방문자의 현장(field) 데이터이기 때문이다. 이 글은 web.dev의 Web Vitals 문서와 Next.js 문서를 바탕으로, 문제 지표를 먼저 찾고 원인별로 고치는 순서를 정리한다. Next.js 16.3의 내비게이션 개선은 Instant Navigations 글에서 다뤘다.
세 지표의 기준값
| 지표 | 측정 대상 | 좋음 기준 |
|---|---|---|
| LCP(Largest Contentful Paint) | 가장 큰 콘텐츠가 보이는 시점(로딩) | 2.5초 이내 |
| INP(Interaction to Next Paint) | 사용자 입력 후 다음 화면 갱신까지(반응성) | 200ms 이하 |
| CLS(Cumulative Layout Shift) | 예상하지 못한 레이아웃 이동(시각 안정성) | 0.1 이하 |
문서는 대부분의 사용자가 목표를 달성하는지 보려면 페이지 로드의 75번째 백분위를 모바일과 데스크톱으로 나눠 측정하라고 권한다. 평균값이 좋아도 느린 기기 사용자 비중이 크면 75백분위는 기준을 넘을 수 있다. Lighthouse 같은 실험실 도구는 INP를 직접 측정하지 않고 Total Blocking Time을 대리 지표로 쓴다는 점도 기억한다.
1단계: 현장 데이터로 문제 지표와 페이지 찾기
- Search Console의 Core Web Vitals 보고서로 기준을 넘는 URL 그룹을 찾는다.
- PageSpeed Insights로 해당 URL의 실제 사용자 데이터(Chrome UX Report)를 확인한다.
- 자체 수집이 필요하면 Next.js의
useReportWebVitals로 지표를 분석 서버에 보낸다.
// app/_components/web-vitals.tsx
'use client'
import { useReportWebVitals } from 'next/web-vitals'
const send = (metric: { name: string; value: number; rating: string; id: string }) => {
const body = JSON.stringify(metric)
if (navigator.sendBeacon) navigator.sendBeacon('/api/vitals', body)
else fetch('/api/vitals', { body, method: 'POST', keepalive: true })
}
export function WebVitals() {
useReportWebVitals(send) // 함수 참조가 바뀌지 않게 모듈 범위에 정의
return null
}문서는 이 훅이 'use client'를 요구하므로 별도 컴포넌트로 만들어 루트 레이아웃에서 불러오는 방식이 클라이언트 경계를 가장 좁게 유지한다고 설명한다. 콜백 함수 참조가 바뀌면 중복 보고가 생길 수 있으므로 모듈 범위에 정의한다.
2단계: LCP 원인별 점검
| 원인 | 확인 방법 | 조치 |
|---|---|---|
| LCP 이미지가 늦게 요청됨 | 네트워크 탭에서 히어로 이미지 요청 시작 시점 | next/image의 preload 또는 loading="eager"·fetchPriority="high" |
| 이미지가 화면보다 큼 | 실제 표시 크기 대비 다운로드 크기 | sizes 지정, 적절한 width/height |
| 서버 응답이 느림 | TTFB 측정 | 캐싱 적용, 데이터 조회를 Suspense 아래로 이동 |
| 렌더링을 막는 리소스 | 필수 CSS·폰트 로딩 순서 | 불필요한 전역 CSS·스크립트 제거 |
import Image from 'next/image'
export function Hero() {
return (
<Image
src="/hero.webp"
alt="제품 대시보드 화면"
width={1200}
height={630}
sizes="(max-width: 768px) 100vw, 1200px"
fetchPriority="high"
/>
)
}Image 문서에 따르면 Next.js 16부터 priority 속성은 deprecated되고 preload로 대체됐다. 다만 문서는 대부분의 경우 preload 대신 loading="eager"나 fetchPriority="high"를 쓰라고 권한다. 뷰포트에 따라 LCP 이미지가 달라지는 경우에는 preload를 쓰지 않는 것이 좋다.
3단계: CLS 원인별 점검
- 크기 없는 이미지·동영상·광고 영역: 너비·높이나
aspect-ratio를 지정해 공간을 미리 확보한다.next/image는width/height또는fill과 부모 크기로 공간을 잡는다. - 웹폰트 교체: 대체 폰트와 웹폰트의 글자 폭 차이로 줄바꿈이 바뀐다.
next/font를 쓰면 폰트를 자체 호스팅하고 레이아웃 이동을 줄이도록 처리한다. - 늦게 삽입되는 배너·알림: 콘텐츠 위에 끼워 넣지 말고 미리 자리를 잡거나 화면 위에 겹쳐 띄운다.
- Suspense 대체 UI 크기 불일치: 대체 UI(스켈레톤)를 실제 콘텐츠와 비슷한 높이로 만든다.
4단계: INP 원인별 점검
- 성능 패널에서 느린 상호작용을 녹화해 50ms 이상 걸리는 긴 작업(long task)을 찾는다.
- 클릭 핸들러 안의 무거운 계산은 결과가 필요한 시점으로 미루거나 Web Worker로 옮긴다.
- 큰 목록 필터링처럼 즉시 반영이 필요 없는 상태 변경은
useTransition으로 낮은 우선순위로 처리한다. - 불필요한 Client Component를 Server Component로 돌려 하이드레이션해야 할 자바스크립트 양을 줄인다.
- 서드파티 스크립트(채팅 위젯, 태그 관리자)의 로딩 시점을 늦춘다.
클라이언트 자바스크립트를 줄이려면 상호작용이 필요한 컴포넌트에만 'use client'를 두고 나머지는 Server Component로 유지한다. 코드 리뷰 단계에서 성능 회귀를 함께 보는 기준은 TypeScript 코드 품질 글을 참고한다.
개선 효과를 확인하는 방법
현장 데이터는 28일 같은 기간 단위로 집계되므로 배포 직후에는 변화가 바로 보이지 않는다. 배포 전후로 자체 수집한 useReportWebVitals 데이터를 같은 페이지·같은 기기 구분으로 비교하고, 75백분위가 기준 안으로 들어왔는지 확인한다. 실험실 도구는 회귀를 빠르게 잡는 용도로 CI에 두고, 최종 판단은 현장 데이터로 한다.
초보자가 자주 실수하는 포인트
- Lighthouse 점수만 보고 현장 데이터를 확인하지 않는 경우
- 모든 이미지에 preload를 걸어 오히려 중요한 리소스 로딩을 늦추는 경우
- 광고·배너 영역의 높이를 확보하지 않아 레이아웃 이동이 생기는 경우
- useReportWebVitals 콜백을 컴포넌트 안에서 매번 새로 만들어 중복 보고가 생기는 경우
체크리스트
- 현장 데이터에서 기준을 넘는 URL 그룹을 확인했는가
- LCP 이미지에 우선 로딩과 sizes가 지정됐는가
- 이미지·광고·임베드 영역의 크기가 고정됐는가
- 느린 상호작용의 긴 작업을 찾아 분리했는가
- 배포 전후 75백분위를 같은 조건으로 비교했는가
자주 묻는 질문
Lighthouse로 INP를 측정할 수 있나요?
web.dev 문서에 따르면 Lighthouse는 LCP와 CLS를 측정하고 INP 대신 Total Blocking Time을 대리 지표로 쓴다. INP는 현장 데이터로 확인한다.
useReportWebVitals로 어떤 지표를 받을 수 있나요?
문서상 TTFB, FCP, LCP, FID, CLS, INP를 받을 수 있으며 name 속성으로 구분한다.
priority 속성을 쓰는 기존 코드는 바로 고쳐야 하나요?
Next.js 16부터 deprecated이므로 업그레이드 시 preload 또는 문서가 권하는 loading="eager"·fetchPriority="high"로 옮긴다.
참고 자료 · 검증 기준
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.