TechFeedTechFeed
Frontend

Core Web Vitals 완벽 분석 — LCP, INP, CLS 점수를 90+로 만드는 실전 전략

Core Web Vitals 3대 지표(LCP, INP, CLS)의 동작 원리와 실전 최적화 전략. 프레임워크별 비교, 성능 예산 운영, CI 자동 검증까지 다룬다.

Lighthouse 점수 50점대. 배포 직후에는 괜찮았는데 이미지 몇 개 추가하고, 서드파티 스크립트 붙이고, 폰트 바꾸다 보니 어느새 빨간불이 켜졌다. Google Search Console에서 "Core Web Vitals 개선 필요" 경고가 뜨고, 검색 순위가 밀리기 시작한다.

Core Web Vitals는 2021년에 Google 검색 랭킹 요소로 도입된 이후, 2024년 3월에 FID가 INP로 교체되면서 더 엄격해졌다. 이제는 단순히 "페이지가 빠르면 된다"가 아니라 로딩(LCP), 상호작용 반응성(INP), 시각적 안정성(CLS) 세 축을 모두 관리해야 한다.

이 글은 각 지표의 동작 원리를 정확히 이해한 뒤, 실제 코드 수준에서 점수를 끌어올리는 전략을 다룬다. Next.js, Astro 등 프레임워크별 최적화 차이도 포함했다.

기준일: 2026년 3월 | Chrome 130+, web-vitals 라이브러리 v4 기준

Core Web Vitals 3대 지표의 실체

Core Web Vitals를 "Lighthouse 점수 올리기"로만 이해하면 본질을 놓친다. 이 지표들은 실제 사용자가 체감하는 성능을 정량화한 것이다. 랩 데이터(Lighthouse)와 필드 데이터(CrUX)의 차이를 먼저 인식해야 한다.

지표측정 대상좋음개선 필요나쁨
LCP뷰포트 내 가장 큰 콘텐츠 요소의 렌더 시점≤ 2.5s2.5~4.0s> 4.0s
INP전체 세션 중 가장 느린 상호작용의 지연 시간≤ 200ms200~500ms> 500ms
CLS세션 동안 발생한 레이아웃 이동의 누적 점수≤ 0.10.1~0.25> 0.25

CrUX(Chrome User Experience Report)는 실제 Chrome 사용자의 28일 롤링 데이터를 집계한다. 여기서 75번째 백분위수(p75)를 기준으로 "좋음/개선필요/나쁨"을 판정한다. Lighthouse가 100점이라도 CrUX p75가 빨간불이면 검색 랭킹에는 반영되지 않는다. 반대로 Lighthouse가 다소 낮아도 실사용자 데이터가 좋으면 문제없다.

측정 도구별 용도를 정리하면 아래와 같다.

  • Lighthouse / PageSpeed Insights — 랩 환경 진단. 개선 포인트 탐색용
  • Chrome DevTools Performance 탭 — INP 원인 추적, Long Task 확인
  • web-vitals 라이브러리 — 프로덕션 필드 데이터를 자체 수집
  • CrUX Dashboard / BigQuery — Google이 집계한 실사용자 데이터 조회
  • Search Console Core Web Vitals 보고서 — 사이트 전체의 URL 그룹별 현황
Core Web Vitals 3대 지표(LCP, INP, CLS) 임계값 도표
LCP, INP, CLS의 "좋음/개선필요/나쁨" 임계값 기준 (출처: web.dev)

web-vitals 라이브러리를 사용하면 프로덕션에서 실제 사용자의 Core Web Vitals를 수집할 수 있다. Next.js는 next/web-vitals를 통해 자동으로 수집을 지원한다. 수집된 데이터를 Google Analytics 4나 자체 분석 파이프라인으로 보내면, CrUX 반영 전에 성능 이슈를 선제적으로 파악할 수 있다.

