어느 날 갑자기 검색 순위가 떨어지는 경험을 해본 적 있을까? 분명 최적화도 했는데 구글 서치 콘솔에서 경고 메시지가 떠있다. 바로 Core Web Vitals 때문일 가능성이 높다. 다만 대부분의 개발자나 마케터들은 LCP, FID, CLS가 정확히 뭔지도 모른 채, 구글에서 제시한 수치만 맞추려고 급급하다. 이번에는 Core Web Vitals가 정확히 뭔지, 왜 SEO 순위에 영향을 미치는지, 그리고 어떻게 개선하는지 실무 관점에서 완벽하게 정리해서 소개하겠다.
Core Web Vitals는 2021년 6월부터 구글의 공식 검색 순위 결정 요소로 포함됐다. 사용자 경험을 수치화한 지표 3가지인데, 각각 로딩 속도, 상호작용 반응성, 시각적 안정성을 측정한다.
LCP(Largest Contentful Paint) - 페이지의 주요 콘텐츠가 화면에 보이는 시간. 2.5초 이내가 목표다. 이미지, 비디오, 텍스트 블록 중 가장 큰 것이 렌더링되는 순간을 측정한다. 사용자가 "이 페이지가 로드됐나?"라고 느끼는 순간이므로 매우 중요하다.
FID(First Input Delay) - 사용자가 처음 상호작용(클릭, 터치, 키 입력)을 했을 때 브라우저가 응답하는 데 걸리는 시간. 100ms 이내가 목표다. 메인 스레드가 다른 작업으로 바쁠 때 사용자의 입력이 지연되는 현상을 측정한다.
CLS(Cumulative Layout Shift) - 페이지 로드 중에 예기치 않게 레이아웃이 변하는 정도. 0.1 이하가 목표다. 광고나 이미지가 늦게 로드되면서 아래쪽 콘텐츠가 밀려나는 현상을 측정한다.
| 지표 | 측정 대상 | 목표 | 개선 난이도 |
|---|---|---|---|
| LCP | 주요 콘텐츠 로딩 속도 | 2.5초 이내 | 중간 |
| FID | 사용자 입력 반응 시간 | 100ms 이내 | 높음 |
| CLS | 레이아웃 변동 | 0.1 이하 | 낮음 |
우선 자신의 사이트가 현재 어느 상태인지 정확히 파악해야 개선 작업을 시작할 수 있다. 구글에서 제공하는 도구들을 활용하자.
구글 서치 콘솔(Google Search Console) - 좌측 메뉴에서 "Core Web Vitals" 섹션을 확인하면 모바일/데스크톱별로 "양호", "개선 필요", "불량"이 실시간으로 표시된다. 이것은 실제 사용자의 방문 데이터를 기반으로 한 결과이므로 가장 신뢰할 수 있다. 만약 "개선 필요" 상태라면, 어떤 지표가 문제인지 클릭해서 상세 보고서를 본다.
PageSpeed Insights - pagespeed.web.dev에 접속해서 URL을 입력하면 LCP, FID, CLS를 포함한 모든 성능 지표를 1~100점 단계로 평가해준다. "필드 데이터"는 실제 사용자 데이터이고, "랩 데이터"는 에뮬레이션 환경의 측정값이다. 두 값의 차이가 크면 실제 사용자 환경이 더 악화돼 있다는 뜻이다.
LCP는 이미지나 큰 텍스트 블록이 화면에 나타나는 시간을 측정한다. 가장 일반적인 원인 3가지와 해결 방법을 알아보자.
① 느린 서버 응답 시간(Server Response Time)
✗ 잘못된 방식 - 페이지 요청 시 데이터베이스 쿼리를 모두 기다린 후 HTML을 반환
<?php
// 사용자 페이지 요청 시 모든 게시물을 조회한 후 렌더링
$articles = $db->query("SELECT * FROM articles WHERE status = 'published' ORDER BY created_at DESC LIMIT 50");
echo "<!DOCTYPE html>";
echo "<title>Blog</title>";
echo "<body>";
foreach($articles as $article) {
echo "<article>{$article['title']}</article>";
}
echo "</body>";
?>
✓ 올바른 방식 - 주요 콘텐츠만 우선 렌더링, 나머지는 비동기로 로드
<?php
// 주요 콘텐츠(상단 3개)만 즉시 렌더링
$featured = $db->query("SELECT id, title, image FROM articles WHERE featured = 1 LIMIT 3");
echo "<!DOCTYPE html>";
echo "<title>Blog</title>";
echo "<body>";
foreach($featured as $article) {
echo "<article>{$article['title']}</article>";
}
// 나머지 목록은 JavaScript로 비동기 로드
echo "<div id='more-articles'></div>";
echo "<script src='/js/load-more.js' async></script>";
echo "</body>";
?>
결과: 초기 HTML 응답 시간이 800ms에서 200ms로 단축되면 LCP가 2~3초에서 1초로 개선된다.
② 큰 이미지 파일
✗ 잘못된 방식 - 원본 고화질 이미지를 그대로 전송
<img src="/images/hero-image.jpg" alt="hero" width="1200" height="600" />
<!-- hero-image.jpg 파일 크기: 2.5MB -->
✓ 올바른 방식 - 우선 로드 이미지(srcset)와 WebP 포맷 활용
<img
srcset="
/images/hero-image-small.webp 480w,
/images/hero-image-medium.webp 800w,
/images/hero-image-large.webp 1200w"
src="/images/hero-image.jpg"
alt="hero"
width="1200"
height="600"
loading="lazy"
/>
<!-- WebP 포맷: 2.5MB → 800KB로 단축 -->
결과: 이미지 전송 크기 65% 감소로 LCP가 3초에서 1.5초로 개선.
③ JavaScript 렌더링 지연
✗ 잘못된 방식 - 헤드에 JavaScript를 동기로 로드
<!DOCTYPE html>
<html>
<head>
<!-- 이 스크립트가 완전히 로드되고 실행될 때까지 HTML 파싱 멈춤 -->
<script src="/js/analytics.js"></script>
<script src="/js/ads.js"></script>
<script src="/js/tracking.js"></script>
</head>
<body>
<!-- body 콘텐츠 -->
</body>
✓ 올바른 방식 - 필수 JS만 동기, 나머지는 비동기/지연 로드
<!DOCTYPE html>
<html>
<head>
<!-- 필수 스크립트만 동기로 로드 -->
<script src="/js/core.js"></script>
</head>
<body>
<!-- body 콘텐츠 -->
<!-- 분석/광고는 비동기로 로드 -->
<script async src="/js/analytics.js"></script>
<script async src="/js/ads.js"></script>
<script defer src="/js/tracking.js"></script>
</body>
</html>
결과: 메인 콘텐츠 렌더링이 분석/광고 스크립트에 의해 지연되는 문제 해결.
CLS는 개선이 가장 쉬운 지표다. 대부분은 간단한 CSS와 HTML 속성 수정으로 해결된다.
① 이미지에 너비/높이 속성 명시
✗ 잘못된 방식 - 너비/높이 미지정으로 이미지 로드 후 공간 확보
<img src="/images/product.jpg" alt="product" />
<!-- 이미지가 로드되기 전에는 0px의 높이를 차지하다가,
로드 후 갑자기 높이가 600px로 변해서 아래 콘텐츠가 밀려남 -->
✓ 올바른 방식 - 너비/높이 미리 지정
<img src="/images/product.jpg" alt="product" width="600" height="400" />
<!-- 또는 aspect-ratio CSS 사용 -->
<img src="/images/product.jpg" alt="product" style="aspect-ratio: 3/2;" />
② 광고/내장 콘텐츠용 공간 사전 예약
✗ 잘못된 방식 - 광고가 로드될 때까지 공간 없음
<div id="ad-container">
<!-- Google AdSense 광고 -->
<ins class="adsbygoogle" data-ad-client="ca-pub-xxx"></ins>
</div>
<p>다음 문단...</p>
✓ 올바른 방식 - 광고 영역의 높이를 미리 정의
<div id="ad-container" style="min-height: 250px; width: 100%;">
<ins class="adsbygoogle" data-ad-client="ca-pub-xxx"></ins>
</div>
<p>다음 문단...</p>
결과: CLS 점수가 0.5 이상에서 0.05 이하로 개선되는 경우가 많다.
FID는 가장 개선이 어려운 지표다. JavaScript의 무거운 작업이 메인 스레드를 블로킹하는 경우가 대부분이다.
① 큰 JavaScript 번들을 Code Splitting으로 분해
✗ 잘못된 방식 - 모든 기능을 하나의 번들로 로드
// bundle.js (2MB)
// - 장바구니 기능
// - 결제 기능
// - 채팅 기능
// - 분석 기능
// 페이지 방문 직후 메인 스레드가 2MB 번들 파싱으로 2초 이상 블로킹됨
import cart from './modules/cart.js';
import checkout from './modules/checkout.js';
import chat from './modules/chat.js';
import analytics from './modules/analytics.js';
window.app = { cart, checkout, chat, analytics };
✓ 올바른 방식 - 필요한 것만 먼저 로드
// main.js (200KB) - 필수 기능만
import cart from './modules/cart.js';
window.app = { cart };
// 사용자의 상호작용이 있을 때만 나머지 로드
document.getElementById('checkout-btn').addEventListener('click', async () => {
const checkout = await import('./modules/checkout.js');
// checkout 모듈 사용
});
// 페이지 유휴 시간에 선택적으로 로드 (requestIdleCallback 사용)
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
import('./modules/analytics.js');
});
}
결과: 초기 JavaScript 파싱 시간 90% 단축으로 메인 스레드 차단 현상 해결.
② Web Worker로 무거운 작업을 별도 스레드에서 처리
✗ 잘못된 방식 - 메인 스레드에서 데이터 처리
// 사용자 클릭 시 10,000개 데이터를 메인 스레드에서 처리
document.getElementById('search-btn').addEventListener('click', () => {
const results = largeDataset.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// 이 작업에 500ms 소요되면 FID가 500ms 이상
renderResults(results);
});
✓ 올바른 방식 - Web Worker에서 처리
// main.js
const worker = new Worker('/js/search-worker.js');
document.getElementById('search-btn').addEventListener('click', () => {
// 메인 스레드는 즉시 반응 (FID 매우 짧음)
worker.postMessage({
type: 'SEARCH',
data: largeDataset,
term: searchTerm
});
});
worker.onmessage = (e) => {
// 백그라운드에서 처리된 결과 수신
renderResults(e.data);
};
// search-worker.js
self.onmessage = (e) => {
if (e.data.type === 'SEARCH') {
const results = e.data.data.filter(item =>
item.name.toLowerCase().includes(e.data.term.toLowerCase())
);
self.postMessage(results);
}
};
결과: 메인 스레드가 차단되지 않아 사용자 입력에 즉시 반응.
Core Web Vitals는 이제 검색 순위를 결정하는 필수 요소가 됐다. 작은 최적화 하나가 모여서 1~2초의 속도 개선을 만들고, 그것이 검색 순위 상승으로 이어진다는 점을 잊지 말자. 이 글의 "구글 서치 콘솔과 PageSpeed Insights 진단" 부분을 참고해 먼저 자신의 사이트 현황을 파악하고, "LCP", "CLS", "FID" 개선 방법 중 해당 지표부터 차례로 최적화하면, 분명히 검색 순위 회복을 경험할 수 있을 것이다.