최적화 — 이미지, 폰트, 번들 사이즈
최적화 — 이미지, 폰트, 번들 사이즈
성능 점수가 낮아서 Lighthouse를 돌려봤더니 "이미지 최적화", "웹폰트 로딩 차단", "미사용 JavaScript"가 잔뜩 나옵니다. Next.js에서 이런 것들을 어떻게 최적화하나요?
Next.js는 프레임워크 레벨에서 이미지, 폰트, 코드 분할 최적화를 내장하고 있습니다.
HTML <img> 대신 next/image, Google Fonts CDN 대신 next/font, ** 수동 코드 분할 대신 dynamic** — 이 세 가지만 적용해도 성능이 눈에 띄게 개선됩니다.
next/image — 이미지 최적화
왜 <img>를 쓰면 안 되는가
- HTML
<img>는 원본 이미지를 그대로 전송합니다. 5MB PNG가 그대로 내려갑니다. - 뷰포트에 보이지 않는 이미지도 즉시 로드하여 초기 로딩을 지연시킵니다.
- 이미지 로드 전후로 레이아웃이 밀리는 CLS(Cumulative Layout Shift) 문제가 발생합니다.
next/image는 이 세 가지를 모두 해결합니다.
import Image from 'next/image';
export default function Profile() {
return (
<Image
src="/profile.jpg"
alt="프로필 사진"
width={300}
height={300}
priority // LCP 이미지는 priority 추가
/>
);
}
next/image가 하는 일
| 기능 | 설명 |
|---|---|
| 포맷 변환 | WebP/AVIF로 자동 변환 (브라우저 지원에 따라) |
| 크기 조정 | 요청된 크기에 맞게 리사이즈 |
| 지연 로딩 | 뷰포트에 진입할 때 로드 (기본 loading="lazy") |
| CLS 방지 | width/height로 미리 공간 확보 |
| 캐싱 | 최적화된 이미지를 서버에 캐싱 |
반응형 이미지
<Image
src="/hero.jpg"
alt="히어로 이미지"
fill // 부모 요소 크기에 맞춤
sizes="(max-width: 768px) 100vw, 50vw" // 뷰포트별 크기 힌트
style={{ objectFit: 'cover' }}
/>
sizes 속성은 브라우저에게 "이 이미지가 화면에서 차지하는 크기"를 알려줍니다.
이 정보를 기반으로 ** 적절한 크기의 이미지만 다운로드 **합니다.
외부 이미지 설정
외부 URL의 이미지를 사용하려면 next.config.ts에 도메인을 등록해야 합니다.
// next.config.ts
const nextConfig = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'images.example.com' },
],
},
};
export default nextConfig;
next/font — 폰트 최적화
일반적인 웹폰트의 문제
- Google Fonts를
<link>로 로드하면 ** 외부 CDN에 추가 네트워크 요청 **이 발생합니다. - 폰트가 로드되기 전까지 텍스트가 안 보이거나(FOIT), 기본 폰트로 보이다 바뀌는(FOUT) 현상이 생깁니다.
- 레이아웃 이동(CLS)이 발생합니다.
import { Noto_Sans_KR } from 'next/font/google';
const notoSansKr = Noto_Sans_KR({
subsets: ['latin'],
weight: ['400', '700'],
display: 'swap',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko" className={notoSansKr.className}>
<body>{children}</body>
</html>
);
}
next/font가 하는 일
- ** 빌드 시점에** 폰트 파일을 다운로드하여 프로젝트에 포함합니다.
- 런타임에 외부 CDN 요청이 0개 입니다.
- CSS
font-display: swap으로 FOIT를 방지합니다. size-adjust속성으로 CLS를 최소화합니다.
로컬 폰트도 지원합니다.
import localFont from 'next/font/local';
const pretendard = localFont({
src: './fonts/PretendardVariable.woff2',
display: 'swap',
});
dynamic import — 코드 분할
모든 코드를 초기 번들에 포함하면 첫 로딩이 느려집니다.
next/dynamic으로 필요한 시점에만 컴포넌트를 로드 할 수 있습니다.
import dynamic from 'next/dynamic';
// 이 컴포넌트는 렌더링될 때 로드됨
const HeavyEditor = dynamic(() => import('./HeavyEditor'), {
loading: () => <p>에디터 로딩 중...</p>,
ssr: false, // 서버에서 렌더링하지 않음 (브라우저 전용 라이브러리)
});
export default function Page() {
return (
<div>
<h1>글 작성</h1>
<HeavyEditor />
</div>
);
}
언제 dynamic import를 쓰나
| 상황 | 적용 |
|---|---|
| 무거운 라이브러리 (차트, 에디터, 지도) | dynamic(() => import(...)) |
| 특정 조건에서만 렌더링되는 컴포넌트 | dynamic + 조건부 렌더링 |
| 브라우저 전용 라이브러리 (window 필요) | dynamic + ssr: false |
| 모든 컴포넌트에 무조건 적용 | 불필요 — Next.js가 라우트별 자동 분할함 |
번들 분석
실제로 번들에 뭐가 들어있는지 확인하려면 @next/bundle-analyzer를 사용합니다.
npm install @next/bundle-analyzer
// next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
export default withBundleAnalyzer(nextConfig);
ANALYZE=true npm run build
빌드 후 브라우저에 번들 구성이 시각적으로 표시됩니다. 예상보다 큰 라이브러리가 보이면 dynamic import나 tree-shaking을 적용합니다.
주의할 점
priority를 남용하면 오히려 느려진다
next/image의 priority는 LCP(Largest Contentful Paint) 이미지에만 사용합니다.
모든 이미지에 priority를 붙이면 지연 로딩이 해제되어 초기 로딩이 오히려 느려집니다.
페이지 상단에 처음 보이는 큰 이미지 1~2개에만 적용하는 것이 맞습니다.
ssr: false를 기본값으로 쓰지 않기
dynamic(() => import(...), { ssr: false })는 해당 컴포넌트를 서버에서 렌더링하지 않습니다.
SEO가 필요한 콘텐츠에 이걸 적용하면 검색 엔진이 해당 내용을 인덱싱하지 못합니다.
ssr: false는 window, document 등 브라우저 전용 API를 사용하는 컴포넌트에만 한정합니다.
정리
| 최적화 영역 | 도구 | 핵심 효과 |
|---|---|---|
| 이미지 | next/image | 자동 포맷 변환, 지연 로딩, CLS 방지 |
| 폰트 | next/font | 빌드 시 다운로드, 외부 요청 0, CLS 최소화 |
| 코드 분할 | next/dynamic | 필요 시점에만 로드, 초기 번들 축소 |
| 분석 | @next/bundle-analyzer | 번들 구성 시각화 |
기억할 한 줄: "<img> 대신 next/image, <link> 대신 next/font, 무거운 건 dynamic."