Next.js 성능 최적화

Next.js

Next.js

vs
React

React

Next.js 성능 최적화 2026 실전 가이드
Next.js 성능 최적화 가이드

Next.js 성능 최적화 가이드 2026

Core Web Vitals · 번들 최적화 · 이미지·폰트 · 서버 컴포넌트 — 실전 최적화 기법 총정리

✦ Core Web Vitals ✦ App Router 최적화 ✦ 실무 코드 예시

Next.js는 기본 설정만으로도 상당한 성능을 제공하지만, 실제 프로덕션 환경에서 Core Web Vitals 점수를 높이고 사용자 경험을 극대화하려면 추가적인 최적화가 필요합니다. 이 글에서는 2026년 App Router 기반 Next.js 프로젝트에서 실제로 적용할 수 있는 핵심 최적화 기법들을 단계별로 정리합니다.

Core Web Vitals 기초 이해

Google의 Core Web Vitals는 실제 사용자 경험을 측정하는 세 가지 핵심 지표입니다. 이 지표들은 검색 랭킹에도 직접 영향을 미칙니다.

지표 Good Needs Improvement Poor 측정 대상
LCP ≤ 2.5s ≤ 4.0s > 4.0s 가장 큰 콘텐츠 요소 렌더링 시간
INP ≤ 200ms ≤ 500ms > 500ms 사용자 인터랙션 응답 지연
CLS ≤ 0.1 ≤ 0.25 > 0.25 레이아웃 이동 누적 점수

이미지 최적화 — next/image 완전 활용

이미지는 페이지 LCP에 가장 큰 영향을 미치는 요소입니다. Next.js의 next/image 컴포넌트를 올바르게 사용하는 것만으로도 성능이 크게 향상됩니다.

// ✅ 올바른 next/image 사용법
import Image from 'next/image'

export default function Hero() {
  return (
    <Image
      src="/hero.webp"
      alt="히어로 이미지 설명"
      width={1200}
      height={630}
      priority  // LCP 요소에는 priority 필수
      sizes="(max-width: 768px) 100vw, 1200px"
    />
  )
}
⚠️ 흔한 실수: priority 누락
Above-the-fold(첫 화면에 보이는) 이미지에 priority prop을 빠뜨리면 LCP 점수가 크게 하락합니다. 히어로 이미지, 헤더 로고, OG 이미지 등에는 반드시 priority를 추가하세요. 반대로 스크롤 아래 이미지에는 priority를 쓰지 않아야 초기 로딩이 빨라집니다.

폰트 최적화 — next/font로 CLS 0 달성

next/font는 폰트 파일을 빌드 타임에 다운로드하고 자동으로 font-display: swap을 적용해 CLS(누적 레이아웃 이동)를 0으로 만들어 줍니다.

// app/layout.tsx
import { Inter, Noto_Sans_KR } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
})

const notoSansKr = Noto_Sans_KR({
  subsets: ['latin'],
  weight: ['400', '700'],  // 필요한 weight만 선택
  variable: '--font-noto',
})

export default function RootLayout({ children }) {
  return (
    <html className={`${inter.variable} ${notoSansKr.variable}`}>
      <body>{children}</body>
    </html>
  )
}

번들 사이즈 최적화

JavaScript 번들 사이즈는 LCP와 INP 모두에 영향을 미칙니다. 번들 분석부터 시작하세요.

# 번들 분석기 설치 및 실행
npm install @next/bundle-analyzer

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

# 실행
ANALYZE=true npm run build

번들 분석 결과에서 큰 라이브러리를 발견하면 다음과 같은 방법으로 줄일 수 있습니다.

  • moment.js → date-fns: 필요한 함수만 import하여 번들 크기를 1/10로 축소 가능
  • lodash → lodash-es: Tree-shaking이 되는 ESM 버전 사용
  • 아이콘 라이브러리: import { FaUser } from 'react-icons/fa'처럼 개별 import

Server Components로 JS 페이로드 줄이기

Next.js App Router의 Server Components는 서버에서만 실행되어 클라이언트로 JS 코드가 전송되지 않습니다. 데이터 패칭과 정적 렌더링에 활용하면 번들 사이즈를 크게 줄일 수 있습니다.

// app/blog/page.tsx — Server Component (기본)
// 이 컴포넌트의 코드는 클라이언트로 전송되지 않음
async function BlogPage() {
  // 서버에서 직접 DB 조회 또는 API 호출
  const posts = await getPosts()

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

// 인터랙션이 필요한 부분만 Client Component로 분리
'use client'
function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false)
  return <button onClick={() => setLiked(!liked)}>{liked ? '♥' : '♡'}</button>
}

캐싱 전략 — fetch, revalidate, unstable_cache

Next.js 15의 캐싱 시스템을 올바르게 활용하면 서버 부하를 줄이고 응답 속도를 높일 수 있습니다.

// 1. ISR — 60초마다 재생성
const data = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 }
})

