개발자라면 구글 Search Console에서 Core Web Vitals 지표가 '나쁨' 상태로 떨어진 적 있을까. 사용자는 여전히 사이트를 이용하는데 갑자기 검색 순위가 밀려나가고, 모바일 트래픽이 뚝 떨어진다. 다만 대부분의 개발자들은 Core Web Vitals가 정확히 뭔지, 어떤 지표가 얼마나 떨어졌는지, 왜 개선되지 않는지 제대로 모른 채로 우왕좌왕한다. 이번에는 Core Web Vitals의 세 가지 핵심 지표가 정확히 뭔지, 실제 데이터로 확인하는 방법, 그리고 각 지표별로 성능을 개선하는 구체적인 방법을 완벽하게 정리해서 소개하겠다.
Core Web Vitals는 구글이 2021년부터 검색 순위 알고리즘에 반영한 성능 지표다. 사용자가 느끼는 실제 체감 속도와 안정성을 측정하는 세 가지 항목이 있다.
LCP(Largest Contentful Paint)는 페이지에서 가장 큰 콘텐츠(텍스트, 이미지, 동영상)가 화면에 나타나는 시간이다. 사용자가 페이지를 열었을 때 '콘텐츠가 로딩됐는가'를 판단하는 지표다. 목표는 2.5초 이하다. LCP가 느리면 사용자는 페이지가 로딩 중이라고 생각하고 떠난다.
FID(First Input Delay)는 사용자가 버튼을 클릭하거나 텍스트를 입력했을 때 브라우저가 반응하는 시간이다. 페이지가 로딩됐지만 클릭이 먹히지 않는 그 답답한 순간을 측정한다. 목표는 100ms 이하다. 다만 구글은 최근 FID 대신 INP(Interaction to Next Paint)로 지표를 변경했으니 참고하자.
CLS(Cumulative Layout Shift)는 페이지가 로딩되면서 레이아웃이 뜻밖에 밀리는 현상의 총합이다. 광고, 이미지, 폰트가 뒤늦게 로딩되면서 아래 콘텐츠를 밀어내는 그 짜증 나는 경험을 수치화한다. 목표는 0.1 이하다.
먼저 Search Console에서 웹사이트의 Core Web Vitals 실제 데이터를 확인해야 한다. 구글 계정으로 search.google.com/search-console에 접속한 후 좌측 메뉴의 '웹사이트 개선'에서 'Core Web Vitals'를 클릭한다.
이 페이지에는 모바일과 데스크톱 데이터가 따로 표시된다. 구글은 모바일 우선 색인을 중심으로 삼으니 모바일 지표가 낮으면 반드시 개선해야 한다. 각 지표 옆에 '나쁨', '개선 필요', '양호' 세 가지 상태가 표시되는데, '나쁨'이 하나라도 있으면 검색 순위 페널티를 받는다.
Search Console 데이터는 실제 사용자의 Chrome 브라우저에서 수집한 정보라 가장 신뢰할 수 있다. 다만 데이터가 매일 업데이트되는 건 아니고, 최소 28일 이상의 데이터가 쌓여야 정확한 지표를 보여준다. 새로운 사이트라면 처음 몇 주는 데이터가 부족할 수 있다.
LCP가 느린 가장 흔한 원인은 대용량 이미지와 렌더링 블로킹 리소스다.
✗ LCP가 느린 경우
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
<script src="/js/jquery.min.js"></script>
</head>
<body>
<img src="/images/hero-banner.jpg" width="1920" height="600" alt="히어로 배너" />
</body>
</html>
이 코드는 CSS와 JavaScript가 모두 로딩될 때까지 LCP가 지연된다. 여기에 대용량 JPG 이미지까지 로딩되면 3초를 넘는다.
✓ LCP 개선된 경우
<!DOCTYPE html>
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<style>
body { margin: 0; padding: 0; }
.hero { width: 100%; height: auto; }
</style>
</head>
<body>
<img src="/images/hero-banner.webp"
srcset="/images/hero-banner-mobile.webp 480w, /images/hero-banner.webp 1920w"
width="1920" height="600" alt="히어로 배너"
loading="lazy" class="hero" />
<link rel="stylesheet" href="/css/style.css">
<script src="/js/jquery.min.js" defer></script>
</body>
</html>
개선 사항은 다음과 같다. 첫째, 이미지를 WebP 포맷으로 변환해 크기를 60% 이상 줄였다. 둘째, srcset으로 모바일에서는 작은 이미지를 받도록 설정했다. 셋째, 렌더링 블로킹 CSS를 인라인 스타일로 변경해 스타일 로딩 시간을 줄였다. 넷째, JavaScript에 defer 속성을 추가해 비동기 로딩하도록 했다.
추가로 PHP에서 응답 헤더에 gzip 압축을 활성화하면 더 빠르다.
<?php
if (substr_count($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) {
ob_start('ob_gzhandler');
} else {
ob_start();
}
?>
INP(구 FID)가 느린 원인은 메인 스레드가 과도한 JavaScript 작업에 점유되는 것이다.
✗ INP가 느린 경우
document.getElementById('search-btn').addEventListener('click', function() {
let results = [];
for (let i = 0; i < 1000000; i++) {
results.push(processData(i));
}
displayResults(results);
});
이 코드는 클릭 후 백만 개의 데이터를 메인 스레드에서 동기 처리한다. 사용자의 입력이 완전히 끝날 때까지 브라우저는 응답할 수 없다.
✓ INP 개선된 경우
document.getElementById('search-btn').addEventListener('click', async function() {
const results = await processDataAsync();
displayResults(results);
});
async function processDataAsync() {
return new Promise((resolve) => {
let results = [];
let i = 0;
function processChunk() {
const chunkSize = 1000;
for (let j = 0; j < chunkSize && i < 1000000; j++, i++) {
results.push(processData(i));
}
if (i < 1000000) {
setTimeout(processChunk, 0);
} else {
resolve(results);
}
}
processChunk();
});
}
개선된 코드는 대용량 처리를 작은 청크 단위로 쪼개서 setTimeout으로 메인 스레드가 숨을 쉴 틈을 준다. 사용자가 클릭하면 즉시 피드백이 오고, 백그라운드에서 데이터가 차근차근 처리된다. 또는 Web Worker를 사용해서 별도 스레드에서 처리하는 게 더 좋다.
CLS가 높은 원인은 대부분 크기가 정해지지 않은 이미지, 폰트 로딩 지연, 뒤늦게 삽입되는 광고와 배너다.
✗ CLS가 높은 경우
<article>
<h1>기술 블로그 제목</h1>
<img src="/images/article.jpg" alt="기사 이미지" />
<p>본문 내용...</p>
</article>
<style>
img { max-width: 100%; height: auto; }
</style>
이 코드에서 이미지 크기가 정해지지 않았다. 원본 이미지가 로딩될 때까지 이미지 공간이 0이고, 로딩되는 순간 아래 콘텐츠가 밀린다.
✓ CLS 개선된 경우
<article>
<h1>기술 블로그 제목</h1>
<div style="width: 100%; max-width: 800px; aspect-ratio: 16 / 9;">
<img src="/images/article.jpg" alt="기사 이미지"
style="width: 100%; height: 100%; object-fit: cover;" />
</div>
<p>본문 내용...</p>
</article>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Noto Sans KR', sans-serif; }
article { max-width: 800px; margin: 0 auto; }
</style>
개선된 코드는 aspect-ratio CSS로 이미지 공간을 미리 확보한다. object-fit: cover로 이미지 비율을 유지하면서 영역을 채운다. 또한 폰트를 display=swap 파라미터로 로딩해서 폰트 완성 전에 대체 폰트로 먼저 텍스트를 표시하도록 한다. 이렇게 하면 폰트 로딩 지연으로 인한 레이아웃 이동을 방지할 수 있다.
Search Console의 데이터는 28일 단위 실제 사용자 데이터인 반면, PageSpeed Insights는 실시간으로 각 페이지의 성능을 상세히 진단할 수 있다. 다만 시뮬레이션 환경(3G 네트워크, 저사양 기기)에서의 테스트이므로 실제보다 느리게 나올 수 있다.
URL을 입력하면 모바일과 데스크톱 각각에 대해 LCP, INP, CLS 뿐만 아니라 TTFB(Time to First Byte), FCP(First Contentful Paint) 등 추가 지표도 보여준다. 각 항목별로 '개선 권장사항'이 구체적으로 나오므로 이를 우선순위 따라 개선하면 된다.
Core Web Vitals 개선은 선택이 아닌 필수다. 특히 경쟁이 치열한 키워드에서는 같은 콘텐츠라도 성능이 낮으면 순위가 밀린다. Search Console에서 '나쁨' 상태인 지표가 하나도 없도록 만드는 것을 첫 목표로 삼자. 그 다음은 '개선 필요' 상태도 모두 '양호'로 옮기는 것이다. 작은 최적화들이 모여서 큰 성능 향상을 만든다는 점을 잊지 말자. 이 글의 LCP, INP, CLS 각 섹션의 구체적인 개선 방법을 참고해서 우선순위 높은 지표부터 개선하면, Search Console에서 다음 업데이트 때 '양호' 상태로 전환되는 것을 확인할 수 있을 것이다.