| name | vite-advanced-splitting |
| description | Vite 고급 코드 스플리팅 — manualChunks 함수형, 모바일/데스크톱 분리 빌드(mode), Gulp 빌드 훅 → Vite 플러그인 전환, vite:preloadError 재시도 처리 |
Vite 고급 코드 스플리팅 & 빌드 자동화
소스: https://vitejs.dev/config/build-options | https://vitejs.dev/guide/api-plugin | https://vitejs.dev/guide/build
검증일: 2026-08-11 (최초 작성 2026-04-20, Vite 8 대응 주의사항 추가)
주의 (Vite 8+): Vite 8부터 Rolldown이 기본 번들러로 전환되면서 build.rollupOptions는 build.rolldownOptions로 개명됨(기존 rollupOptions는 deprecated alias로 하위호환 유지, 당장 깨지지 않음). output.manualChunks 객체 형식은 더 이상 지원되지 않음(함수 형식은 deprecated로 계속 동작). 이 문서의 예시는 Vite 6.x 기준. 출처: https://vite.dev/guide/migration
1. manualChunks 전략
기본 형식 비교
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'ui-vendor': ['@mui/material'],
}
manualChunks(id: string) {
if (id.includes('node_modules')) { ... }
}
주의 (Vite 8+): 객체 형식 manualChunks는 미지원으로 전환됨. 아래 "패키지명 기반 자동 분할"의 함수 형식을 사용할 것.
패키지명 기반 자동 분할
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (!id.includes('node_modules')) return
const parts = id.split('/node_modules/')
const rawPkg = parts[parts.length - 1]
const pkg = rawPkg.startsWith('@')
? rawPkg.split('/').slice(0, 2).join('/')
: rawPkg.split('/')[0]
if (['react', 'react-dom', 'react-router-dom'].includes(pkg)) {
return 'common-react-dom'
}
if (pkg === 'swiper') return
(pkg.())
(pkg.() || pkg === || pkg === ) {
}
(pkg.())
(pkg.() && pkg.()) {
pkg
}
},
},
},
},
})
React.lazy + Suspense와 함께 사용
const ProductPage = lazy(() => import('./pages/ProductPage'))
manualChunks(id) {
if (id.includes('node_modules')) { ... }
}
2. 모바일 / 데스크톱 분리 빌드
환경 변수 + mode 방식
"build:mobile": "cross-env VITE_DEVICE_TYPE=1 vite build --mode mobile",
"build:desktop": "cross-env VITE_DEVICE_TYPE=2 vite build --mode desktop",
"dev:mobile": "cross-env VITE_DEVICE_TYPE=1 vite --mode mobile",
"dev:desktop": "cross-env VITE_DEVICE_TYPE=2 vite --mode desktop",
# .env.mobile
VITE_DEVICE_TYPE=1
VITE_ENTRY=src/mobile/index.tsx
# .env.desktop
VITE_DEVICE_TYPE=2
VITE_ENTRY=src/desktop/index.tsx
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react-swc'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const isMobile = env.VITE_DEVICE_TYPE === '1'
return {
plugins: [react()],
build: {
outDir: isMobile ? 'build/mobile' : 'build/desktop',
rollupOptions: {
input: isMobile ? 'src/mobile/index.html' : 'src/desktop/index.html',
},
},
define: {
__IS_MOBILE__: isMobile,
},
}
})
분리 entry (index.html이 루트 1개인 경우)
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const deviceType = env.VITE_DEVICE_TYPE ?? '1'
return {
plugins: [
react(),
{
name: 'device-entry',
transformIndexHtml(html) {
return html.replace(
'/src/index.tsx',
deviceType === '2' ? '/src/desktop/index.tsx' : '/src/mobile/index.tsx'
)
},
},
],
}
})
3. Gulp 빌드 스크립트 → Vite 플러그인 전환
Gulp의 pre/postbuild 태스크(version.json 생성, 빌드 타입 파일 생성)를 Vite 플러그인 훅으로 대체한다.
Gulp 태스크 원본
gulp.task('touchVersion', () => {
const version = `${format(new Date(), 'yyyyMMdd')}@${gitRev}`
fs.writeFileSync('src/version.json', JSON.stringify({ version }))
})
gulp.task('touchBuildType', () => {
const type = process.env.DEVICE_TYPE === '1' ? 'mobile' : 'desktop'
fs.writeFileSync(`build/${type}.json`, JSON.stringify({ type }))
})
Vite 플러그인으로 대체
import type { Plugin } from 'vite'
import { execSync } from 'child_process'
import fs from 'fs'
export function buildMetaPlugin(deviceType: string): Plugin {
return {
name: 'build-meta',
buildStart() {
const gitRev = execSync('git rev-parse --short HEAD').toString().trim()
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '')
const version = `${date}@${gitRev}`
fs.writeFileSync('src/version.json', JSON.stringify({ version }))
console.log(`[build-meta] version: ${version}`)
},
() {
= deviceType === ? :
outDir =
fs.(, .({ }))
fs.(, )
.()
},
}
}
import { buildMetaPlugin } from './plugins/build-meta'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [
react(),
buildMetaPlugin(env.VITE_DEVICE_TYPE ?? '1'),
],
}
})
Vite 플러그인 훅 실행 순서
build 명령 실행
│
├─ config() 설정 해석 (초기화)
├─ buildStart() 빌드 시작 ← prebuild 태스크
├─ transform() 파일 변환
├─ generateBundle() 번들 생성
├─ writeBundle() 파일 쓰기
└─ closeBundle() 빌드 완료 ← postbuild 태스크
4. 청크 로드 실패 재시도 (vite:preloadError)
webpack의 webpack-retry-chunk-load-plugin 대체 패턴.
index.html 직접 주입 플러그인
import type { Plugin } from 'vite'
export function retryChunkPlugin(maxRetries = 3): Plugin {
return {
name: 'retry-chunk-load',
apply: 'build',
transformIndexHtml: {
order: 'post',
handler(html) {
const script = `
<script>
(function() {
var retryCount = 0;
var MAX_RETRIES = ${maxRetries};
window.addEventListener('vite:preloadError', function(event) {
event.preventDefault();
if (retryCount < MAX_RETRIES) {
retryCount++;
var url = new URL(window.location.href);
url.searchParams.set('_retry', retryCount);
window.location.href = url.toString();
}
});
})();
</script>`
return html.replace('</head>', `${script}\n</head>`)
},
},
}
}
앱 코드에서 직접 처리 (플러그인 없이)
window.addEventListener('vite:preloadError', (event) => {
window.location.reload()
})
5. 빌드 출력 최적화
export default defineConfig({
build: {
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
manualChunks: { },
},
},
sourcemap: process.env.GENERATE_SOURCEMAP === 'true',
},
})
흔한 실수 패턴
1. manualChunks에서 앱 코드까지 지정
manualChunks(id) {
if (id.includes('src/pages')) return 'pages'
}
const ProductPage = lazy(() => import('./pages/ProductPage'))
2. loadEnv 미사용으로 .env.{mode} 파일 못 읽음
export default defineConfig({
build: { outDir: process.env.OUT_DIR }
})
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return { build: { outDir: env.OUT_DIR } }
})
3. closeBundle vs writeBundle 혼동
closeBundle() {
fs.copyFileSync('src/version.json', 'build/version.json')
}