본문으로 건너뛰기
yutils

Core Web Vitals 는 어떻게 측정될까?

Core Web Vitals 의 LCP · CLS · INP 가 실제로 무엇을 재는지, field vs lab 데이터 차이, 각 지표를 망치는 원인과 측정·개선 방법.

약 9분 읽기

Lighthouse 에선 100 점인데 Search Console 은 "개선 필요" 라고 뜬다. 똑같은 페이지인데 왜? 답은 Core Web Vitals 가 실제로 무엇을, 어디서 재는지에 있다. Google 의 사용자 경험 지표 3 종 — LCP · CLS · INP — 은 lab(Lighthouse) 과 field(실제 사용자) 에서 다르게 측정되고, "느린 첫 페인트" 같은 막연한 느낌을 구체적인 ms 와 점수로 환산한다. 이 가이드는 세 지표가 각각 무엇을 재는지, lab 과 field 가 왜 갈리는지, 무엇이 점수를 망치고 어떻게 고치는지를 정리한다.

Core Web Vitals 란

Google 이 정의한 "좋은 페이지 경험" 의 정량 지표. 2024 년 3 월부터 현재 3 종으로 굳었다:

지표                            good        needs improvement   poor
─────────────────────────────────────────────────────────────────────
LCP  (Largest Contentful Paint)  ≤ 2.5s      2.5s – 4.0s         > 4.0s
CLS  (Cumulative Layout Shift)   ≤ 0.1       0.1 – 0.25         > 0.25
INP  (Interaction to Next Paint) ≤ 200ms     200ms – 500ms      > 500ms

세 지표 모두 "good" 에 들어야 페이지가 Core Web Vitals 를 통과한 것으로 본다. INP 는 2024 년 3 월에 기존 FID (First Input Delay) 를 정식 대체했다 — FID 는 첫 입력의 지연만 쟀지만, INP 는 페이지 수명 전체의 모든 상호작용 반응성을 본다.

LCP — 가장 큰 콘텐츠가 그려지는 시점

viewport 안에서 가장 큰 콘텐츠 요소(보통 hero image, 큰 텍스트 블록, video poster)가 화면에 칠해지는 시각. "이 페이지 로딩됐다" 고 사용자가 체감하는 순간의 proxy.

navigation 시작 (t=0)
   │
   ├─ TTFB ──── 서버가 첫 byte 보냄
   │
   ├─ FCP ───── 첫 픽셀 (아무 텍스트/배경)
   │
   └─ LCP ───── 가장 큰 요소 그려짐  ← 이 시점이 지표
                ↑ ≤ 2.5s 여야 good

LCP 를 망치는 주범:

  • 느린 TTFB — 서버/CDN 이 첫 byte 를 늦게 보내면 이후 모든 게 밀린다. LCP 예산의 절반 이상을 TTFB 가 먹기도.
  • render-blocking CSS / JS<head> 의 동기 stylesheet·script 가 첫 페인트를 막는다.
  • 늦게 발견되는 hero image — CSS background-image 나 JS 로 주입된 이미지는 브라우저 preload scanner 가 일찍 못 찾아 다운로드가 늦어진다.
  • LCP 요소에 lazy-loading loading="lazy" 를 hero image 에 붙이면 viewport 안인데도 지연 로드 → LCP 폭망. lazy 는 fold 아래 이미지에만.
  • 최적화 안 된 이미지 — 1MB PNG hero 는 그 자체로 느리다. WebP/AVIF + 적절한 크기로.

CLS — 예상치 못한 레이아웃 이동

페이지가 로드되는 동안 이미 보이던 요소가 갑자기 밀려나는 정도의 누적값. 점수 = (이동한 영역의 비율) × (이동 거리의 비율) 을 모든 예상치 못한 shift 에 대해 합산. 0 에 가까울수록 안정적.

나쁜 CLS 시나리오:
1. 텍스트가 먼저 그려진다
2. 위쪽에 image(크기 미지정)가 뒤늦게 도착
3. 텍스트가 아래로 "쿵" 밀림 ← 사용자가 누르려던 버튼이 도망감

좋은 CLS:
1. image 자리를 미리 예약 (width/height 또는 aspect-ratio)
2. image 도착 → 예약된 공간에 채워짐, 아무것도 안 밀림

CLS 를 망치는 주범:

  • 크기 미지정 image / video / iframe — 브라우저가 공간을 예약 못 해 도착 시점에 밀어낸다.
  • 광고 / embed 슬롯 — 동적으로 채워지는 ad·widget 이 자리 예약 없이 들어오면 콘텐츠를 밀어낸다.
  • web font swap — fallback font 로 먼저 그렸다가 웹폰트 도착 시 글자 폭이 바뀌어 reflow (FOUT).
  • 기존 콘텐츠 위에 주입 — 배너·쿠키 알림·"읽기 전" 메시지를 DOM 상단에 삽입하면 아래 전체가 밀린다.

