devlog.

웹 성능 최적화 실전 가이드: Core Web Vitals와 최적화 기법

·9분 읽기

사이트가 느리면 사용자는 떠납니다. Google 연구에 따르면 페이지 로딩이 1초 지연될 때마다 전환율이 7% 감소합니다. 성능은 사용자 경험이자 비즈니스 지표입니다.

Core Web Vitals#

Google이 정의한 웹 성능의 핵심 지표입니다. SEO 순위에도 직접 영향을 줍니다.

지표이름의미좋음나쁨
LCPLargest Contentful Paint가장 큰 콘텐츠 렌더링 시간≤ 2.5s> 4.0s
FIDFirst Input Delay첫 입력 반응 지연≤ 100ms> 300ms
CLSCumulative Layout Shift레이아웃 이동 누적 점수≤ 0.1> 0.25
INPInteraction 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투명도 필요, 로고무손실, 큰 용량
WebPJPEG/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 번들 크기를 먼저 잡는 것이 훨씬 효과적입니다.

관련 포스트