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 여야 goodLCP 를 망치는 주범:
- 느린 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 여야 goodINP 를 망치는 주범:
- 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 로 공간 예약. CSSaspect-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 지표라, 랭킹을 떠나서도 고칠 값어치가 있다.
참고 자료
- web.dev — Core Web Vitals — web.dev/articles/vitals
- INP 가 FID 를 대체 — web.dev/blog/inp-cwv-launch
- web-vitals JS 라이브러리 — github.com/GoogleChrome/web-vitals
- PageSpeed Insights — pagespeed.web.dev
요약
- 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 패널.