💡 핵심 요약: Core Web Vitals는 구글이 정한 사용자 체감 성능 3대 지표입니다. LCP(가장 큰 콘텐츠가 그려지는 시간, 양호 2.5초 이하), CLS(화면이 덜컹거리는 정도, 양호 0.1 이하), INP(클릭·입력에 화면이 반응하는 시간, 양호 200ms 이하)를 뜻합니다. 이 글은 세 지표가 각각 무엇을 재고, 왜 나빠지며, 어떤 코드로 개선하는지를 개념 중심으로 정리한 안내서입니다.
웹사이트가 "빠르다"는 느낌은 사람마다 다릅니다. 누구는 로딩 속도를, 누구는 버튼을 눌렀을 때의 반응을 봅니다. 구글은 이 모호한 체감을 측정 가능한 숫자로 바꾸려고 Core Web Vitals(핵심 웹 지표) 를 정했습니다. 지금 기준으로 세 가지입니다.
| 지표 | 무엇을 재나 | 양호 | 개선 필요 | 나쁨 |
|---|---|---|---|---|
| LCP | 로딩 체감 | ≤ 2.5초 | ≤ 4.0초 | > 4.0초 |
| CLS | 시각적 안정성 | ≤ 0.1 | ≤ 0.25 | > 0.25 |
| INP | 반응성 | ≤ 200ms | ≤ 500ms | > 500ms |
한 가지 짚을 점은 이 값이 실제 사용자 데이터의 75 백분위수로 판정된다는 것입니다. 방문의 75%가 양호 기준을 넘겨야 그 페이지가 "양호"로 분류됩니다. 내 빠른 노트북에서 잘 나온다고 통과가 아니라, 느린 기기와 네트워크까지 포함한 다수의 경험이 기준이라는 뜻입니다.
세 지표는 성능 순위 신호이기도 해서 검색 노출과도 연결됩니다. 다만 순위보다 먼저 생각할 것은, 이 숫자들이 결국 사용자가 답답함을 느끼는 지점을 가리킨다는 사실입니다.
LCP(Largest Contentful Paint) 는 화면에서 가장 큰 콘텐츠 요소(주로 히어로 이미지나 큰 제목 텍스트)가 그려지는 시점을 잽니다. 사용자가 "아, 페이지가 떴다"고 느끼는 순간과 가장 가깝습니다.
LCP가 느려지는 흔한 원인은 이렇습니다.
가장 효과가 큰 개선은 핵심 이미지를 브라우저가 빨리 발견하게 만드는 것입니다. 브라우저는 HTML을 위에서부터 읽는데, CSS 안에 숨은 배경 이미지나 자바스크립트로 나중에 삽입되는 이미지는 발견이 늦습니다. 그래서 히어로 이미지는 <img>로 직접 넣고 우선순위를 높여줍니다.
html<!-- 첫 화면의 핵심 이미지: 우선순위를 올리고 즉시 로드 --> <img src="/hero.webp" alt="서비스 대표 이미지" width="1200" height="630" fetchpriority="high" decoding="async" />
반대로 첫 화면 아래에 있는 이미지는 loading="lazy"로 미뤄, 정작 중요한 LCP 이미지가 대역폭을 먼저 쓰게 합니다.
Next.js를 쓴다면 next/image의 priority 속성이 같은 일을 해줍니다. 이 속성이 붙은 이미지는 미리 로드되고 지연 로딩에서 제외됩니다.
plainimport Image from 'next/image'; export function Hero() { return ( <Image src="/hero.webp" alt="서비스 대표 이미지" width={1200} height={630} priority // LCP 후보 이미지에만 붙입니다. 남발하면 효과가 사라집니다. /> ); }
폰트로 그려지는 큰 제목이 LCP인 경우도 많습니다. 이때는 웹폰트를 preload하고 font-display: swap으로 폰트가 늦어도 텍스트가 먼저 보이게 합니다.
CLS(Cumulative Layout Shift) 는 로딩 도중 이미 보이던 요소가 갑자기 밀려나는 정도를 누적해서 잽니다. 기사를 읽으려는데 위에서 광고가 뒤늦게 끼어들며 본문을 아래로 밀어버리는 경험, 버튼을 누르려는 순간 레이아웃이 움직여 엉뚱한 걸 누르는 경험이 전부 CLS입니다.
원인은 대개 크기가 예약되지 않은 요소입니다. 브라우저는 이미지나 광고의 최종 크기를 모른 채 일단 자리 없이 그렸다가, 나중에 크기를 알게 되면 그만큼 주변을 밀어냅니다.
핵심 해법은 들어올 자리를 미리 잡아두는 것입니다. 이미지에는 항상 width와 height(또는 aspect-ratio)를 지정합니다. 그러면 브라우저가 실제 파일이 오기 전에도 빈 공간을 정확히 비워둡니다.
html<!-- 나쁨: 크기 정보가 없어 이미지가 도착하면 아래 콘텐츠가 밀림 --> <img src="/thumb.webp" alt="썸네일" /> <!-- 좋음: 자리를 먼저 예약 --> <img src="/thumb.webp" alt="썸네일" width="320" height="180" />
반응형이라 픽셀값을 고정하기 어렵다면 CSS의 aspect-ratio로 비율만 잡아둬도 됩니다.
css.thumb { width: 100%; aspect-ratio: 16 / 9; /* 실제 이미지가 오기 전에도 높이를 확보 */ object-fit: cover; }
이 밖에 자주 걸리는 함정도 있습니다.
size-adjust 계열 속성으로 완화할 수 있습니다.INP(Interaction to Next Paint) 는 2024년 3월에 기존 FID를 대체한 지표로, 사용자의 상호작용(클릭·탭·키 입력)부터 화면이 실제로 반응해 다시 그려지기까지의 지연을 잽니다. 페이지 로딩이 아니라 로딩 이후의 반응성을 본다는 점이 LCP와 다릅니다. 2026년 기준으로 세 지표 중 가장 자주 실패하는 항목이기도 합니다.
INP가 나빠지는 주된 이유는 긴 작업(Long Task) 입니다. 자바스크립트는 기본적으로 한 줄에서 실행되기 때문에, 클릭 이벤트 처리 중 무거운 계산이 오래 돌면 그동안 브라우저는 화면을 다시 그리지 못하고 멈춰 있습니다.
가장 기본적인 처방은 긴 작업을 잘게 쪼개고, 중간에 브라우저에 제어권을 돌려주는 것입니다. 핵심 반응(예: 버튼이 눌린 표시)을 먼저 처리하고, 무거운 계산은 뒤로 미룹니다.
javascript// 나쁨: 클릭 한 번에 무거운 계산이 통째로 돌아 화면이 멈춤 button.addEventListener('click', () => { const result = heavyComputation(items); // 수백 ms 동안 메인 스레드 점유 render(result); });
javascript// 좋음: 브라우저가 반응할 틈(yield)을 준 뒤 무거운 작업을 이어감 function yieldToMain() { return new Promise((resolve) => setTimeout(resolve, 0)); } button.addEventListener('click', async () => { showPending(); // 눌렸다는 반응을 즉시 보여줌 await yieldToMain(); // 여기서 브라우저가 화면을 갱신할 수 있음 const result = heavyComputation(items); render(result); });
최신 브라우저는 이 패턴을 위한 scheduler.yield()를 제공합니다. setTimeout보다 우선순위를 잘 유지하면서 제어권을 넘겨줍니다(미지원 환경을 위한 대비만 해두면 됩니다).
javascriptasync function yieldToMain() { if ('scheduler' in window && 'yield' in scheduler) { return scheduler.yield(); } return new Promise((resolve) => setTimeout(resolve, 0)); }
그 밖에도 도움이 되는 처방을 정리하면 다음과 같습니다.
측정 방식은 크게 두 갈래입니다.
내 사이트의 실제 값을 코드로 직접 수집하고 싶다면 구글이 만든 web-vitals 라이브러리가 간단합니다.
javascriptimport { onLCP, onCLS, onINP } from 'web-vitals'; function report(metric) { // 콘솔 확인용. 실제로는 분석 서버로 전송합니다. console.log(metric.name, Math.round(metric.value), metric.rating); } onLCP(report); onCLS(report); onINP(report);
진단은 랩 도구로 빠르게, 판정과 우선순위는 필드 데이터로. 이 두 가지를 함께 봐야 "내 눈에는 빠른데 왜 점수가 나쁘지?" 같은 착오를 피할 수 있습니다.
세 지표는 결국 서로 다른 순간의 답답함을 가리킵니다. LCP는 "언제 다 뜨나", CLS는 "왜 자꾸 움직이나", INP는 "왜 눌러도 반응이 없나" 입니다. 개선의 우선순위를 정한다면, 필드 데이터에서 가장 많이 실패하는 지표부터 손대는 것이 효율적입니다. 통계적으로는 INP가 가장 자주 걸리고, 그다음이 LCP인 경우가 많습니다.
한 번에 완벽하게 만들려 하기보다, 지표 하나를 골라 원인을 좁히고 개선한 뒤 필드 데이터로 확인하는 순환을 반복하는 편이 현실적입니다. 이 글의 코드들은 개념을 잡기 위한 최소 예시이므로, 실제 적용 시에는 각자의 프레임워크와 측정값에 맞게 조정해서 쓰시길 권합니다.
참고 자료
// Comments