주의 — 사용자 인터랙션 직후 500ms 안의 이동은 "예상된" 것으로 보아 점수에서 제외한다. 버튼 눌러서 펼쳐지는 아코디언은 CLS 에 안 잡힌다.

INP — 상호작용에 대한 반응성

사용자가 클릭·탭·키 입력을 했을 때, 다음 프레임이 화면에 그려질 때까지의 지연. 페이지 수명 동안 발생한 모든 상호작용 중 (대략) 가장 느린 것을 대표값으로 보고한다. "눌렀는데 멈칫한다" 의 정량화.

사용자 클릭
   │
   ├─ input delay ──── main thread 가 다른 일(long task) 중이라 대기
   │
   ├─ processing ───── event handler 실행 (무거우면 길어짐)
   │
   └─ presentation ─── 다음 paint 까지 (DOM 변경 렌더)
                       ↑ 전체 ≤ 200ms 여야 good

INP 를 망치는 주범:

  • long task — main thread 를 50ms 이상 점유하는 JS 작업. 그 동안 들어온 입력은 줄 서서 대기한다.
  • 무거운 event handler — 클릭 한 번에 동기로 큰 계산·대량 DOM 조작을 하면 처리 시간이 길어진다.
  • 큰 DOM — 노드 수천 개면 style/layout 재계산이 비싸져 presentation 단계가 늘어난다.
  • 과도한 JS — hydration·third-party script 가 main thread 를 계속 붙잡으면 모든 상호작용이 느려진다.

Field 데이터 vs Lab 데이터 — 왜 점수가 갈리나

같은 페이지가 Lighthouse 에선 통과하고 Search Console 에선 실패하는 핵심 이유. 둘은 측정 출처가 다르다.

            Lab (Lighthouse)              Field (CrUX)
─────────────────────────────────────────────────────────────────
출처        내 머신/CI 의 1 회 측정        실제 Chrome 사용자 수집
환경        통제된 throttling             제각각 — 구형폰, 느린 망
INP         lab 에선 직접 측정 안 함*      실제 클릭/탭으로 측정
집계        단발 snapshot                 28 일 / 75th percentile
랭킹 사용   X (진단용)                    O (page experience 신호)

* Lighthouse 는 INP 를 직접 못 잰다 — 실제 사용자 상호작용이 있어야 하기 때문. lab 은 TBT(Total Blocking Time) 같은 대리 지표를 본다.

CrUX (Chrome User Experience Report) 는 실제 Chrome 사용자의 데이터를 28 일 rolling 으로 모은다. 보고값은 75th percentile — 즉 "사용자 75% 가 이 값보다 좋게 경험했다". 평균이 아니라 p75 라서 느린 꼬리(구형폰·느린 네트워크)가 점수를 좌우한다.

그래서 흔한 함정:

  • 내 맥북 + 광랜 = lab 만점. 그러나 field 의 p75 는 중급 안드로이드 + 모바일 망 → LCP 4s.
  • field 는 28 일 누적 → 방금 배포한 개선이 즉시 반영 안 됨. 며칠 ~몇 주 기다려야 점수가 움직인다.
  • 트래픽이 적은 페이지는 CrUX 데이터가 origin 단위로 묶이거나 아예 없을 수 있다.

결론 — lab 은 빠른 반복·디버깅용, field 는 진짜 랭킹·실사용 판정용. 둘 다 봐야 한다.

어떻게 측정하나

1. PageSpeed Insights

URL 만 넣으면 lab(Lighthouse) + field(CrUX) 를 한 화면에 보여준다. 가장 빠른 시작점. 상단의 "실제 사용자 데이터" 섹션이 field, 하단의 진단이 lab.

2. web-vitals JS 라이브러리

실제 방문자에게서 직접 RUM(Real User Monitoring) 으로 측정해 내 분석 endpoint 로 보낸다. Google 이 권장하는 field 측정 방식:

import {onLCP, onCLS, onINP} from "web-vitals";

function send(metric) {
  // 분석 서버로 전송 (beacon 이 페이지 이탈에도 안전)
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,   // "good" | "needs-improvement" | "poor"
    id: metric.id,
  });
  navigator.sendBeacon("/analytics", body);
}

onLCP(send);
onCLS(send);
onINP(send);

이 데이터를 모으면 CrUX 의 28 일 지연 없이, 내 실사용자 분포를 직접 본다.

3. Search Console — Core Web Vitals 리포트

CrUX field 데이터를 URL 그룹별로 묶어 "good / needs improvement / poor" 로 분류한다. 사이트 전체에서 어떤 URL 패턴이 실패하는지 한눈에. 랭킹에 쓰이는 바로 그 데이터.