web-vitals 라이브러리로 필드 데이터 수집
import { onLCP, onINP, onCLS } from 'web-vitals'; function sendToAnalytics(metric) { const body = JSON.stringify({ name: metric.name, value: metric.value, rating: metric.rating, // 'good' | 'needs-improvement' | 'poor' delta: metric.delta, id: metric.id, navigationType: metric.navigationType, }); // sendBeacon은 페이지 언로드 시에도 전송 보장 if (navigator.sendBeacon) { navigator.sendBeacon('/api/vitals', body); } else { fetch('/api/vitals', { body, method: 'POST', keepalive: true }); } } onLCP(sendToAnalytics); onINP(sendToAnalytics); onCLS(sendToAnalytics);

LCP를 2.5초 이내로 잡는 4단계 전략

LCP(Largest Contentful Paint)는 뷰포트에서 가장 큰 콘텐츠 요소가 렌더링된 시점을 측정한다. 일반적으로 히어로 이미지, 배너, 또는 큰 텍스트 블록이 LCP 요소가 된다. LCP가 느린 원인은 크게 네 가지로 나뉜다.

1단계: 서버 응답 시간(TTFB) 최적화

HTML 자체가 느리게 도착하면 LCP도 늦어진다. TTFB(Time to First Byte)가 800ms를 넘는다면 서버 측 문제를 먼저 해결해야 한다.

  • CDN 사용: Vercel, Cloudflare Pages, Netlify 등은 엣지 캐싱을 기본 지원한다. 오리진 서버까지 가지 않아도 HTML을 반환할 수 있다.
  • SSG/ISR 활용: 변경이 드문 페이지는 빌드 타임에 생성(SSG)하거나, ISR(Incremental Static Regeneration)로 캐시 주기를 설정한다.
  • 스트리밍 SSR: React 18+의 renderToPipeableStream을 사용하면 HTML을 청크 단위로 전송해 첫 바이트를 빠르게 보낼 수 있다.

2단계: 리소스 발견 시간 최적화

LCP 리소스(보통 이미지)를 브라우저가 HTML 파싱 중에 최대한 빨리 발견해야 한다. CSS background-image나 JavaScript로 삽입되는 이미지는 발견이 늦어진다.

  • LCP 후보 이미지는 반드시 <img> 태그로 HTML에 직접 포함
  • <link rel="preload" as="image" fetchpriority="high">로 우선 로드 힌트 제공
  • fetchpriority="high" 속성을 LCP 이미지에 명시

3단계: 이미지 최적화

이미지 자체의 용량을 줄이는 것이 가장 직접적인 효과가 있다. 2026년 기준으로 WebP/AVIF 포맷이 표준이 되었고, 대부분의 CDN이 자동 변환을 지원한다.

4단계: 렌더 차단 리소스 최소화

CSS와 동기 JavaScript는 렌더를 차단한다. Critical CSS를 인라인으로 삽입하고, 나머지 CSS는 비동기로 로드하면 렌더 시작이 빨라진다.

Next.js에서 LCP 이미지 최적화
// next/image는 자동으로 WebP/AVIF 변환, lazy loading, srcset 생성 import Image from 'next/image'; export default function Hero() { return ( <Image src="/hero-banner.jpg" alt="메인 배너" width={1200} height={630} priority // LCP 후보: lazy loading 비활성화 fetchPriority="high" sizes="100vw" // 뷰포트 너비에 맞게 srcset 선택 quality={85} /> ); } // HTML <head>에 preload 힌트가 자동 삽입됨 // <link rel="preload" as="image" href="/_next/image?..." fetchpriority="high">

히어로 이미지가 아닌 텍스트가 LCP 요소인 경우(예: 블로그 제목, 뉴스 헤드라인)도 많다. 이 경우 웹폰트 로딩이 LCP에 직접 영향을 미친다. font-display: optional은 폰트 로드에 실패하면 시스템 폰트를 사용하므로 LCP를 지연시키지 않는다. font-display: swap은 시스템 폰트로 먼저 보여주고 교체하므로 LCP는 빠르지만 CLS가 발생할 수 있다.

실전에서는 font-display: swap + size-adjust 속성을 조합해 폴백 폰트의 메트릭을 웹폰트에 맞추는 것이 최적 전략이다. Next.js의 next/font는 이 작업을 자동으로 처리한다.

INP 200ms 벽을 넘는 JavaScript 최적화

INP(Interaction to Next Paint)는 2024년 3월에 FID를 대체한 지표다. FID가 "첫 번째 입력의 지연만" 측정한 반면, INP는 페이지 전체 세션 동안 발생한 모든 상호작용 중 가장 느린 것의 지연 시간을 측정한다. 훨씬 엄격하다.

INP가 높아지는 근본 원인은 하나다: 메인 스레드가 바쁘다. JavaScript가 메인 스레드를 오래 점유하면 사용자의 클릭/탭/키보드 입력에 대한 처리가 밀린다.

Long Task 분해하기

50ms를 초과하는 작업은 Long Task로 분류된다. Long Task가 실행 중에 사용자가 클릭하면, 해당 작업이 끝날 때까지 이벤트 처리가 지연된다. 해결 핵심은 큰 작업을 작은 단위로 쪼개 메인 스레드에 "숨 쉴 틈"을 주는 것이다.

scheduler.yield()는 2024년부터 Chrome에 도입된 API로, Long Task 내부에서 명시적으로 메인 스레드를 양보한다. 기존의 setTimeout(fn, 0)이나 requestIdleCallback과 달리, yield 후에도 우선순위를 유지하기 때문에 작업이 뒤로 밀리지 않는다.

INP 측정 구조 — 입력 지연, 처리 시간, 렌더 지연 분해 다이어그램
INP는 Input Delay + Processing Time + Presentation Delay의 합이다 (출처: web.dev)
scheduler.yield()로 Long Task 분해
// 대량 데이터 처리를 청크로 분해 async function processLargeList(items) { const CHUNK_SIZE = 50; for (let i = 0; i < items.length; i += CHUNK_SIZE) { const chunk = items.slice(i, i + CHUNK_SIZE); processChunk(chunk); // 각 청크 후 메인 스레드 양보 if (typeof scheduler !== 'undefined' && scheduler.yield) { await scheduler.yield(); } else { // 폴백: setTimeout으로 양보 await new Promise(resolve => setTimeout(resolve, 0)); } } } // React에서 비동기 상태 업데이트로 INP 개선 import { startTransition } from 'react'; function SearchFilter({ onFilter }) { const handleChange = (e) => { // 긴급: 입력값 즉시 반영 setInputValue(e.target.value); // 비긴급: 필터링 결과는 트랜지션으로 처리 startTransition(() => { onFilter(e.target.value); }); }; }

이벤트 핸들러 내부에서 무거운 작업을 직접 실행하지 않는 것이 핵심이다. React의 startTransition, Vue의 nextTick, 또는 직접 scheduler.yield()를 사용해 사용자 입력 처리와 부가 작업을 분리한다.

서드파티 스크립트도 INP의 주요 원인이다. Google Analytics, 채팅 위젯, A/B 테스트 도구 등이 메인 스레드를 점유한다. Chrome DevTools의 Performance 탭에서 Interactions 트랙을 열면 각 상호작용의 지연 원인을 추적할 수 있다. 서드파티 스크립트는 async 또는 defer 속성으로 로드하고, 가능하면 Web Worker로 격리하거나 Partytown 같은 라이브러리로 Worker 스레드에서 실행하는 것을 고려해야 한다.

CLS 0.1 이하로 유지하는 레이아웃 안정화

CLS(Cumulative Layout Shift)는 페이지가 로드되면서 요소들이 예상치 못하게 이동하는 정도를 측정한다. 뉴스 기사를 읽는 중에 갑자기 광고가 삽입되면서 본문이 아래로 밀려나는 경험 — 이것이 CLS 점수를 악화시키는 전형적 사례다.

이미지와 비디오에 명시적 크기 지정

<img><video>widthheight 속성을 반드시 지정한다. 브라우저가 리소스를 다운로드하기 전에 aspect ratio를 계산해 공간을 미리 확보한다. CSS의 aspect-ratio 속성을 사용하면 반응형에서도 비율을 유지할 수 있다.

웹폰트에 의한 레이아웃 이동 방지

웹폰트가 로드되면서 텍스트 크기가 변하면 전체 레이아웃이 밀린다. font-display: swap을 사용할 때 폴백 폰트와 웹폰트의 메트릭 차이가 클수록 CLS가 커진다. size-adjust, ascent-override, descent-override CSS 디스크립터로 폴백 폰트의 메트릭을 웹폰트에 맞추면 폰트 교체 시 레이아웃 이동을 최소화할 수 있다.

동적 콘텐츠와 광고 슬롯

동적으로 삽입되는 배너, 알림, 쿠키 동의 바 등은 기존 콘텐츠를 밀어내면 안 된다. 광고 슬롯은 반드시 min-height를 지정해 빈 공간을 미리 확보하고, 토스트/배너는 position: fixedtransform 기반 애니메이션으로 다른 요소의 위치에 영향을 주지 않게 구현한다.

CLS 방지: 이미지 aspect-ratio + 광고 슬롯 예약
/* 이미지: aspect-ratio로 공간 미리 확보 */ .hero-image { width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } /* 광고 슬롯: min-height로 레이아웃 shift 방지 */ .ad-container { min-height: 250px; /* Medium Rectangle 기준 */ display: flex; align-items: center; justify-content: center; background: var(--surface-bg, #f5f5f5); } /* 폴백 폰트 메트릭 조정 (CLS 최소화) */ @font-face { font-family: 'Adjusted Arial'; src: local('Arial'); size-adjust: 105%; ascent-override: 90%; descent-override: 22%; line-gap-override: 0%; }

프레임워크별 Core Web Vitals 최적화 비교

같은 코드라도 프레임워크의 렌더링 전략에 따라 Core Web Vitals 점수가 크게 달라진다. 각 프레임워크가 기본으로 제공하는 최적화와 추가로 설정해야 하는 항목을 비교한다.

기능Next.js 15Astro 5Remix / React Router 7SvelteKit 2
기본 렌더링RSC + 스트리밍 SSR정적 우선(Island)SSR + 스트리밍SSR/SSG 선택
JS 번들 크기중~대 (hydration 필요)최소 (JS 선택적)중 (full hydration)소 (컴파일 최적화)
이미지 최적화next/image 내장astro:assets 내장별도 설정 필요@sveltejs/enhanced-img
폰트 최적화next/font 자동수동 설정수동 설정수동 설정
INP 유리도RSC로 클라이언트 JS 감소최고 (JS 거의 없음)보통좋음 (런타임 가벼움)
ISR/캐싱revalidate 내장SSG 기본, SSR 선택loader + CDN 캐시 헤더prerender 옵션

콘텐츠 중심 사이트(블로그, 문서, 마케팅 페이지)에서 Core Web Vitals를 가장 쉽게 달성하는 프레임워크는 Astro다. JavaScript를 기본적으로 배제하고 필요한 인터랙션만 Island 컴포넌트로 하이드레이션하므로 INP가 자연스럽게 낮다. 반면 복잡한 대시보드나 SPA가 필요한 경우에는 Next.js의 RSC + Partial Prerendering 조합이 현실적 대안이다.

프레임워크별 Lighthouse 성능 점수 비교 차트
Next.js, Astro, Remix, SvelteKit의 기본 설정 기준 Lighthouse 성능 점수 비교 (출처: 벤치마크 데이터 기반 정리)

성능 모니터링 체계와 성능 예산 운영

최적화는 일회성이 아니다. 기능 추가, 디자인 변경, 서드파티 스크립트 추가가 반복되면 성능은 자연스럽게 하락한다. 이를 방지하려면 성능 예산(Performance Budget)을 설정하고 CI/CD 파이프라인에서 자동으로 검증해야 한다.

성능 예산의 핵심 지표를 아래와 같이 설정한다.

지표예산 기준검증 도구
LCP≤ 2.5sLighthouse CI
JS 번들 크기초기 로드 ≤ 200KB (gzip)bundlesize, size-limit
이미지 총 크기ATF(Above the Fold) ≤ 500KBLighthouse CI
서드파티 스크립트총 메인스레드 시간 ≤ 300msWebPageTest, DevTools

GitHub Actions에서 Lighthouse CI를 실행해 PR마다 성능 점수를 자동 체크하는 것을 권장한다. @lhci/cli 패키지를 사용하면 성능 예산 초과 시 빌드를 실패시킬 수 있다. Vercel은 Speed Insights를 통해 배포 후 실사용자 데이터도 대시보드에서 확인 가능하다.

📌 주의: Lighthouse CI의 랩 데이터 점수와 CrUX 필드 데이터 사이에는 항상 차이가 있다. 모바일 네트워크, 저사양 디바이스, 지역별 CDN 성능 등 실제 환경 변수를 랩 환경이 완전히 재현하지는 못한다. 랩 점수는 "상대적 변화 감지"로, 필드 데이터는 "실제 사용자 경험 판단"으로 구분해서 사용해야 한다.

CrUX 데이터를 BigQuery에서 직접 조회하면 경쟁 사이트와의 비교도 가능하다. chrome-ux-report 데이터셋에서 URL 또는 origin 단위로 p75 값을 추출할 수 있다. 월간 리포트를 자동화해 성능 트렌드를 추적하면 성능 하락을 조기에 발견할 수 있다.

Core Web VitalsLCPINPCLS웹성능최적화LighthouseNext.jsAstro프론트엔드

관련 도구

관련 포스트

웹 성능 최적화 — Core Web Vitals 2026 가이드2026-02-20Remix vs Next.js — 풀스택 프레임워크 비교 20262026-03-14Next.js 15 핵심 변경사항 총정리2026-02-15Tailwind CSS v4 — 무엇이 달라졌나2026-02-19