Next.js에서 Core Web Vitals를 개선하려면 중요한 콘텐츠를 일찍 제공하고 브라우저 JavaScript를 줄이며 나중에 나타나는 요소의 공간을 확보해야 합니다. Next.js에는 유용한 기능이 있지만 방문자 기기가 수행할 작업량은 렌더링 전략과 컴포넌트 경계에 따라 달라집니다. 프레임워크 설정을 바꾸기 전에 느린 측정항목부터 진단합니다.
예제는 Next.js 16의 App Router를 대상으로 하며 v16.4.2로 표시된 문서와 대조했습니다. 구현 방법을 보여 주는 예제이지 벤치마크 결과가 아닙니다. Pages Router 애플리케이션이나 Next.js를 사용하지 않는 React 애플리케이션은 통합 지점이 다르지만 브라우저에서 발생하는 문제는 같습니다.
경로와 측정항목부터 정합니다
최적화하기 쉬운 페이지가 아니라 측정 가능한 문제가 있는 실제 경로를 선택합니다. 기기 유형, 탐색 방식, 배포 버전, 사용자 상태를 기록합니다. 캐시된 공개 문서, 로그인한 대시보드, 개인화된 추천이 포함된 상품 페이지는 중요한 로딩 경로가 서로 다릅니다.
LCP는 측정 대상에 해당하는 가장 큰 가시 콘텐츠가 표시되는 시점을 나타냅니다. 느린 서버 응답, 늦게 시작한 이미지 요청, 메인 스레드 작업이 모두 영향을 줄 수 있습니다. INP는 방문 전체의 상호작용 응답성을 다룹니다. 하이드레이션, 필터 이벤트 처리, 상태 변경 후 렌더링이 서로 다른 시점에 영향을 줄 수 있습니다. CLS는 첫 화면 이후의 움직임을 포함한 예상하지 못한 레이아웃 이동을 측정합니다.
필드 데이터로 영향을 받는 사용자 집단을 찾고 브라우저 성능 기록으로 지연을 설명합니다. 일반 Lighthouse 탐색 감사로 운영 환경의 INP를 확정할 수는 없습니다. 개발자 노트북에서 한 번 빠르게 클릭했다고 실제 사용자 응답성이 좋다고 판단할 수도 없습니다. INP 진단 가이드에서는 입력 지연, 처리 시간, 표시 지연을 구분합니다.
수정 전에는 경로, 관찰한 LCP 요소, 관련된 긴 작업, 레이아웃 이동 원인, 해당 경로에서 로드하는 스크립트를 기록합니다. 이렇게 해야 막연한 최적화가 검증 가능한 가설이 됩니다. 대표 이미지 요청이 늦게 시작한다면 먼저 발견 시점이나 우선순위를 고쳐야 합니다. 관계없는 패키지 정리는 첫 해결책이 아닙니다.
콘텐츠의 실제 요구에 맞게 렌더링을 선택합니다
정적 생성, 요청 시 렌더링, 스트리밍, 클라이언트 렌더링은 전달 과정의 서로 다른 부분을 설명합니다. Server Components는 컴포넌트 코드가 실행되는 위치를 나타냅니다. 서버 컴포넌트는 정적 경로에도, 요청마다 처리하는 작업에도 참여할 수 있습니다. 서버 컴포넌트라는 이름만으로 캐시 정책이 정해지지는 않습니다.
실제 선택에서는 다음 장단점을 확인합니다.
| 방식 | 기대할 수 있는 장점 | 확인할 위험 |
|---|---|---|
| 정적 또는 캐시된 HTML | 원본 서버 작업을 반복하지 않고 콘텐츠를 일찍 제공 | 잘못된 무효화로 오래된 콘텐츠 제공 |
| 요청 시 서버 렌더링 | 최신 또는 개인화된 HTML 제공 | 느린 데이터 의존성이 응답 지연 |
| 스트리밍 | 느린 부분보다 유용한 부분을 먼저 제공 | LCP 콘텐츠가 경계 뒤에서 계속 대기 |
| 클라이언트 렌더링 | 필요할 때 데이터를 받는 상호작용 화면 | 유용한 콘텐츠 전에 스크립트와 데이터 요청이 연쇄 발생 |
React를 쓴다는 이유만으로 요청마다 처리할 필요는 없습니다. 게시할 때만 바뀌는 공개 문서는 미리 생성할 수 있는 경우가 많습니다. 개인화된 콘텐츠에는 동적 처리가 필요할 수 있지만 주변 탐색 메뉴와 설명까지 모든 개인화 의존성을 기다릴 이유는 없습니다.
스트리밍은 효과를 오해하기 쉽습니다. 스켈레톤을 먼저 보내면 첫인상은 좋아져도 최종 LCP 요소가 빨라지지 않을 수 있습니다. 문서의 첫 바이트뿐 아니라 실제 콘텐츠가 도착하는 시점을 확인합니다. 대체 표시와 최종 콘텐츠의 크기를 비슷하게 유지해야 점진적 전달로 화면이 뛰지 않습니다.
캐시 동작은 Next.js 버전과 활성화한 기능에 따라 다릅니다. 이전 버전의 가정을 그대로 적용하지 말고 생성된 경로와 배포 동작을 확인합니다. 이미 리소스가 준비된 개발 세션으로 운영 환경의 콜드 요청을 예측할 수는 없습니다. 직접 문서를 여는 경우와 클라이언트 탐색을 모두 시험합니다. 서로 다른 리소스를 요청할 수 있기 때문입니다.
클라이언트 경계를 작고 명확하게 유지합니다
App Router의 페이지와 레이아웃은 기본적으로 Server Components입니다. 상태, 이벤트 핸들러, 브라우저 API가 필요할 때 Client Components를 사용합니다. 필터나 펼침 제어처럼 상호작용에 필요한 가장 작은 적절한 경계에 'use client'를 둡니다. 버튼 하나 때문에 콘텐츠가 많은 전체 레이아웃을 클라이언트 코드로 만들지 않습니다.
이 경계는 가져오는 모듈 그래프에도 영향을 줍니다. 클라이언트 모듈이 무거운 라이브러리를 가져오면 페이지 대부분이 정적으로 보여도 브라우저 번들에 포함될 수 있습니다. 상호작용 컴포넌트의 import를 검토하고 필요한 데이터만 전달합니다. 큰 객체를 직렬화된 props로 넘기면 데이터 변환이 서버에서 끝났더라도 전송과 처리 비용이 생깁니다.
Client Component가 곧 클라이언트에서만 생성되는 HTML은 아닙니다. Next.js는 초기 출력을 미리 렌더링하고 브라우저에서 하이드레이션할 수 있습니다. 화면은 완성되어 보이지만 제어 요소가 반응하지 않는 상황을 조사할 때 이 차이가 중요합니다. HTML은 도착했어도 JavaScript와 이벤트 처리는 아직 준비 중일 수 있습니다.
운영 번들 분석으로 특정 의존성이 경로에 포함되는 이유를 찾습니다. 그다음 성능 추적에서 실제 실행 비용이 중요한지 확인합니다. 압축된 파일 크기와 CPU 시간은 다른 질문에 답합니다. 다운로드를 개선해도 긴 작업이 남는다면 JavaScript 실행 시간 가이드를 참고합니다.
실제 LCP 이미지의 로딩 계획을 명시합니다
next/image로 크기와 반응형 이미지 후보를 제공하고 실제 표시 너비를 정확하게 설명합니다. 고유 크기인 width와 height는 종횡비 공간을 확보하고 CSS가 화면상의 크기를 정합니다. fill을 사용하면 부모 요소에 위치 지정과 크기 또는 종횡비가 필요합니다. 공간 정의가 빠지면 이미지 최적화가 레이아웃 문제로 바뀔 수 있습니다.
다음 app/page.jsx 예제는 public/images/storefront.webp가 존재하고 초기 화면에서 중요한 이미지라고 가정합니다.
// app/page.jsx
import Image from 'next/image';
export default function Page() {
return (
<main style={{ maxWidth: 960, margin: '0 auto' }}>
<h1>A guide to faster storefronts</h1>
<Image
src="/images/storefront.webp"
alt="A storefront with a wide display window"
width={1600}
height={900}
sizes="(max-width: 960px) 100vw, 960px"
loading="eager"
fetchPriority="high"
style={{ width: '100%', height: 'auto' }}
/>
</main>
);
}sizes는 가용 너비를 사용하되 960 CSS 픽셀로 제한되는 예제 컨테이너에 맞춰져 있습니다. 레이아웃이 달라지면 이 값도 바꿉니다. 여러 열로 구성된 그리드의 카드가 편의상 전체 뷰포트 너비를 선언해서는 안 됩니다. 대표적인 화면 너비와 기기 픽셀 비율에서 네트워크 패널로 선택된 후보를 확인합니다.
Next.js 16에서는 priority가 지원 중단 예정 속성이 되고 preload가 대안으로 제시됩니다. 그렇다고 기존 priority를 모두 무조건적인 사전 로드로 바꿔야 한다는 뜻은 아닙니다. Image 컴포넌트 문서는 대부분의 경우 즉시 로드나 높은 요청 우선순위를 권장하며 preload를 loading 또는 fetchPriority와 함께 사용하지 않도록 안내합니다.
따라서 예제는 preload 없이 즉시 로딩과 우선순위 힌트를 함께 사용합니다. 사전 로드는 이미 알고 있는 중요 이미지를 더 일찍 발견해야 할 때 선택하는 별도 방법입니다. 브라우저가 어떤 리소스를 쓸지 확인하지 않은 상태에서 모바일과 데스크톱용 대표 이미지 여러 개를 경쟁적으로 사전 로드하지 않습니다. 중요하지 않은 화면 밖 이미지는 지연 로드를 유지하고 변경 후 실제 LCP 요소를 다시 확인합니다.
이미지 전달 서비스도 점검합니다. 원격 소스에는 적절한 remotePatterns 설정이 필요하고 기본 최적화 기능에는 호환되는 실행 환경이 필요합니다. 정적 내보내기에는 사용자 정의 loader나 미리 최적화한 파일 같은 명시적인 전달 전략이 필요합니다. 이미지 최적화 가이드는 컴포넌트 밖에서 결정해야 할 형식, 크기, 품질을 설명합니다.
글꼴과 레이아웃 안정성을 함께 판단합니다
next/font는 애플리케이션 배포에서 글꼴 파일을 제공할 수 있습니다. next/font/google을 사용하면 방문자의 브라우저가 Google에 글꼴을 요청할 필요가 없습니다. next/font/local은 프로젝트 파일을 사용합니다. 어느 방법이든 문자 지원 범위, 굵기, 대체 글꼴 메트릭, 전송량을 검토해야 합니다.
다음 루트 레이아웃은 app/fonts/body-latin.woff2에 사용 권한이 있는 라틴 글꼴이 있고 뒤에서 설명하는 측정 컴포넌트가 있다고 가정합니다.
// app/layout.jsx
import localFont from 'next/font/local';
import { WebVitals } from './web-vitals';
const bodyFont = localFont({
src: './fonts/body-latin.woff2',
display: 'swap',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={bodyFont.className}>
<body>
<WebVitals />
{children}
</body>
</html>
);
}이 코드는 영어 레이아웃이며 모든 언어에 통하는 글꼴 설정은 아닙니다. 한국어와 히브리어 페이지에는 적절한 문자 범위와 언어 설정이 필요하고 해당하는 경우 글 방향도 설정해야 합니다. 라틴 문자만 포함한 파일로 모든 언어를 표시할 수는 없습니다. 방문자가 한 언어만 사용하는데 모든 경로에서 여러 전체 글꼴 패밀리를 내려받지 않도록 합니다.
display: 'swap'은 글꼴을 기다리는 동안 대체 글꼴로 텍스트를 보여 줍니다. 교체 후 줄바꿈, 탐색 메뉴 너비, 카드 높이가 바뀌는지 확인합니다. 프레임워크의 대체 글꼴 조정은 도움이 되지만 전체 페이지의 CLS가 0이라는 증거는 아닙니다. 광고, 임베드, 나중에 삽입하는 배너, 스트리밍 구역에도 공간을 확보해야 합니다.
느린 네트워크에서 글꼴이 로드되기 전후를 비교합니다. 예시 문단뿐 아니라 LCP를 결정하는 제목을 확인합니다. 텍스트가 재배치되면 대체 글꼴과 레이아웃을 조정하거나 글꼴 의존성을 줄입니다. 웹 글꼴 최적화 가이드에서 이 과정을 자세히 설명합니다.
지연과 제거를 구분해 스크립트를 예약합니다
next/script는 스크립트 로딩 시점을 제어하지만 실행 비용을 없애지는 않습니다. 전략만큼 적용 범위도 신중하게 선택합니다. 한 경로에서만 필요한 위젯을 루트 레이아웃에 넣어 모든 경로에서 실행하지 않습니다.
각 전략의 목적은 다음과 같습니다.
| 전략 | 의도한 시점 | 확인할 사항 |
|---|---|---|
beforeInteractive |
루트 레이아웃의 초기 필수 스크립트 | 전체 앱이 실제로 일찍 필요로 하는 코드에만 사용 |
afterInteractive |
일부 하이드레이션 이후, 기본값 | 초기 상호작용과 경쟁할 수 있음 |
lazyOnload |
로딩 이후 브라우저 유휴 시간 | 나중의 실행도 방문자를 방해할 수 있음 |
worker |
실험적인 worker 기반 로딩 | 확인한 문서에서는 App Router 미지원 |
나중에 로드하면 초기 중요 경로를 보호하면서도 이후 상호작용을 느리게 만들 수 있습니다. 위젯 초기화와 겹치는 사용자 동작을 재현합니다. 적절한 경우 선택 기능은 방문자가 요청할 때 로드하고 기다리는 동안 눈에 보이는 피드백을 제공합니다. 동의 요건은 로딩 전략과 별개입니다. 분석 스크립트를 늦게 로드한다고 동의를 따르는 통합이 되는 것은 아닙니다.
하이드레이션과 그 이후 작업을 조사합니다
하이드레이션은 미리 렌더링한 출력에 React 동작을 연결합니다. 서버 출력과 첫 클라이언트 출력을 일치시킵니다. 브라우저 전용 상태, 비결정적 값, 서로 다른 콘텐츠 분기는 불일치를 일으킬 수 있습니다. 모든 경고를 숨겨도 전달 방식의 문제는 해결되지 않습니다.
큰 하이드레이션 작업은 입력을 늦출 수 있지만 모든 INP 문제가 하이드레이션 때문은 아닙니다. 이후 상태 업데이트도 큰 하위 트리를 다시 렌더링하고 비싼 레이아웃 계산이나 동기 연산을 유발할 수 있습니다. 초기 로딩 중뿐 아니라 페이지가 안정된 뒤에도 느린 동작을 기록합니다. 사용자 입력부터 다음 화면 표시까지의 구간을 살펴봅니다.
React의 스케줄링 기능은 일부 업데이트의 우선순위를 조정하지만 임의의 동기 JavaScript를 메인 스레드 밖으로 옮기지는 않습니다. 먼저 불필요한 계산을 제거하고 갱신하는 하위 트리를 줄이며 반복 레이아웃 작업을 피합니다. CPU 연산이 상당하다면 적절한 worker 경계를 검토합니다. 정확성도 확인해야 합니다. 빠르지만 잠시 잘못된 결과를 보여 주는 필터는 성공한 최적화가 아닙니다.
안정적인 콜백으로 운영 빌드를 측정합니다
App Router는 작은 Client Component 안에서 next/web-vitals의 useReportWebVitals를 사용합니다. 콜백 참조를 안정적으로 유지하면 다시 렌더링할 때 함수를 교체하면서 생기는 중복 보고를 피할 수 있습니다. 다음 예제는 브라우저 콘솔에 관찰값을 기록하며 사용자 데이터를 전송하지 않습니다.
// app/web-vitals.jsx
'use client';
import { useReportWebVitals } from 'next/web-vitals';
function reportMetric({ id, name, value, rating }) {
console.info('web-vital', { id, name, value, rating });
}
export function WebVitals() {
useReportWebVitals(reportMetric);
return null;
}앞의 레이아웃은 이 컴포넌트를 한 번 마운트합니다. 운영 빌드에서 페이지를 조작하고 수명 주기 후반에 보고되는 지표를 확인할 때는 페이지 가시성도 변경합니다. 로드 직후 모든 지표가 나타날 것으로 기대하지 않습니다. 해당하는 상호작용이 없는 방문은 INP 값이 없을 수 있습니다. 누락 값을 0으로 바꾸면 안 됩니다.
실제 사용자 데이터를 수집하려면 로그를 명시적으로 설계한 수집 과정으로 교체합니다. 샘플링, 해당하는 동의, 데이터 최소화, 중복 제거를 정의합니다. 측정 ID와 이름을 유지하고 라이브러리 버전에 맞게 업데이트를 처리합니다. LCP와 INP는 시간 단위이며 CLS는 단위가 없습니다. 다른 단위를 함께 평균 내거나 여러 업데이트를 별개의 방문으로 세지 않습니다.
경로 변경에는 추가 주의가 필요합니다. 소프트 탐색의 측정 동작은 브라우저와 라이브러리 지원에 달려 있습니다. 클라이언트 URL 변경이 모든 표준 Core Web Vitals를 초기화한다고 가정하지 않습니다. 늦게 도착한 측정값을 그 순간 보이는 URL에 무조건 연결하지도 않습니다. 실제 배포 버전의 탐색 처리 방식을 문서화합니다.
명확한 통과 조건을 두고 하나씩 배포합니다
병목을 고르고 가장 작은 관련 변경을 적용한 다음 비슷한 조건에서 같은 사용자 경로를 반복합니다. 이미지 수정은 요청 시점, 선택한 후보, LCP를 확인합니다. 클라이언트 경계 변경은 번들 포함 여부, 상호작용 시간, 기능을 확인합니다. 레이아웃 수정은 초기 로딩과 나중의 콘텐츠 갱신을 모두 살펴봅니다.
Core Web Vitals 검사 도구로 측정값을 해석한 뒤 영향을 받은 운영 경로와 기기 집단을 모니터링합니다. 플랫폼 최적화 가이드를 배포 점검표와 함께 활용합니다. 다음 단계는 한 원인과 연결된 개선을 기록하고 나머지 사용자 경험에서 회귀가 없는지 확인하는 것입니다.