4. Chrome DevTools — Performance 패널

녹화하면 LCP 마커, layout shift 영역, long task(빨간 삼각형 표시) 를 timeline 에서 직접 본다. 어떤 task 가 main thread 를 몇 ms 붙잡는지, 어떤 요소가 shift 했는지 원인을 짚을 때.

각 지표 고치기

LCP 고치기

  • hero image 를 preload 하고 fetchpriority="high" 부여 — 브라우저가 가장 먼저 받게:
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high">

<!-- 또는 img 자체에 -->
<img src="/hero.avif" fetchpriority="high" alt="...">
  • LCP 이미지에는 절대 loading="lazy" 를 붙이지 말 것.
  • render-blocking CSS 최소화 — critical CSS 인라인, 나머지는 비동기 로드.
  • TTFB 단축 — CDN edge cache, 서버 응답 캐싱, static 생성.
  • 이미지 포맷·크기 최적화(WebP/AVIF, responsive srcset). 이미지 자체 비중이 LCP 의 대부분.

브라우저가 무엇을 먼저 발견하고 받는지는 how-browsers-render-pages how-resource-hints-work 에서 더 깊게 다룬다. 이미지 비중 자체를 줄이는 건 image-optimization 가이드 참조.

CLS 고치기

  • 모든 <img> / <video> width · height 속성 명시 → 브라우저가 aspect-ratio 로 공간 예약. CSS aspect-ratio 도 가능:
<!-- 브라우저가 16:9 공간을 미리 잡아둠 -->
<img src="/photo.jpg" width="1600" height="900" alt="...">

/* CSS 방식 */
.thumb { aspect-ratio: 16 / 9; width: 100%; }
  • 광고·embed 슬롯에 min-height 로 공간 예약 → 채워질 때 안 밀림.
  • web font 는 font-display: optional 또는 swap + preload + metric-override(size-adjust) 로 swap reflow 억제. 자세한 건 how-web-fonts-load 가이드.
  • 동적 콘텐츠는 기존 콘텐츠 아래에 삽입하거나, 삽입 자리를 미리 예약.

INP 고치기

  • long task 쪼개기 — 50ms 넘는 작업을 잘게 나눠 중간에 main thread 를 양보(yield):
// 무거운 루프를 yield 로 끊어 입력이 끼어들 틈을 준다
async function processChunks(items) {
  for (const item of items) {
    doWork(item);
    // 모던 브라우저: scheduler.yield(), 폴백: setTimeout
    if (window.scheduler && scheduler.yield) {
      await scheduler.yield();
    } else {
      await new Promise((r) => setTimeout(r, 0));
    }
  }
}
  • event handler 안의 무거운 계산은 requestIdleCallback 이나 web worker 로 미룬다 — 시각적 피드백(클릭 반응)을 먼저 그리고 무거운 일은 나중에.
  • DOM 크기 줄이기 — 가상 스크롤·페이지네이션으로 노드 수 통제.
  • JS 페이로드 줄이기 — 안 쓰는 third-party script 제거, code splitting, hydration 범위 축소.

왜 중요한가

Core Web Vitals 는 Google 의 page experience 랭킹 신호의 일부다. 동등한 콘텐츠끼리 경쟁할 때 tie-breaker 로 작동한다 — 압도적 1 순위 요인은 아니지만, 통과 못 하면 불이익이 있고 통과는 기본 위생이다. 게다가 LCP·CLS·INP 는 그 자체로 실제 사용자 이탈·전환과 직결되는 UX 지표라, 랭킹을 떠나서도 고칠 값어치가 있다.

참고 자료

요약

  • Core Web Vitals = LCP(≤2.5s) · CLS(≤0.1) · INP(≤200ms). 세 지표 모두 good 이어야 통과.
  • INP 는 2024 년 3 월 FID 를 대체 — 첫 입력만이 아니라 전체 상호작용 반응성을 본다.
  • Lab(Lighthouse) 은 1 회 통제 측정·진단용, Field(CrUX) 는 28 일 p75 실사용·랭킹용. lab 통과해도 field 실패 가능.
  • LCP 망침 — 느린 TTFB, render-blocking, 늦게 발견되는 hero, LCP 요소 lazy-load. 고침 — preload + fetchpriority=high, lazy 금지.
  • CLS 망침 — 크기 미지정 미디어·광고, font swap, 상단 주입. 고침 — width/height·aspect-ratio·슬롯 예약·font-display.
  • INP 망침 — long task, 무거운 handler, 큰 DOM, 과도한 JS. 고침 — task 쪼개기·yield, worker, JS 줄이기.
  • 측정 — PageSpeed Insights, web-vitals 라이브러리(RUM), Search Console 리포트, DevTools Performance 패널.
가이드 목록으로