뛰어난 사용자 경험을 위한 핵심 웹 바이탈 마스터하기
Lukas Schneider
DevOps Engineer · Leapcell

소개
끊임없이 진화하는 웹 개발 환경에서 원활하고 매력적인 사용자 경험을 제공하는 것이 무엇보다 중요합니다. 오늘날 사용자는 번개처럼 빠른 로딩 시간, 즉각적인 응답성, 안정적인 시각적 레이아웃을 기대합니다. 이러한 기대를 충족시키지 못하면 높은 이탈률, 전환율 감소, 궁극적으로는 브랜드 평판 손상으로 이어질 수 있습니다. 사용자 경험의 이러한 중요한 측면을 해결하기 위해 Google은 Core Web Vitals를 도입했습니다. 이는 사용자의 실제 경험을 측정하는 세 가지 주요 지표인 Largest Contentful Paint(LCP), Interaction to Next Paint(INP), Cumulative Layout Shift(CLS)로 구성됩니다. 이 글에서는 이러한 중요한 지표를 최적화하기 위한 포괄적인 전략을 자세히 살펴보고, 개발자가 실제 잠재고객을 만족시키는 웹사이트를 만드는 데 필요한 지식과 도구를 제공할 것입니다.
핵심 웹 바이탈 이해하기
최적화에 앞서 각 핵심 웹 바이탈에 대한 명확한 이해를 바탕으로 시작하겠습니다.
- Largest Contentful Paint (LCP): 이 지표는 뷰포트 내에서 보이는 가장 큰 이미지 또는 텍스트 블록의 렌더링 시간을 측정합니다. 본질적으로 웹 페이지의 주요 콘텐츠가 사용자에게 얼마나 빨리 표시되는지를 알려줍니다. 낮은 LCP 점수는 빠른 로딩과 우수한 사용자 경험을 나타냅니다.
- Interaction to Next Paint (INP): INP는 사용자가 방문 중에 수행하는 모든 상호 작용의 지연 시간을 측정하여 페이지의 응답성을 평가합니다. 이는 상호 작용 시간을 기준으로 모든 사용자 상호 작용이 거의 모두 아래에 있었던 단일 값을 보고합니다. 여기에는 클릭, 탭, 스크롤이 포함됩니다. 낮은 INP 점수는 페이지가 사용자 입력에 매우 잘 반응함을 의미합니다.
- Cumulative Layout Shift (CLS): CLS는 웹 페이지 수명 동안 시각적 요소의 예기치 않은 이동을 정량화합니다. 이미지나 텍스트 블록이 예기치 않게 뛰어다니며 사용자의 읽기 또는 클릭 흐름을 방해하는 것을 상상해 보세요. 낮은 CLS 점수는 안정적이고 시각적으로 예측 가능한 페이지를 의미합니다.
이러한 지표를 최적화하는 것은 단순히 검색 엔진 순위에 대한 확인란을 선택하는 것이 아니라 사용자가 웹사이트와 상호 작용하고 인식하는 방식을 근본적으로 개선하는 것입니다.
핵심 웹 바이탈 최적화 전략
LCP, INP, CLS를 개선하려면 웹 성능의 다양한 측면을 대상으로 하는 다각적인 접근 방식이 필요합니다.
Largest Contentful Paint(LCP) 최적화
LCP는 주로 주요 콘텐츠가 렌더링되는 속도에 초점을 맞춥니다. LCP 저하의 일반적인 원인은 느린 서버 응답 시간, 렌더링 차단 리소스, 최적화되지 않은 이미지, 클라이언트 측 렌더링입니다.
- 서버 응답 시간 개선:
- 서버 측 캐싱: 서버 수준(예: Redis, Memcached)에서 강력한 캐싱 전략을 구현하여 HTML 생성 시간을 줄입니다.
- 데이터베이스 쿼리 최적화: 데이터베이스 쿼리가 효율적이고 적절하게 인덱싱되었는지 확인합니다.
- 빠른 호스팅 제공업체 선택: 강력하고 잘 구성된 서버가 기본입니다.
- 콘텐츠 전송 네트워크(CDN): CDN을 사용하여 정적 자산을 사용자에게 더 가깝게 제공하여 지연 시간을 줄입니다.
// 간단한 서버 측 캐싱 시연(개념적) 예시 // 실제 애플리케이션에서는 전용 캐싱 라이브러리/서비스를 사용합니다. const express = require('express'); const app = express(); const cache = {}; // 메모리 내 캐시 app.get('/data', (req, res) => { if (cache['data']) { // 데이터가 캐시에 있는지 확인 console.log('Serving from cache'); return res.send(cache['data']); } // 시간이 많이 걸리는 작업 시뮬레이션(예: 데이터베이스 쿼리) setTimeout(() => { const result = 'This is cached data.'; cache['data'] = result; // 캐시에 저장 console.log('Serving fresh data and caching'); res.send(result); }, 500); }); app.listen(3000, () => console.log('Server running on port 3000'));
- 렌더링 차단 리소스 제거:
- JavaScript의
defer
또는async
: HTML 파서가 차단되지 않도록<script>
태그에defer
또는async
를 표시합니다.async
는 스크립트가 로드되는 즉시 실행되며 순서가 보장되지 않을 수 있습니다.defer
는 HTML이 구문 분석된 후DOMContentLoaded
이벤트 전에 스크립트를 실행하여 실행 순서를 유지합니다. - 중요 CSS: 위에 표시되는 콘텐츠에 필수적인 CSS를 추출하여 HTML에 직접 인라인으로 삽입하여 필수 요소를 즉시 렌더링합니다. 중요하지 않은 CSS는 비동기적으로 로드합니다.
- JavaScript의
<!-- 중요하지 않은 JavaScript 지연 로드 --> <script src="non-critical.js" defer></script> <!-- 스크립트를 비동기적으로 로드(순서 보장 안 됨) --> <script src="analytics.js" async></script> <!-- 중요 CSS 인라인 --> <style> /* 위에 표시되는 콘텐츠에 대한 중요 스타일 */ body { margin: 0; font-family: sans-serif; } .hero { background-color: #f0f0f0; padding: 20px; } </style> <!-- 나머지 CSS 비동기 로드 --> <link rel="preload" href="full-styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="full-styles.css"></noscript>
- 이미지 최적화:
- 이미지 압축: WebP 또는 AVIF와 같은 최신 이미지 형식을 사용하고 적절한 압축을 적용하여 품질 저하 없이 파일 크기를 줄입니다. ImageOptim 또는 다양한 온라인 압축기와 같은 도구가 도움이 될 수 있습니다.
- 반응형 이미지: 사용자의 장치 및 뷰포트에 따라 적절한 크기의 이미지를 제공하기 위해
<img>
태그에서srcset
및sizes
속성을 사용합니다. - 지연 로딩: 폴드 아래에 있는 이미지 및 iframe에 대해
loading="lazy"
속성을 사용하여 지연 로딩을 구현합니다. 이렇게 하면 뷰포트에 들어갈 때 로드가 지연됩니다. - LCP 이미지 미리 로드: LCP 이미지를 미리 알고 있는 경우
<link rel="preload">
를 사용하여 더 일찍 가져옵니다.
<img src="hero-small.jpg" srcset="hero-small.jpg 480w, hero-medium.jpg 800w, hero-large.jpg 1200w" sizes="(max-width: 600px) 480px, (max-width: 1000px) 800px, 1200px" alt="Hero image" loading="lazy" > <!-- 중요한 이미지 미리 로드 (LCP 요소인 경우) --> <link rel="preload" as="image" href="path/to/lcp-image.jpg">
Interaction to Next Paint(INP) 향상
INP는 페이지의 사용자 입력에 대한 응답성에 중점을 둡니다. INP 저하의 주요 원인은 많은 JavaScript 실행, 메인 스레드를 차단하는 긴 작업, 비효율적인 이벤트 핸들러입니다.
-
JavaScript 실행 시간 줄이기:
- 코드 분할 및 트리 쉐이킹: JavaScript 번들을 더 작고 필요에 따라 로드되는 청크로 분할합니다. 사용되지 않는 코드를 제거합니다.
- 디바운싱 및 스로틀링: 자주 트리거되는 이벤트(예: 크기 조정, 스크롤, 입력 필드)의 경우 디바운싱 및 스로틀링을 사용하여 이벤트 핸들러 호출 속도를 제한합니다.
// 디바운스 함수 const debounce = (func, delay) => { let timeout; return function(...args) { const context = this; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), delay); }; }; const handleInput = debounce((event) => { console.log('Input changed:', event.target.value); }, 300); // 스로틀링 함수 const throttle = (func, limit) => { let inThrottle; return function(...args) { const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => (inThrottle = false), limit); } }; }; const handleScroll = throttle(() => { console.log('Scrolling...'); }, 200); // document.getElementById('myInput').addEventListener('input', handleInput); // window.addEventListener('scroll', handleScroll);
- 웹 워커: UI를 응답성 있게 유지하면서 메인 스레드에서 무거운 계산 작업을 웹 워커로 오프로드합니다.
// main.js const myWorker = new Worker('worker.js'); myWorker.postMessage({ operation: 'heavyComputation', data: 1000000 }); myWorker.onmessage = function(e) { console.log('Result from worker:', e.data); }; // worker.js self.onmessage = function(e) { if (e.data.operation === 'heavyComputation') { let sum = 0; for (let i = 0; i < e.data.data; i++) { sum += i; } self.postMessage(sum); } };
- 메인 스레드 작업 최소화:
setTimeout
,requestAnimationFrame
또는isInputPending
(지원되는 경우)을 사용하여 길게 실행되는 JavaScript 작업을 작고 비동기적인 청크로 나눕니다.
-
이벤트 핸들러 최적화:
- 수동 이벤트 리스너: 터치 및 휠 이벤트의 경우 핸들러가
preventDefault()
를 호출하지 않음을 나타내기 위해passive: true
를 사용하여 브라우저가 핸들러 완료를 기다리지 않고 기본적으로 스크롤할 수 있도록 합니다.document.addEventListener('touchstart', (event) => { // ... }, { passive: true });
- 이벤트 위임: 많은 개별 요소에 이벤트 리스너를 연결하는 대신 공통 상위 요소에 단일 리스너를 연결하고 이벤트 위임을 통해 이벤트를 처리합니다.
- 수동 이벤트 리스너: 터치 및 휠 이벤트의 경우 핸들러가
Cumulative Layout Shift(CLS) 최소화
CLS는 페이지의 시각적 안정성을 다룹니다. 예기치 않은 레이아웃 이동은 종종 크기가 지정되지 않은 이미지, 동적으로 삽입된 콘텐츠, 스타일이 적용되지 않은 텍스트(FOUT) 또는 보이지 않는 텍스트(FOIT)의 플래시와 함께 로드되는 웹 글꼴, 비동기적으로 로드되는 광고 또는 임베드에 의해 발생합니다.
- 항상 이미지 및 비디오 크기 설정:
- 이미지 및 비디오 요소에
width
및height
속성(또는 종횡비 CSS)을 제공합니다. 이렇게 하면 브라우저가 미디어가 로드되기 전에 공간을 예약할 수 있습니다. - 반응형 이미지의 경우 CSS
aspect-ratio
속성을 사용하여 공간을 예약합니다.
- 이미지 및 비디오 요소에
<img src="product.jpg" width="600" height="400" alt="Product image"> <!-- 종횡비 CSS 사용 --> <style> .responsive-image-container { width: 100%; padding-bottom: 75%; /* 4:3 종횡비 (높이 / 너비 * 100) */ position: relative; overflow: hidden; /* 이미지의 오버플로 숨기기 */ } .responsive-image { position: absolute; width: 100%; height: 100%; object-fit: cover; } </style> <div class="responsive-image-container"> <img src="responsive-img.jpg" class="responsive-image" alt="Responsive image"> </div>
-
동적으로 삽입된 콘텐츠 안전하게 처리:
- 공간 예약: 동적으로 콘텐츠를 삽입하는 경우(예: 광고, 팝업, 임베드)
min-height
/min-width
CSS 또는 자리 표시자 요소를 사용하여 공간을 할당합니다. - 자리 표시자: 동적 콘텐츠가 로드되는 동안 자리 표시자 또는 스켈레톤 UI를 표시합니다.
- 공간 예약: 동적으로 콘텐츠를 삽입하는 경우(예: 광고, 팝업, 임베드)
-
웹 글꼴 로드 최적화:
font-display
속성:@font-face
선언에서font-display: swap
(폴백 글꼴을 즉시 표시하고 웹 글꼴이 로드되면 전환) 또는font-display: optional
(웹 글꼴이 너무 오래 걸리면 폴백을 사용할 수 있으며 레이아웃 이동 방지)을 사용합니다. CLS에는 일반적으로swap
이 선호됩니다.- 글꼴 미리 로드:
<link rel="preload" as="font" crossorigin>
를 사용하여 중요한 웹 글꼴을 미리 로드하여 더 일찍 사용할 수 있도록 합니다.
@font-face { font-family: 'MyWebFont'; src: url('myfont.woff2') format('woff2'); font-display: swap; /* 또는 'optional' */ }
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>
- 기존 콘텐츠 위에 콘텐츠 삽입 방지:
- 동적으로 콘텐츠를 삽입해야 하는 경우 기존 콘텐츠 아래에 삽입하거나 미리 공간을 예약합니다.
- 초기 렌더링 후 뷰포트 상단에 나타나는 배너 또는 팝업은 상당한 CLS를 유발할 수 있으므로 주의해야 합니다.
결론
LCP, INP, CLS를 최적화하는 것은 단순히 기술 벤치마크를 충족하는 것이 아니라 사용자에게 더 나은 즐거운 경험을 제공하는 것입니다. 서버 성능, 효율적인 리소스 로딩, 반응형 상호 작용 및 시각적 안정성에 집중함으로써 개발자는 빠르게 로드되고 즉시 반응하며 예측 가능하고 즐거운 사용자 여정을 제공하는 웹사이트를 만들 수 있습니다. 이러한 전략을 구현하면 더 나은 핵심 웹 바이탈 점수를 얻을 수 있을 뿐만 아니라 사용자 참여와 만족도를 높일 수 있습니다.