웹 성능 주요 지표와 실무 최적화 방법
웹 성능을 제대로 이해하고 개선하려면 핵심 지표(Core Web Vitals)를 먼저 알아야 한다. Google에서 정의한 Core Web Vitals는 사용자가 실제로 느끼는 웹 경험을 정량적으로 측정할 수 있도록 설계되었다. 여기서는 FCP, LCP, CLS, TTI의 의미와 측정 방법, 그리고 실무에서 개선할 수 있는 전략을 자세히 살펴본다.
1. FCP (First Contentful Paint)
FCP는 사용자 화면에 처음으로 텍스트, 이미지, SVG 등 콘텐츠 요소가 렌더링되는 시점을 의미한다. 즉, 브라우저가 빈 화면에서 벗어나 “사이트가 살아 있다”는 신호를 주는 순간이다.
- 권장 기준: 1.8초 이하
- 중요성: 사용자는 화면에 무언가 뜨기를 기대하며, 이를 통해 첫인상이 결정된다.
예시: 사용자가 블로그 글을 클릭했을 때, 헤더나 제목이 화면에 나타나는 순간이 FCP다.
실제 서비스 환경용 Web Vitals 측정 코드 예시
// web-vitals-report.js
import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals';
// 성능 측정 결과를 서버로 전송하거나, 로그 시스템에 기록하는 함수
function sendToAnalytics(metric) {
// 예시: 서버로 데이터 전송 (Google Analytics, Firebase, 혹은 자체 API)
fetch('/api/web-vitals', {
method: 'POST',
body: JSON.stringify(metric),
keepalive: true, // 페이지 언로드 중에도 전송 유지
headers: { 'Content-Type': 'application/json' },
});
}
// 각 Web Vital 측정값을 수집
export function reportWebVitals() {
getCLS(sendToAnalytics); // 누적 레이아웃 이동량 (Cumulative Layout Shift)
getFID(sendToAnalytics); // 첫 입력 지연 (First Input Delay)
getLCP(sendToAnalytics); // 가장 큰 콘텐츠 렌더링 시점 (Largest Contentful Paint)
getFCP(sendToAnalytics); // 첫 콘텐츠 렌더링 시점 (First Contentful Paint)
getTTFB(sendToAnalytics); // 첫 바이트까지의 시간 (Time to First Byte)
}
크롬 Performance 탭에서도 동일한 결과를 확인할 수 있다.
2. LCP (Largest Contentful Paint)
LCP는 시각적으로 가장 큰 콘텐츠 요소(이미지, 텍스트 블록 등)가 화면에 렌더링 완료되는 시점을 의미한다. 페이지의 핵심 콘텐츠가 언제 사용자에게 표시되는지를 측정하며, UX 평가에 중요한 지표다.
- 권장 기준: 2.5초 이하
- 중요성: 페이지의 핵심 콘텐츠가 나타나는 속도가 빠를수록 사용자의 만족도가 높다.
예시: 뉴스 사이트에서 기사 제목과 썸네일 이미지가 완전히 나타나는 순간이 LCP다.
개선 전략:
1. 이미지 최적화(WebP, AVIF)
JPEG/PNG 대신 WebP나 AVIF 사용 → 이미지 용량 감소, 로딩 속도 향상
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="메인 이미지">
</picture>
2. Critical CSS 적용
페이지 상단 콘텐츠에 필요한 최소 CSS만 인라인으로 삽입하고, 나머지 CSS는 비동기 로딩
<style>
/* 상단 헤더, 타이틀 등 핵심 스타일만 인라인 적용 */
header { display: flex; align-items: center; justify-content: space-between; }
h1 { font-size: 2rem; margin: 0; }
</style>
<link rel="stylesheet" href="main.css" media="print" onload="this.media='all'">
3. 폰트 지연 로딩 제거
중요한 텍스트는 font-display: swap 사용.
swap은 웹 폰트가 아직 로드되지 않았을 때 시스템 폰트로 먼저 텍스트를 표시하고, 웹 폰트가 로드되면 이를 교체하는 방식이다. 이 과정에서 FOUT(Flash of Unstyled Text, 스타일 없는 텍스트 깜빡임)이 발생하는데, 이는 텍스트가 보이지만 처음에는 다른 폰트로 표시되었다가 교체되는 현상을 의미한다. 반면, FOIT(Flash of Invisible Text, 텍스트 안 보임)과 달리 사용자에게 빈 화면을 보여주지 않기 때문에 LCP 개선에 도움이 된다.
@font-face {
font-family: 'Pretendard';
src: url('/fonts/Pretendard.woff2') format('woff2');
font-display: swap;
}
4. 이미지 로딩 최적화 (Lazy Load 제외, LCP 요소 우선 로딩)
LCP 요소(메인 이미지)는 loading="eager"로 즉시 로딩하고, 그 외 부수 이미지만 loading="lazy" 적용
<img src="hero.jpg" alt="메인 이미지" loading="eager">
<img src="thumbnail.jpg" alt="썸네일" loading="lazy">
3. CLS (Cumulative Layout Shift)
CLS는 페이지 로딩 중 레이아웃이 얼마나 많이 이동했는지를 측정한다. 불필요한 레이아웃 이동은 UX에 큰 악영향을 끼친다.
- 권장 기준: 0.1 이하
- 중요성: 버튼 클릭이나 콘텐츠 스크롤 중 레이아웃이 밀리면 클릭 오류나 구매 오류를 유발할 수 있다.
예시: 텍스트를 읽고 있는데 갑자기 이미지가 로드되면서 글이 밀리는 현상.
개선 전략:
1. 이미지와 미디어 영역에 크기 명시
- HTML에서 <img width="600" height="400">처럼 이미지의 가로/세로 크기를 지정하거나, CSS aspect-ratio를 사용해 미리 공간을 확보한다.
- 광고, 동영상 등 외부 콘텐츠도 로드 전에 고정된 영역을 설정하여 레이아웃 이동을 방지한다.
2. 폰트 교체 시 FOUT 대비
- font-display: swap을 사용해 텍스트가 보이지 않는 FOIT 현상을 방지하고, FOUT으로 인한 CLS를 최소화한다.
- 중요한 텍스트는 시스템 폰트를 우선 표시하고, 웹 폰트가 로드되면 자연스럽게 교체되도록 설정한다.
3. 동적 콘텐츠 삽입 시 공간 확보
- 자바스크립트로 동적 요소를 추가할 때, 예상되는 크기만큼 미리 공간을 만들어 두어 레이아웃 이동을 줄인다.
- 예: 댓글 섹션, 추천 상품 리스트 등.
이런 전략을 적용하면 CLS 점수를 개선하고, 사용자 경험을 안정적으로 유지할 수 있다.
4. TTI (Time To Interactive)
TTI는 페이지가 완전히 상호작용 가능한 상태가 되는 시간을 의미한다. 겉으로는 페이지가 보이지만, 버튼 클릭이나 스크롤이 동작하지 않으면 사용자 경험이 크게 저하된다.
- 권장 기준: 5초 이하
- 중요성: JavaScript 번들 로딩/파싱/실행 지연으로 인해 TTI가 늦어질 수 있다.
예시: 로그인 페이지가 표시되지만 버튼 클릭이 즉시 반응하지 않는 상황.
개선 전략:
1. 코드 스플리팅(Code Splitting)
목적: 초기 로딩 시 불필요하게 큰 JS 번들을 로딩하지 않고, 필요한 모듈만 로딩하여 페이지 상호작용 속도 향상
React 예시
import React, { Suspense } from 'react';
// 느리게 로드되는 무거운 컴포넌트 - 코드 스플리팅 + 지연로딩
// Suspense와 React.lazy를 함께 사용해서, 컴포넌트가 실제로 렌더링될 때까지 로드를 지연한다.
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<h1>로그인 페이지</h1>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
<button onClick={() => alert('로그인 클릭!')}>로그인</button>
</div>
);
}
초기 렌더링 시 HeavyComponent는 로딩되지 않아, 버튼 클릭 등 인터랙션이 빠르게 동작한다.
2. 지연 로딩(Lazy Loading)
목적: 화면에 바로 필요하지 않은 리소스(JS, 이미지 등)를 나중에 로드하여 TTI를 단축
이미지 지연 로딩 예시 - 이미지가 화면에 나타날 때까지 실제 src를 로드하지 않음
import React, { useEffect, useRef } from 'react';
function LazyImage({ src, alt }) {
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
});
observer.observe(imgRef.current);
return () => observer.disconnect();
}, []);
return <img ref={imgRef} data-src={src} alt={alt} />;
}
export default function App() {
return (
<div>
<h1>Lazy Loading 이미지 예제</h1>
<LazyImage src="https://example.com/large-image.jpg" alt="예시 이미지" />
</div>
);
}
3. JS 최적화
목적: 불필요한 JS 실행, 번들 크기, DOM 처리 지연 최소화
// Debounce 사용으로 이벤트 처리 최적화
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
window.addEventListener('resize', debounce(() => {
console.log('리사이즈 이벤트 처리');
}, 200));
이벤트 처리 부담을 줄여 페이지 초기 상호작용 속도 향상
4. 성능 지표가 중요한 상황
| 지표 | 중요한 상황 |
| FCP | 사용자에게 로딩 시작 피드백 제공, 이탈 방지 |
| LCP | 메인 콘텐츠를 빠르게 보여주는 페이지 (뉴스, 쇼핑몰 등) |
| CLS | 정적 레이아웃 유지가 중요한 페이지 (결제, 양식) |
| TTI | 상호작용이 많은 앱(SPA, 대시보드, 폼)에서 UX 품질 관리 |
5. 정리
- FCP/LCP 개선: 이미지 최적화, Critical CSS, 폰트 지연 로딩 제거
- CLS 개선: 이미지/광고 영역 크기 지정, 폰트 FOUT 대응
- TTI 개선: 코드 스플리팅, Lazy Loading, JS 최적화
웹 성능 최적화는 사용자가 느끼는 속도와 경험을 기준으로 계획하는 것이 핵심이다. Core Web Vitals를 기반으로 성능을 측정하고, 필요한 전략을 단계적으로 적용하면 더 빠르고 안정적인 웹 경험을 제공할 수 있다.