웹 성능 최적화 완벽 가이드: 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 수익에도 직접적인 영향을 미치므로, 성능 지표를 항상 좋은 범위에 유지하는 것이 수익 극대화의 기본입니다.
관련 게시글
React Server Components (RSC) 심층 가이드: Next.js App Router 활용
React Server Components(RSC)의 개념, 동작 원리, 장점, 그리고 Next.js App Router에서 RSC를 활용하는 방법을 심층적으로 탐구합니다. 프론트엔드 개발의 새로운 패러다임을 이해하고 실전 코드 예제를 통해 RSC를 마스터하세요.
Progressive Web App (PWA) 구축 실전: Next.js와 React를 활용한 PWA 개발 가이드
Next.js와 React 기반 PWA 구축 실전 가이드. Service Worker, Web App Manifest 설정 및 next-pwa 활용법을 통해 사용자 경험을 향상시키세요.
CSS Container Queries: 반응형 웹 디자인의 새로운 지평
CSS Container Queries를 활용하여 컴포넌트 기반 반응형 웹 디자인을 구현하는 방법을 심층적으로 알아봅니다. React, Next.js 환경에서 실전 예제를 통해 강력한 기능을 경험하세요.