웹 성능 최적화 완벽 가이드: Core Web Vitals
Core Web Vitals를 중심으로 웹 성능을 측정하고 개선하는 방법을 알아봅니다. LCP, FID, CLS 지표와 실전 최적화 기법을 다룹니다.
웹 성능 최적화 완벽 가이드: Core Web Vitals
웹 성능은 사용자 경험과 비즈니스 성과에 직접적인 영향을 미칩니다. Google은 Core Web Vitals를 검색 순위 요인으로 포함시켰으며, 페이지 로딩 시간이 1초에서 3초로 증가하면 이탈률이 32% 높아진다는 연구 결과도 있습니다. 이 글에서는 웹 성능의 핵심 지표와 실전 최적화 기법을 살펴보겠습니다.
Core Web Vitals 이해하기
Core Web Vitals는 Google이 정의한 세 가지 핵심 사용자 경험 지표입니다.
LCP (Largest Contentful Paint)
LCP는 페이지의 주요 콘텐츠가 화면에 렌더링되기까지의 시간을 측정합니다.
| 등급 | 시간 | 의미 |
|---|---|---|
| 좋음 | 2.5초 이하 | 빠른 로딩 |
| 개선 필요 | 2.5~4.0초 | 보통 |
| 나쁨 | 4.0초 초과 | 느린 로딩 |
LCP에 영향을 미치는 요소는 큰 이미지, 비디오, 텍스트 블록입니다. 가장 큰 콘텐츠 요소가 빠르게 로드되도록 최적화해야 합니다.
INP (Interaction to Next Paint)
INP는 사용자 상호작용(클릭, 탭, 키 입력)에 대한 응답 속도를 측정합니다. 기존 FID(First Input Delay)를 대체한 지표로, 페이지 전체 수명 동안의 응답성을 평가합니다.
| 등급 | 시간 | 의미 |
|---|---|---|
| 좋음 | 200ms 이하 | 빠른 응답 |
| 개선 필요 | 200~500ms | 느리게 느껴짐 |
| 나쁨 | 500ms 초과 | 반응 없음 |
CLS (Cumulative Layout Shift)
CLS는 페이지 로딩 중 예상치 못한 레이아웃 변동을 측정합니다. 광고, 이미지, 동적 콘텐츠가 뒤늦게 로드되면서 화면이 밀리는 현상이 여기에 해당합니다.
| 등급 | 점수 | 의미 |
|---|---|---|
| 좋음 | 0.1 이하 | 안정적 레이아웃 |
| 개선 필요 | 0.1~0.25 | 약간의 변동 |
| 나쁨 | 0.25 초과 | 심한 변동 |
이미지 최적화
이미지는 웹 페이지 용량의 대부분을 차지합니다. 이미지 최적화만으로도 큰 성능 개선을 달성할 수 있습니다.
차세대 이미지 포맷 사용
WebP와 AVIF는 JPEG/PNG 대비 25-50% 더 작은 파일 크기를 제공합니다.
<!-- picture 태그로 포맷 폴백 -->
<picture>
<source srcset="image.avif" type="image/avif" />
<source srcset="image.webp" type="image/webp" />
<img src="image.jpg" alt="설명" width="800" height="600" loading="lazy" />
</picture>
Next.js Image 컴포넌트
Next.js의 Image 컴포넌트는 자동으로 이미지를 최적화합니다.
import Image from 'next/image';
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="히어로 이미지"
width={1200}
height={600}
priority // LCP 이미지는 priority 추가
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
);
}
이미지 최적화 체크리스트
- 적절한 크기로 리사이징합니다 (실제 표시 크기의 2배 이하)
- WebP 또는 AVIF 포맷을 사용합니다
width와height속성을 반드시 지정합니다 (CLS 방지)- 뷰포트 밖 이미지는
loading="lazy"를 적용합니다 - LCP 이미지는
fetchpriority="high"를 적용합니다 srcset과sizes로 반응형 이미지를 제공합니다
JavaScript 번들 최적화
큰 JavaScript 번들은 파싱과 실행에 시간이 걸려 INP에 악영향을 미칩니다.
코드 스플리팅
// Next.js dynamic import
import dynamic from 'next/dynamic';
// 무거운 컴포넌트를 지연 로딩
const HeavyChart = dynamic(() => import('../components/Chart'), {
loading: () => <div className="h-64 bg-gray-100 animate-pulse rounded" />,
ssr: false,
});
// React.lazy 사용
const LazyModal = React.lazy(() => import('./Modal'));
트리 쉐이킹
사용하지 않는 코드를 번들에서 제거하려면 named import를 사용합니다.
// 나쁜 예 - 전체 라이브러리 번들링
import _ from 'lodash';
_.debounce(fn, 300);
// 좋은 예 - 필요한 함수만 임포트
import debounce from 'lodash/debounce';
debounce(fn, 300);
번들 분석
# Next.js 번들 분석
npm install @next/bundle-analyzer
# 번들 크기 확인
ANALYZE=true npm run build
CSS 최적화
사용하지 않는 CSS 제거
Tailwind CSS를 사용하면 JIT 컴파일러가 자동으로 사용하지 않는 스타일을 제거합니다. 전통적 CSS의 경우 PurgeCSS를 활용합니다.
크리티컬 CSS 인라인
페이지 렌더링에 즉시 필요한 CSS를 인라인으로 포함하면 렌더 블로킹을 방지할 수 있습니다.
<head>
<!-- 크리티컬 CSS 인라인 -->
<style>
body { margin: 0; font-family: system-ui; }
.header { height: 60px; background: white; }
/* 초기 렌더링에 필요한 최소 스타일 */
</style>
<!-- 나머지 CSS는 비동기 로딩 -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
</head>
폰트 최적화
웹 폰트는 FOUT(Flash of Unstyled Text)이나 FOIT(Flash of Invisible Text)를 유발할 수 있습니다.
최적화 전략
/* 1. font-display 설정 */
@font-face {
font-family: 'Noto Sans KR';
src: url('/fonts/NotoSansKR-Regular.woff2') format('woff2');
font-weight: 400;
font-display: swap; /* 텍스트를 먼저 보여주고 폰트 로드 후 교체 */
}
<!-- 2. 폰트 사전 로딩 -->
<link rel="preload" href="/fonts/NotoSansKR-Regular.woff2" as="font" type="font/woff2" crossorigin />
<!-- 3. Next.js next/font 활용 (자동 최적화) -->
// Next.js 내장 폰트 최적화
import { Noto_Sans_KR } from 'next/font/google';
const notoSansKR = Noto_Sans_KR({
subsets: ['latin'],
weight: ['400', '700'],
display: 'swap',
});
캐싱 전략
적절한 캐싱은 재방문 사용자의 로딩 속도를 극적으로 개선합니다.
HTTP 캐시 헤더
# 정적 에셋 (이미지, CSS, JS) - 1년 캐시
Cache-Control: public, max-age=31536000, immutable
# HTML 페이지 - 재검증 필요
Cache-Control: no-cache
# API 응답 - 짧은 캐시
Cache-Control: public, max-age=60, s-maxage=300
Next.js에서의 캐싱
// next.config.ts
const nextConfig = {
async headers() {
return [
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
};
렌더링 전략 선택
SSG vs SSR vs ISR vs CSR
| 전략 | 빌드 시 | 요청 시 | 장점 | 단점 |
|---|---|---|---|---|
| SSG | HTML 생성 | 정적 서빙 | 가장 빠름 | 데이터 갱신 어려움 |
| SSR | - | HTML 생성 | 항상 최신 데이터 | 서버 부하 |
| ISR | HTML 생성 | 주기적 재생성 | 빠르고 신선함 | 복잡한 설정 |
| CSR | 빈 HTML | JS로 렌더링 | 인터랙티브 | 초기 로딩 느림 |
블로그나 콘텐츠 사이트는 SSG 또는 ISR이 가장 적합합니다. 사용자별 데이터가 필요한 대시보드는 SSR이나 CSR을 고려합니다.
성능 측정 도구
추천 도구
- Google PageSpeed Insights: Core Web Vitals 점수 확인
- Lighthouse: Chrome DevTools 내장 성능 감사
- WebPageTest: 상세한 폭포수 차트와 비디오 분석
- Chrome DevTools Performance 탭: 실시간 성능 프로파일링
자동 모니터링
// Web Vitals 자동 수집
import { onCLS, onINP, onLCP } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
});
navigator.sendBeacon('/api/analytics', body);
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
성능 최적화 체크리스트
- 이미지를 WebP/AVIF 포맷으로 변환하고 적절한 크기로 제공합니다
- JavaScript 번들을 코드 스플리팅으로 분할합니다
- 중요하지 않은 리소스는 지연 로딩합니다
- 정적 에셋에 적절한 캐시 헤더를 설정합니다
- 웹 폰트에 font-display: swap을 적용합니다
- 이미지와 iframe에 width/height를 명시하여 CLS를 방지합니다
- 크리티컬 CSS를 인라인으로 포함합니다
- 서드파티 스크립트를 비동기로 로드합니다
마무리
웹 성능 최적화는 일회성 작업이 아닌 지속적인 프로세스입니다. Core Web Vitals를 정기적으로 모니터링하고, 새로운 기능 추가 시 성능 영향을 평가하는 습관이 중요합니다. 특히 SEO와 AdSense 수익에도 직접적인 영향을 미치므로, 성능 지표를 항상 좋은 범위에 유지하는 것이 수익 극대화의 기본입니다.
관련 게시글
Vite Build Tool: Fast Frontend Development Guide
Vite는 현대적인 프론트엔드 개발을 위한 빠르고 효율적인 빌드 도구입니다. 이 가이드에서는 Vite의 핵심 기능, React 및 TypeScript 프로젝트 설정, 플러그인 활용법, 그리고 빌드 최적화 전략까지 완벽하게 다룹니다.
React Server Components (RSC) 심층 가이드: Next.js와 함께하는 Full-stack React
React Server Components (RSC)의 개념, 등장 배경, 동작 원리, 그리고 Next.js 13+ App Router에서의 활용법을 심층적으로 다룹니다. 클라이언트/서버 컴포넌트 분리 전략과 실전 코드 예제를 통해 RSC의 강력한 이점을 이해하고 웹 애플리케이션 성능을 최적화하는 방법을 알아봅니다.
Next.js Middleware: 강력한 요청 처리 활용법
Next.js Middleware를 활용하여 사용자 인증, 국제화, A/B 테스트 등 다양한 요청 처리 로직을 효율적으로 구현하는 방법을 심층적으로 알아봅니다. 실전 코드 예제를 통해 Next.js 애플리케이션의 프론트엔드 기능을 강화하세요.