
바이브 코더를 위한 Next.js 최적화 (3/3): 코드 분할과 캐싱
dynamic import로 번들 크기를 줄이고, 캐싱으로 중복 DB 호출을 없애는 방법을 정리했어요.
Introduction
이미지를 최적화하고 렌더링 전략을 잘 골랐는데도 Lighthouse 점수가 안 오르는 경우가 있어요. 원인은 보통 두 가지예요.
첫째, 번들 크기. 페이지에 진입하는 순간 차트 라이브러리, 에디터, 지도 SDK가 한꺼번에 로드되면 첫 화면이 느려질 수밖에 없죠. 둘째, 중복 데이터 호출. Layout에서 한 번, Page에서 또 한 번, 같은 유저 정보를 DB에서 가져오면 서버 자원이 낭비돼요.
이 글에서는 코드 분할(Code Splitting)로 번들을 나누고, 캐싱으로 데이터 호출을 줄이는 방법을 다룰게요.
이 글은 3부작 시리즈의 마지막 편이에요.
1편: 이미지 최적화
img vs Image, next.config.js, 비용 구조, 아키텍처
2편: 렌더링 전략
CSR, SSR, SSG, ISR, PPR 비교와 선택 기준
3편: 코드 분할과 캐싱 (현재 글)
dynamic import, Script 최적화, unstable_cache
코드 분할 (Code Splitting)
코드 분할의 핵심은 간단해요. 지금 당장 필요하지 않은 코드는 나중에 로드하는 방식이에요.
아래 두 코드를 비교해보세요. 정적 import 탭은 모든 컴포넌트를 페이지 진입 시 한꺼번에 다운로드해요. 동적 import 탭은 dynamic을 사용해서 실제로 필요한 시점에만 로드하죠.
import Header from '../components/Header';
import HeavyChart from '../components/HeavyChart';
import RichTextEditor from '../components/RichTextEditor';
import _ from 'lodash';
export default function BadDashboard({ data }) {
const processedData = _.map(data, (item) => item.value);
return (
<div>
<Header />
<h1>대시보드</h1>
<HeavyChart data={processedData} />
<RichTextEditor />
</div>
);
}ssr: false 옵션은 서버 사이드 렌더링(SSR) 자체를 건너뛰게 해요. window나 document에 의존하는 컴포넌트에 유용하죠.
recharts나 chart.js 같은 차트 라이브러리는 번들 크기가 수백 KB에 달해요. Tiptap이나 Lexical 같은 리치 텍스트 에디터도 비슷한 수준이죠. 이런 무거운 컴포넌트는 사용자가 해당 영역에 도달했을 때 로드해도 늦지 않기 때문에, dynamic import의 대표적인 적용 대상이에요.
트리 셰이킹(Tree Shaking)도 함께 신경 쓰면 좋아요. import _ from 'lodash'로 전체를 가져오면 약 70KB가 번들에 포함돼요. import { map } from 'lodash-es'로 바꾸면 필요한 함수만 포함되기 때문에 번들 크기가 크게 줄어요.
작은 프로젝트에서는 코드 분할이 과최적화 영역일 수 있어요. 초기 번들 크기가 이미 작다면 굳이 모든 컴포넌트를 dynamic으로 바꿀 필요는 없어요.
번들 분석기(Bundle Analyzer)로 시각화하기
어떤 모듈이 번들에서 얼마나 차지하는지 눈으로 확인하면, 최적화할 대상을 정확히 파악할 수 있어요.

