블로그: 빌드 결과물 뜯어보기 — Vite 청크 분할 전략
블로그: 빌드 결과물 뜯어보기을 어떤 기준으로 선택해야 할까요?
\n\n> 블로그: 빌드 결과물 뜯어보기을 어떤 기준으로 선택해야 할까요?\n이전 글에서 React.lazy()로 페이지별 코드 스플리팅을 적용했습니다. 그런데 빌드 결과를 자세히 보면 아직 최적화할 여지가 있습니다.
dist/assets/index-BcJ88B4w.js 925.87 kB │ gzip: 283.02 kB
index.js가 여전히 925KB입니다. 여기에 React, React Router, React DOM, 유틸리티 라이브러리, 공통 컴포넌트가 전부 섞여 있습니다.
문제는 내 코드를 한 줄만 바꿔도 index.js의 해시가 달라져서 사용자가 전체를 다시 받아야 한다는 점입니다. 라이브러리 코드는 안 바뀌었는데도요.
manualChunks
Vite는 내부적으로 Rollup을 사용하는데, Rollup의 manualChunks 옵션으로 특정 패키지를 별도 chunk로 분리할 수 있습니다.
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-react': [
'react',
'react-dom',
'react-router-dom',
],
'vendor-markdown': [
'react-markdown',
'remark-gfm',
'rehype-raw',
'rehype-slug',
'rehype-highlight',
],
},
},
},
},
});
이렇게 하면 빌드 결과가 이렇게 바뀝니다.
vendor-react.js 47 kB ← React 관련
vendor-markdown.js 523 kB ← 마크다운 관련
index.js 350 kB ← 내 코드 + 나머지
왜 분리하는가
캐싱 효율
Vite는 빌드할 때 파일 내용 기반으로 해시를 생성합니다. vendor-react-D2kupECk.js에서 D2kupECk가 그 해시입니다.
라이브러리를 별도 chunk로 분리하면:
- 내 코드를 수정해도
vendor-react.js의 해시는 안 바뀜 - 사용자 브라우저에 캐시된
vendor-react.js를 그대로 사용 - 변경된
index.js만 새로 다운로드
라이브러리 업데이트 없이 코드만 수정하는 경우가 대부분이니까, 이 전략이 효과적입니다.
로딩 병렬화
브라우저는 같은 도메인에 대해 보통 6개의 동시 연결을 지원합니다. 파일이 하나면 직렬 다운로드지만, 여러 chunk로 나뉘면 병렬로 받을 수 있습니다.
어떤 기준으로 나누나
아무거나 막 나누면 오히려 역효과입니다. 기준은 이렇습니다.
| 기준 | 예시 | 이유 |
|---|---|---|
| 변경 빈도가 낮은 것 | React, React Router | 거의 안 바뀌니까 캐시 효과 큼 |
| ** 크기가 큰 것** | 마크다운 파서 (523KB) | 분리하면 필요할 때만 로드 가능 |
| ** 특정 페이지에서만 쓰는 것** | rehype 플러그인들 | 글 상세 안 가면 안 받아도 됨 |
반대로, 작은 유틸리티 라이브러리를 일일이 chunk로 만들면 HTTP 요청 수만 늘어나고 오히려 느려집니다. 어느 정도 크기가 있는 것들만 분리하는 게 좋습니다.
빌드 분석
빌드 결과를 시각적으로 분석하고 싶으면 rollup-plugin-visualizer를 쓸 수 있습니다.
npm install --save-dev rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({ open: true }),
],
});
빌드하면 트리맵이 뜨는데, 어떤 라이브러리가 번들의 몇 퍼센트를 차지하는지 한눈에 볼 수 있습니다. 예상 외로 큰 라이브러리를 발견하는 경우가 종종 있습니다.
결과 비교
** 분리 전:**
index.js 1.5 MB (gzip: ~450 KB)
** 분리 후:**
vendor-react.js 47 kB (gzip: 17 kB) ← 거의 안 바뀜, 캐시 적중률 높음
vendor-markdown.js 523 kB (gzip: 161 kB) ← 글 상세에서만 로드
index.js 350 kB (gzip: 107 kB) ← 내 코드만, 자주 바뀌지만 작음
페이지별 chunk 4~10 kB each ← 필요할 때만 로드
총 크기는 비슷하지만, 사용자가 ** 실제로 받는 양 **은 크게 줄었습니다. 홈만 방문하면 마크다운 파서를 안 받아도 되고, 코드를 수정해 배포해도 라이브러리 chunk는 캐시에서 가져옵니다.
주의할 점
chunk를 너무 잘게 나누면 안 됩니다. HTTP/2에서 멀티플렉싱이 된다고 해도, 각 요청마다 오버헤드가 있습니다. 경험적으로 5~10개 정도의 chunk가 적당합니다.
그리고 manualChunks에서 라이브러리 간 의존성이 꼬이면 빌드가 실패하거나 런타임 에러가 날 수 있습니다. 같이 쓰이는 라이브러리는 같은 chunk에 넣는 게 안전합니다. React와 React DOM을 따로 분리하는 건 별 의미가 없는 것처럼요.