// 2. 태그 기반 재검증 — On-Demand ISR
const data = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] }
})

// 특정 태그 무효화 (서버 액션 또는 API Route에서)
import { revalidateTag } from 'next/cache'
revalidateTag('posts')  // CMS에서 글 수정 시 호출

// 3. unstable_cache — DB 쿼리 캐싱
import { unstable_cache } from 'next/cache'

const getCachedUser = unstable_cache(
  async (userId: string) => getUserFromDB(userId),
  ['user'],
  { revalidate: 3600, tags: ['user'] }
)

지연 로딩 — dynamic import와 Suspense

초기 페이지 로딩에 필요하지 않은 컴포넌트는 지연 로딩하여 번들 분할과 초기 로드 시간을 단축할 수 있습니다.

import dynamic from 'next/dynamic'

// 차트 라이브러리처럼 무거운 컴포넌트는 동적 로딩
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <div>차트 로딩 중...</div>,
  ssr: false,  // 서버사이드 렌더링 불필요 시
})

// Suspense로 스트리밍 SSR 활용
import { Suspense } from 'react'

export default function Page() {
  return (
    <div>
      <h1>빠르게 로드되는 헤더</h1>
      <Suspense fallback={<Skeleton />}>
        <SlowDataComponent />  {/* 데이터 로딩이 느린 컴포넌트 */}
      </Suspense>
    </div>
  )
}

성능 측정 도구

최적화 전후 성능 변화를 수치로 확인하는 것이 중요합니다.

  • Lighthouse: Chrome DevTools에서 바로 실행. 로컬 환경에서 빠른 확인에 적합합니다.
  • PageSpeed Insights: 실제 사용자 데이터(CrUX) 기반 측정. 프로덕션 URL로 확인하세요.
  • Vercel Analytics: Vercel 배포 시 Real User Monitoring(RUM) 자동 수집.
  • Web Vitals 라이브러리: npm install web-vitals 후 커스텀 분석 서버로 데이터 전송 가능.
💡 최적화 우선순위
성능 최적화는 측정 → 병목 파악 → 개선 순으로 진행하세요. 추측으로 최적화하면 효과 없는 작업에 시간을 낭비합니다. Lighthouse의 "Opportunities" 섹션이 제시하는 항목부터 순서대로 처리하시면 효율적입니다.

자주 묻는 질문 (FAQ)

Q. next/image 없이 일반 img 태그를 써도 되나요?
A. 기능적으로는 동작하지만 권장하지 않습니다. next/image는 자동 WebP 변환, 리사이징, lazy loading, blur placeholder 등을 제공합니다. 특히 LCP 이미지에 priority prop 없이 일반 img 태그를 사용하면 LCP 점수가 크게 하락할 수 있습니다. next/image 사용을 강력히 권장드립니다.
Q. 모든 컴포넌트를 Server Component로 만들어야 하나요?
A. 그렇지 않습니다. useState, useEffect, onClick 같은 클라이언트 기능이 필요한 컴포넌트는 'use client'를 선언해야 합니다. 전략은 "서버 컴포넌트를 기본으로, 상호작용이 필요한 최소한의 부분만 클라이언트 컴포넌트로 분리"입니다. 클라이언트 컴포넌트를 트리의 하단 leaf에 배치할수록 클라이언트로 전송되는 JS가 줄어듭니다.
Q. Next.js 15에서 캐싱 동작이 크게 바뀌었나요?
A. 네, Next.js 15에서는 fetch 요청이 기본적으로 캐시되지 않도록 변경되었습니다(Next.js 14까지는 기본 캐시). 따라서 캐싱이 필요한 fetch 요청에는 명시적으로 next: { revalidate: N } 또는 cache: 'force-cache'를 지정해야 합니다. 기존 14 버전에서 15로 업그레이드할 때 이 변경 사항을 반드시 확인하시기 바랍니다.
Q. Lighthouse 점수가 높은데 실제 사용자 경험이 느린 이유는 무엇인가요?
A. Lighthouse는 고성능 개발 환경에서 시뮬레이션한 점수입니다. 실제 사용자는 다양한 기기와 네트워크 환경에서 접속하기 때문에 차이가 발생할 수 있습니다. PageSpeed Insights의 "Field Data" 섹션(실제 CrUX 데이터)과 Vercel Analytics의 실제 사용자 데이터를 함께 확인하시는 것을 권장드립니다.
Q. Tailwind CSS가 CSS 번들 사이즈에 영향을 미치나요?
A. Tailwind CSS는 빌드 타임에 실제로 사용된 클래스만 추출(purging)하기 때문에 프로덕션 번들 크기가 매우 작습니다. 일반적으로 gzip 기준 5~15KB 수준입니다. 오히려 커스텀 CSS를 많이 작성하는 것보다 Tailwind를 사용하는 것이 번들 크기 관리에 유리합니다.

댓글