웹 성능 최적화 실전 가이드: Core Web Vitals와 최적화 기법
·9분 읽기
사이트가 느리면 사용자는 떠납니다. Google 연구에 따르면 페이지 로딩이 1초 지연될 때마다 전환율이 7% 감소합니다. 성능은 사용자 경험이자 비즈니스 지표입니다.
Core Web Vitals#
Google이 정의한 웹 성능의 핵심 지표입니다. SEO 순위에도 직접 영향을 줍니다.
| 지표 | 이름 | 의미 | 좋음 | 나쁨 |
|---|---|---|---|---|
| LCP | Largest Contentful Paint | 가장 큰 콘텐츠 렌더링 시간 | ≤ 2.5s | > 4.0s |
| FID | First Input Delay | 첫 입력 반응 지연 | ≤ 100ms | > 300ms |
| CLS | Cumulative Layout Shift | 레이아웃 이동 누적 점수 | ≤ 0.1 | > 0.25 |
| INP | Interaction to Next Paint | 모든 상호작용 반응성 | ≤ 200ms | > 500ms |
이미지 최적화#
이미지는 대부분 사이트에서 가장 큰 성능 병목입니다.
Next.js Image 컴포넌트#
import Image from 'next/image'
// ❌ 기본 img 태그 — 최적화 없음
<img src="/hero.jpg" alt="히어로 이미지" />
// ✅ Next.js Image — WebP 변환, 사이즈 최적화, Lazy Loading 자동
<Image
src="/hero.jpg"
alt="히어로 이미지"
width={1200}
height={630}
priority // LCP 이미지에만 사용 (lazy loading 비활성화)
/>
// 반응형 이미지
<Image
src="/hero.jpg"
alt="히어로 이미지"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>
이미지 형식 비교#
| 형식 | 용도 | 특징 |
|---|---|---|
| JPEG | 사진, 복잡한 이미지 | 손실 압축, 작은 용량 |
| PNG | 투명도 필요, 로고 | 무손실, 큰 용량 |
| WebP | JPEG/PNG 대체 | 30% 더 작음, 현대 브라우저 지원 |
| AVIF | 차세대 형식 | 50% 더 작음, 지원 브라우저 증가 중 |
| SVG | 아이콘, 일러스트 | 벡터, 무한 확대 가능 |
코드 분할 (Code Splitting)#
처음부터 모든 JS를 로드하면 느립니다. 필요할 때만 로드합니다.
import dynamic from 'next/dynamic'
// ❌ 초기 번들에 포함 — 페이지 로딩 시 무조건 로드
import HeavyChart from '@/components/HeavyChart'
// ✅ 동적 import — 실제 렌더링 시점에 로드
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <div>차트 로딩 중...</div>,
ssr: false, // 서버 렌더링 제외 (클라이언트 전용 라이브러리)
})
// 사용자 상호작용 시점에 로드
const Modal = dynamic(() => import('@/components/Modal'))
렌더링 전략#
// 1. 정적 생성 (SSG) — 빌드 시 HTML 생성, 가장 빠름
// Next.js App Router: async 서버 컴포넌트의 기본
async function BlogPost({ params }) {
const post = await getPost(params.slug) // 빌드 시 실행
return <article>{post.content}</article>
}
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(p => ({ slug: p.slug }))
}
// 2. 서버 사이드 렌더링 (SSR) — 요청 시 HTML 생성
export const dynamic = 'force-dynamic' // SSG 비활성화
// 3. 클라이언트 사이드 렌더링 (CSR) — 브라우저에서 렌더링
'use client'
// 사용자 맞춤 데이터, 자주 바뀌는 데이터
폰트 최적화#
// ❌ 외부 폰트 — 별도 HTTP 요청, FOIT/FOUT 발생
<link href="https://fonts.googleapis.com/..." rel="stylesheet" />
// ✅ Next.js 내장 폰트 — 자동 최적화, zero layout shift
import { Pretendard } from 'next/font/google'
const pretendard = Pretendard({
subsets: ['latin'],
weight: ['400', '600', '700'],
display: 'swap', // FOIT 방지
})
export default function Layout({ children }) {
return (
<html className={pretendard.className}>
{children}
</html>
)
}
번들 크기 최적화#
# 번들 분석
npm install --save-dev @next/bundle-analyzer
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({})
# 실행
ANALYZE=true npm run build
불필요한 의존성 줄이기#
// ❌ 라이브러리 전체 import
import _ from 'lodash'
_.debounce(fn, 300)
// ✅ 필요한 함수만 import
import debounce from 'lodash/debounce'
// ✅ 또는 직접 구현 (lodash 의존성 제거)
function debounce(fn, delay) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
캐싱 전략#
// Next.js App Router 캐싱
async function getData() {
// 기본: 무한 캐시 (SSG와 동일)
const res = await fetch('https://api.example.com/data')
// 일정 시간마다 재검증 (ISR)
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 1시간
})
// 캐시 없이 매 요청마다 (SSR)
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
}
CLS 방지#
레이아웃 이동은 사용자 경험을 크게 해칩니다.
// ❌ 이미지 크기 미지정 → 로드 후 레이아웃 이동
<img src="/photo.jpg" alt="사진" />
// ✅ 크기 명시 → 공간 미리 확보
<img src="/photo.jpg" alt="사진" width={400} height={300} />
// ❌ 폰트 로드 후 글자 크기 변경 → 레이아웃 이동
// → font-display: swap 또는 optional 사용
// ❌ 동적으로 삽입되는 광고/배너 → 공간 미리 확보
<div style={{ minHeight: '90px' }}>
<AdBanner />
</div>
성능 측정 도구#
# Lighthouse (크롬 DevTools > Lighthouse 탭)
# 로컬 측정: 네트워크/CPU 쓰로틀링 적용
# PageSpeed Insights (실제 사용자 데이터)
# https://pagespeed.web.dev
# WebPageTest (상세 분석)
# https://www.webpagetest.org
성능 최적화 우선순위#
1순위 (임팩트 큼):
✓ 이미지 최적화 (WebP, lazy loading, 적절한 크기)
✓ 렌더 블로킹 리소스 제거 (CSS, JS defer/async)
2순위 (중간):
✓ 코드 분할 (동적 import)
✓ 폰트 최적화
3순위 (세부 조정):
✓ HTTP 캐싱 헤더
✓ 번들 크기 최적화
✓ 서드파티 스크립트 최소화
작은 최적화 여러 개보다 이미지와 JS 번들 크기를 먼저 잡는 것이 훨씬 효과적입니다.
관련 포스트
개발#Docker#DevOps#컨테이너#인프라
Docker 입문 가이드: 개발자가 꼭 알아야 할 컨테이너 기초
Docker가 왜 필요한지부터 이미지, 컨테이너, Docker Compose까지 개발자 관점에서 실용적으로 정리했습니다.
·8분 읽기
개발#API#REST#GraphQL#백엔드
REST API vs GraphQL: 실무에서 뭘 선택해야 할까?
REST와 GraphQL의 핵심 차이를 이해하고, 프로젝트 상황에 따른 올바른 선택 기준을 정리했습니다.
·8분 읽기
개발#CSS#Flexbox#Grid#레이아웃
CSS Flexbox vs Grid: 언제 뭘 써야 할까?
Flexbox와 Grid의 차이점을 명확히 이해하고, 각 상황에 어떤 레이아웃 방식을 선택해야 하는지 실전 예제와 함께 정리했습니다.
·7분 읽기