8. 성능
효과 대비 노력이 가장 좋은 순서로 한다
사용자가 실제로 느끼는 세 가지
섹션 제목: “사용자가 실제로 느끼는 세 가지”| 지표 | 무엇을 재나 | 목표 |
|---|---|---|
| LCP | 가장 큰 콘텐츠가 보이기까지 | 2.5초 이하 |
| INP | 클릭·입력에 대한 반응 지연 | 200ms 이하 |
| CLS | 화면이 얼마나 튀는가 | 0.1 이하 |
- LCP — 보통 히어로 이미지나 첫 텍스트 블록. 서버 컴포넌트가 직접적으로 개선한다
- INP — JS 실행량과 직결된다. 클라이언트 번들이 작을수록 좋다
- CLS — 이미지·폰트·광고가 늦게 도착하며 레이아웃을 밀어내는 문제
셋 다 Chrome이 실제 사용자로부터 수집하며 검색 순위에도 반영된다. 이 셋 밖의 숫자를 쫓기 시작하면 대체로 시간을 낭비하는 중이다.
이미지와 폰트 — CLS와 LCP
섹션 제목: “이미지와 폰트 — CLS와 LCP”next/image
섹션 제목: “next/image”import Image from 'next/image'import hero from './hero.png'
// 로컬 이미지 — 크기를 자동으로 안다<Image src={hero} alt="제품 화면" priority />
// 원격 이미지 — 크기를 직접 알려준다<Image src="https://cdn.example.com/a.jpg" alt="상품" width={640} height={480} sizes="(max-width: 768px) 100vw, 640px"/>- 포맷 변환 — 브라우저가 지원하면 AVIF/WebP로 자동 변환
- 크기별 생성 —
sizes에 맞춰 여러 해상도를 만들고srcset으로 제공 - 지연 로딩 — 뷰포트 밖 이미지는 기본적으로 나중에 로드
priority— LCP 이미지에는 반드시 붙인다. 이게 없으면 LCP가 나빠진다
next/font
섹션 제목: “next/font”import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], display: 'swap' })
export default function RootLayout({ children }) { return ( <html className={inter.className}> <body>{children}</body> </html> )}- 폰트 파일을 빌드 타임에 다운로드해 자체 호스팅한다 — 구글 서버로 요청이 안 나간다
size-adjust를 자동 계산해 폰트 교체 시 레이아웃이 튀지 않게 한다- 한글 폰트는 용량이 커서 서브셋 지정이 특히 중요하다
Pretendard 같은 로컬 폰트는 next/font/local로 같은 이점을 얻는다.
번들 줄이기
섹션 제목: “번들 줄이기”무엇이 번들을 키우는가
섹션 제목: “무엇이 번들을 키우는가”| 흔한 원인 | 대안 |
|---|---|
import _ from 'lodash' |
import debounce from 'lodash/debounce' |
moment.js (locale 전체) |
date-fns 필요 함수만, 또는 내장 Intl |
| 날짜 포맷 라이브러리 | Intl.DateTimeFormat — 브라우저 내장, 0 kB |
| 아이콘 라이브러리 전체 import | 개별 import가 되는지 확인 (lucide-react는 된다) |
| 차트·에디터·지도 | next/dynamic으로 잘라낸다 |
next/dynamic
섹션 제목: “next/dynamic”// 차트, 에디터, 지도처럼 크고 즉시 필요 없는 것import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('@/components/chart'), { loading: () => <ChartSkeleton />, ssr: false, // 브라우저 API에 의존한다면})
// 조건부로만 쓰이는 것const AdminPanel = dynamic(() => import('./admin-panel'))
export function Page({ isAdmin }) { return ( <> <Dashboard /> {isAdmin && <AdminPanel />} {/* 관리자가 아니면 아예 안 받는다 */} </> )}측정 도구
섹션 제목: “측정 도구”# 번들 구성 시각화pnpm add -D @next/bundle-analyzerANALYZE=true pnpm build
# 빌드 결과에 라우트별 First Load JS가 표시된다pnpm buildRoute (app) Size First Load JS┌ ○ / 1.2 kB 92 kB├ ○ /dashboard 45 kB 186 kB ← 여기를 본다└ ● /posts/[slug] 2.1 kB 94 kB
○ (Static) 정적 생성● (SSG) generateStaticParams로 생성First Load JS가 라우트별로 크게 차이 난다면 그 라우트에 무거운 클라이언트 컴포넌트가 있다는 뜻이다. 4장의 경계 검토를 그 라우트부터 시작한다.
도구가 대신해 주는 것
섹션 제목: “도구가 대신해 주는 것”React Compiler
섹션 제목: “React Compiler”React 19와 함께 1.0이 정식 출시됐다. 메모이제이션을 자동으로 해준다.
const sorted = useMemo(() => items.sort(cmp), [items])const onClick = useCallback(() => select(id), [id])의존성 배열을 사람이 관리해야 했다. 빠뜨리면 stale closure, 과하면 무의미.
const sorted = items.sort(cmp)const onClick = () => select(id)컴파일 시점에 필요한 메모이제이션을 자동으로 삽입한다.
서버 컴포넌트에는 해당 없다 — 애초에 리렌더가 없기 때문이다. 클라이언트 컴포넌트가 많은 프로젝트에서 효과가 크다.
Turbopack
섹션 제목: “Turbopack”Next.js 16부터 기본 번들러다.
- dev 서버 시작과 HMR이 눈에 띄게 빨라진다
next.config.ts의webpack:설정을 쓰고 있었다면 turbopack 설정으로 옮겨야 한다- 커스텀 로더가 있으면 대체 방법을 확인해야 한다
대부분의 프로젝트는 아무것도 안 해도 된다. webpack 플러그인을 쓰던 프로젝트만 작업이 필요하다.
개선의 우선순위
섹션 제목: “개선의 우선순위”flowchart TB
A["1. 서버 컴포넌트로 옮길 수 있는가<br/>= 번들에서 완전히 제거"] --> B["2. 이미지 · 폰트를 최적화했는가<br/>= LCP · CLS"]
B --> C["3. 무거운 의존성을 잘라냈는가<br/>= dynamic import · tree-shaking"]
C --> D["4. 캐싱 전략이 맞는가<br/>= use cache · 정적 껍데기"]
D --> E["5. 그래도 느린가<br/>= 프로파일러로 측정"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class A,B ok
class C,D warn
class E mute
점검 순서
섹션 제목: “점검 순서”-
pnpm build로 라우트별 First Load JS를 본다가장 큰 라우트 하나를 고른다.
-
그 라우트의
use client를 전부 찾는다Terminal window grep -rl "use client" app/dashboard -
트리 위쪽에 있는 것부터 잎사귀로 밀어본다 (4장)
-
LCP 이미지에
priority가 붙어 있는지 확인한다 -
번들 분석기로 남은 무거운 의존성을 찾는다
Terminal window ANALYZE=true pnpm build -
여기까지 하고도 느리면 그때 프로파일러를 켠다
8장 요약
섹션 제목: “8장 요약”- LCP · INP · CLS — 이 셋만 보면 된다
next/image는 크기 지정을 강제해 CLS를 원천 차단한다. LCP 이미지엔prioritynext/font는 폰트를 자체 호스팅하고 교체 시 레이아웃 시프트를 막는다- 번들은 서버 컴포넌트화 → tree-shaking → dynamic import 순으로 줄인다
- React Compiler가
useMemo/useCallback을 대신해 준다 - Next.js 16의 기본 번들러는 Turbopack
9. CSS의 문제Tailwind는 무엇을 끝냈는가 — 유틸리티 이전의 10년을 먼저 본다.