@next/bundle-analyzer를 설치하고 설정하면 빌드 결과를 시각적으로 볼 수 있어요.
npm install @next/bundle-analyzerconst withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer(nextConfig);ANALYZE=true npm run build빌드가 완료되면 브라우저에 모듈별 크기가 사각형으로 표시돼요. 예상보다 큰 모듈이 보이면 바로 dynamic import 후보에 올리세요.
스크립트 로딩 최적화
Google Analytics, 채팅 위젯 같은 서드파티 스크립트를 HTML <script> 태그로 <head>에 넣으면 문제가 생겨요. 브라우저가 이 스크립트를 다운로드하고 실행하느라 첫 화면 렌더링이 늦어지기 때문이에요.
GA 같은 분석 스크립트는 조금 늦게 로드돼도 데이터 수집에 문제가 없어요. 분석 스크립트를 로딩하느라 전체 첫 프레임이 늦어지는 건 비효율적이죠.
export default function RootLayout({ children }) {
return (
<html lang="ko">
<head>
<script src="https://www.googletagmanager.com/gtag/js?id=GA_ID" />
<script>
{'window.dataLayer = window.dataLayer || [];'}
</script>
<script src="https://chatbot.com/widget.js" />
</head>
<body>{children}</body>
</html>
);
}strategy="afterInteractive"는 HTML 렌더링과 하이드레이션(hydration)이 완료된 직후에 스크립트를 로드해요. 사용자가 페이지를 보고 상호작용할 수 있는 상태가 된 다음에야 GA가 로드되는 거죠. 사용자 경험에는 영향을 주지 않으면서 분석 데이터는 빠짐없이 수집할 수 있어요.
데이터 캐싱
Next.js App Router에서는 Layout과 Page가 같은 데이터를 각각 따로 호출하는 경우가 흔해요. 예를 들어, Layout에서 유저 이름을 헤더에 표시하고, Page에서도 유저 정보로 대시보드를 구성한다면 DB 호출이 두 번 발생해요.
import { db } from './drizzle';
export async function getUser(id: string) {
console.log('DB 호출 발생!');
return await db.query.users.findFirst({ where: eq(users.id, id) });
}
// app/layout.tsx - 1번째 호출
// app/page.tsx - 2번째 호출 (동일한 데이터인데 중복 호출)unstable_cache는 캐시 키(['user-data'])와 옵션(revalidate, tags)을 직접 지정해요. use cache는 더 간결해요. 함수 상단에 'use cache' 디렉티브를 선언하면 캐시 키가 함수 인자에서 자동 생성되고, JSON 데이터뿐 아니라 컴포넌트까지 캐싱할 수 있어요.
캐싱 적용 전후를 비교하면 차이가 분명해요.
| 비교 항목 | 캐싱 없음 | 최적화 적용 |
|---|---|---|
| DB/API 호출 횟수 | 컴포넌트 개수만큼 N번 | 1번 또는 0번 (캐시 히트) |
| 응답 속도 | 매번 DB 조회 (약 200ms) | 캐시에서 즉시 반환 (1ms 미만) |
| 서버 부하 | 트래픽 증가 시 DB 과부하 위험 | 트래픽이 몰려도 DB는 안정적 |
| 데이터 일관성 | Layout과 Page 데이터가 다를 수 있음 | 같은 렌더링 패스 내 동일 보장 |
캐싱을 적용한 위치를 반드시 문서화하세요. 캐시된 데이터는 실시간이 아니기 때문에, 어디에 어떤 캐시가 적용돼 있는지 모르면 실제 버그가 아닌 현상을 버그로 착각할 수 있어요.
Next.js 16부터는 unstable_cache 대신 use cache 디렉티브 사용을 권장해요. API 형태는 달라졌지만 "한번 가져온 데이터를 일정 시간 재사용한다"는 캐싱의 핵심 개념은 동일해요.
AI로 캐싱 전략 점검하기
바이브 코딩으로 프로젝트를 만들었다면, AI에게 캐싱 전략을 점검해달라고 요청할 수 있어요.
내 Next.js 프로젝트의 데이터 캐싱 전략을 점검해줘.
분석 기준:
1. Layout과 Page에서 동일한 데이터를 중복 호출하는 곳을 찾아줘
2. DB 직접 호출에 unstable_cache를 적용할 수 있는 코드를 작성해줘
3. 데이터 수정 후 캐시를 갱신할 수 있도록 tags와 revalidateTag 사용 예시를 보여줘
아래 포맷으로 리포트를 작성해줘:
| 함수/위치 | 현재 상태 | 개선 제안 | 예상 효과 |캐싱은 적용보다 관리가 어려워요. 어떤 데이터에 캐시가 적용돼 있는지, 갱신 주기는 적절한지를 주기적으로 점검해야 해요.
TL;DR
- 무거운 컴포넌트(차트, 에디터, 지도)는
dynamic으로 지연 로드해서 초기 번들 크기를 줄이세요. import _ from 'lodash'대신import { map } from 'lodash-es'로 Tree Shaking을 활용하세요.- GA 같은 서드파티 스크립트는
<Script strategy="afterInteractive">로 첫 화면 렌더링을 막지 않게 하세요. - Layout과 Page에서 같은 데이터를 호출한다면
use cache(Next.js 16+) 또는unstable_cache(Next.js 15)로 중복 호출을 제거하세요. - 캐시를 적용한 위치는 반드시 문서화하세요. 캐시 때문에 생긴 현상을 버그로 착각하는 일을 막을 수 있어요.
참고 자료
- 원본 영상: 바이브 코더를 위한 웹사이트 최적화 — 코드팩토리 YouTube
- Next.js Caching 공식 문서 — Next.js Documentation
- use cache 디렉티브 — Next.js 16+ 캐싱 API 레퍼런스
- Next.js 번들 최적화 가이드 — Next.js Documentation