Vite 패턴
Vite 8+ 프로젝트를 위한 빌드 도구 및 개발 서버 패턴입니다. 설정, 환경 변수, 프록시 설정, 라이브러리 모드, 의존성 사전 번들링 및 일반적인 프로덕션 문제를 다룹니다.
사용 시점
vite.config.ts또는vite.config.js설정- 환경 변수 또는
.env파일 설정 - API 백엔드를 위한 개발 서버 프록시 구성
- 빌드 출력 최적화 (청크, 축소, 에셋)
build.lib로 라이브러리 게시- 의존성 사전 번들링 또는 CJS/ESM 상호 운용성 문제 해결
- HMR, 개발 서버 또는 빌드 오류 디버깅
- Vite 플러그인 선택 또는 순서 지정
작동 방식
- 개발 모드는 소스 파일을 네이티브 ESM으로 제공합니다 — 번들링하지 않습니다. 모듈 요청 시 변환이 온디맨드로 발생하므로 콜드 스타트가 빠르고 HMR이 정확합니다.
- 빌드 모드는 Rolldown(v7+) 또는 Rollup(v5–v6)을 사용하여 트리 셰이킹, 코드 분할 및 Oxc 기반 축소를 통해 프로덕션용 애플리케이션을 번들링합니다.
- 의존성 사전 번들링은 CJS/UMD 의존성을 esbuild를 통해 한 번 ESM으로 변환하고 결과를
node_modules/.vite에 캐시하여 이후 시작 시 작업을 건너뜁니다. - 플러그인은 개발 및 빌드에서 통합 인터페이스를 공유합니다. 동일한 플러그인 객체가 개발 서버의 온디맨드 변환과 프로덕션 파이프라인 모두에서 작동합니다.
- 환경 변수는 빌드 타임에 정적으로 인라인됩니다.
VITE_접두사가 있는 변수는 번들에서 공개 상수가 되고, 접두사가 없는 것은 클라이언트 코드에 보이지 않습니다.
예제
설정 구조
기본 설정
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': new URL('./src', import.meta.url).pathname },
},
})
조건부 설정
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd()) // VITE_ 접두사만 (안전)
return {
plugins: [react()],
server: command === 'serve' ? { port: 3000 } : undefined,
define: {
__API_URL__: JSON.stringify(env.VITE_API_URL),
},
}
})
주요 설정 옵션
| 키 | 기본값 | 설명 |
|---|---|---|
root | '.' | 프로젝트 루트 (index.html이 있는 위치) |
base | '/' | 배포된 에셋의 공개 기본 경로 |
envPrefix | 'VITE_' | 클라이언트에 노출되는 환경 변수의 접두사 |
build.outDir | 'dist' | 출력 디렉터리 |
build.minify | 'oxc' | 축소기 ('oxc', 'terser' 또는 false) |
build.sourcemap | false | true, 'inline' 또는 'hidden' |
플러그인
필수 플러그인
대부분의 플러그인 요구 사항은 잘 유지 관리되는 몇 가지 패키지로 충족됩니다. 직접 작성하기 전에 먼저 이것들을 사용하세요.
| 플러그인 | 목적 | 사용 시점 |
|---|---|---|
@vitejs/plugin-react-swc | SWC를 통한 React HMR + Fast Refresh | React 앱의 기본 (Babel 변형보다 빠름) |
@vitejs/plugin-react | Babel을 통한 React HMR + Fast Refresh | Babel 플러그인이 필요한 경우에만 (emotion, MobX 데코레이터) |
@vitejs/plugin-vue | Vue 3 SFC 지원 | Vue 앱 |
vite-plugin-checker | HMR 오버레이와 함께 워커 스레드에서 tsc + ESLint 실행 | 모든 TypeScript 앱 — Vite는 vite build 중 타입 검사를 하지 않습니다 |
vite-tsconfig-paths | tsconfig.json paths 별칭 준수 | tsconfig.json에 이미 별칭이 있는 경우 |
vite-plugin-dts | 라이브러리 모드에서 .d.ts 파일 생성 | TypeScript 라이브러리 게시 |
vite-plugin-svgr | SVG를 React 컴포넌트로 가져오기 | SVG를 컴포넌트로 사용하는 React 앱 |
rollup-plugin-visualizer | 번들 트리맵/선버스트 보고서 | 주기적인 번들 크기 감사 (enforce: 'post' 사용) |
vite-plugin-pwa | 설정 없는 PWA + Workbox | 오프라인 지원 앱 |
중요 경고: vite build는 트랜스파일하지만 타입 검사는 하지 않습니다. vite-plugin-checker를 추가하거나 CI에서 tsc --noEmit을 실행하지 않으면 타입 오류가 자동으로 프로덕션에 배포됩니다.
커스텀 플러그인 작성
작성은 드뭅니다 — 대부분의 요구 사항은 기존 플러그인으로 충족됩니다. 필요할 때는 vite.config.ts에서 인라인으로 시작하고 재사용되는 경우에만 분리하세요.
// vite.config.ts — 최소 인라인 플러그인
function myPlugin(): Plugin {
return {
name: 'my-plugin', // 필수, 고유해야 함
enforce: 'pre', // 'pre' | 'post' (선택 사항)
apply: 'build', // 'build' | 'serve' (선택 사항)
transform(code, id) {
if (!id.endsWith('.custom')) return
return { code: transformCustom(code), map: null }
},
}
}
주요 훅: transform (소스 수정), resolveId + load (가상 모듈), transformIndexHtml (HTML에 주입), configureServer (개발 미들웨어 추가), hotUpdate (커스텀 HMR — v7+에서 폐지된 handleHotUpdate 대체).
가상 모듈은 \0 접두사 규칙을 사용합니다 — resolveId가 '\0virtual:my-id'를 반환하여 다른 플러그인이 건너뛰도록 합니다. 사용자 코드는 'virtual:my-id'를 가져옵니다.
전체 플러그인 API는 vite.dev/guide/api-plugin을 참조하세요. 개발 중에는 vite-plugin-inspect를 사용하여 변환 파이프라인을 디버깅하세요.
HMR API
프레임워크 플러그인(@vitejs/plugin-react, @vitejs/plugin-vue 등)은 HMR을 자동으로 처리합니다. 커스텀 상태 저장소, 개발 도구 또는 업데이트 간에 상태를 유지해야 하는 프레임워크에 구애받지 않는 유틸리티를 구축할 때만 import.meta.hot을 직접 사용하세요.
// src/store.ts — 바닐라 모듈의 수동 HMR
if (import.meta.hot) {
// 업데이트 간 상태 유지 (반드시 MUTATE해야 하며, .data를 재할당하지 마세요)
import.meta.hot.data.count = import.meta.hot.data.count ?? 0
// 모듈이 교체되기 전에 부작용 정리
import.meta.hot.dispose((data) => clearInterval(data.intervalId))
// 이 모듈의 자체 업데이트 수락
import.meta.hot.accept()
}
모든 import.meta.hot 코드는 프로덕션 빌드에서 트리 셰이킹됩니다 — 가드 제거가 필요 없습니다.
환경 변수
Vite는 .env, .env.local, .[mode], .env.[mode].local 파일을 순서대로 로드합니다 (나중에 로드된 것이 이전 것을 덮어씁니다). *.local 파일은 git에서 무시되며 로컬 비밀용입니다.
클라이언트 측 접근
VITE_ 접두사가 있는 변수만 클라이언트 코드에 노출됩니다:
import.meta.env.VITE_API_URL // 문자열
import.meta.env.MODE // 'development' | 'production' | 사용자 정의
import.meta.env.BASE_URL // 기본 설정 값
import.meta.env.DEV // 불리언
import.meta.env.PROD // 불리언
import.meta.env.SSR // 불리언
설정에서 환경 변수 사용
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd()) // VITE_ 접두사만 (안전)
return {
define: {
__API_URL__: JSON.stringify(env.VITE_API_URL),
},
}
})
보안
VITE_ 접두사는 보안 경계가 아닙니다
VITE_ 접두사가 있는 모든 변수는 빌드 타임에 클라이언트 번들에 정적으로 인라인됩니다. 축소, base64 인코딩 및 소스 맵 비활성화는 이를 숨기지 않습니다. 결정적인 공격자는 제공된 JavaScript에서 VITE_ 변수를 추출할 수 있습니다.
규칙: 공개 값(API URL, 기능 플래그, 공개 키)만 VITE_ 변수에 넣으세요. 비밀(API 토큰, 데이터베이스 URL, 개인 키)은 반드시 API 또는 서버리스 함수 뒤에 서버 측에 있어야 합니다.
loadEnv('') 함정
// 나쁨: 세 번째 인수로 ''를 전달하면 모든 환경 변수(서버 비밀 포함)를 로드하여
// `define`을 통해 클라이언트 코드에 인라인할 수 있게 됩니다.
const env = loadEnv(mode, process.cwd(), '')
// 좋음: 명시적 접두사 목록
const env = loadEnv(mode, process.cwd(), ['VITE_', 'APP_'])
프로덕션의 소스 맵
프로덕션 소스 맵은 원본 소스 코드를 유출합니다. 오류 추적기(Sentry, Bugsnag)에 업로드하지 않는 한 비활성화하고 그 후 로컬에서 삭제하세요:
build: {
sourcemap: false, // 기본값 — 이렇게 유지하세요
}
.gitignore 체크리스트
.env.local,.env.*.local— 로컬 비밀 재정의dist/— 빌드 출력node_modules/.vite— 사전 번들 캐시 (오래된 항목으로 인해 유령 오류 발생)
서버 프록시
// vite.config.ts — server.proxy
server: {
proxy: {
'/foo': 'http://localhost:4567', // 문자열 약어
'/api': {
target: 'http://localhost:8080',
changeOrigin: true, // 가상 호스트 백엔드에 필요
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
}
WebSocket 프록시의 경우 경로 구성에 ws: true를 추가하세요.
빌드 최적화
수동 청크
// vite.config.ts — build.rolldownOptions
build: {
rolldownOptions: {
output: {
// 객체 형태: 특정 패키지 그룹화
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-popover'],
},
},
},
}
// 함수 형태: 휴리스틱으로 분할
manualChunks(id) {
if (id.includes('node_modules/react')) return 'react-vendor'
if (id.includes('node_modules')) return 'vendor'
}
성능
배럴 파일 회피
배럴 파일(디렉토리의 모든 것을 다시 내보내는 index.ts)은 단일 심볼을 가져올 때도 Vite가 다시 내보낸 모든 파일을 로드하도록 강제합니다. 공식 문서에서 지목하는 1위 개발 서버 속도 저하 원인입니다.
// 나쁨 — 하나의 유틸을 가져오면 Vite가 전체 배럴을 로드함
import { slash } from '@/utils'
// 좋음 — 직접 가져오기, 해당 파일만 로드됨
import { slash } from '@/utils/slash'
가져오기 확장자를 명시적으로 지정
각 암시적 확장자는 resolve.extensions를 통해 최대 6번의 파일 시스템 검사를 강제합니다. 대규모 코드베이스에서는 이 비용이 누적됩니다.
// 나쁨
import Component from './Component'
// 좋음
import Component from './Component.tsx'
tsconfig.json의 allowImportingTsExtensions와 resolve.extensions를 실제 사용하는 확장자로만 좁히세요.
핫 경로 웜업
server.warmup.clientFiles는 브라우저가 요청하기 전에 알려진 핫 진입점을 사전 변환하여 대규모 앱의 콜드 로드 요청 폭포를 제거합니다.
// vite.config.ts
server: {
warmup: {
clientFiles: ['./src/main.tsx', './src/routes/**/*.tsx'],
},
}
느린 개발 서버 프로파일링
vite dev가 느리다고 느껴지면 vite --profile로 시작하고, 앱과 상호 작용한 다음 p+enter를 눌러 .cpuprofile을 저장하세요. Speedscope에서 로드하여 어떤 플러그인이 시간을 소비하는지 찾으세요 — 일반적으로 커뮤니티 플러그인의 buildStart, config 또는 configResolved 훅입니다.
라이브러리 모드
npm 패키지를 게시할 때는 build.lib를 사용하세요. 설정 세부 사항보다 두 가지 함정이 더 중요합니다:
- 타입이 생성되지 않습니다 —
vite-plugin-dts를 추가하거나tsc --emitDeclarationOnly를 별도로 실행하세요. - 피어 의존성은 반드시 외부화되어야 합니다 — 목록에 없는 피어가 라이브러리에 번들링되어 사용자에게 중복 런타임 오류를 발생시킵니다.
// vite.config.ts
build: {
lib: {
entry: 'src/index.ts',
formats: ['es', 'cjs'],
fileName: (format) => `my-lib.${format}.js`,
},
rolldownOptions: {
external: ['react', 'react-dom', 'react/jsx-runtime'], // 모든 피어 의존성
},
}
SSR 외부화
단순 createServer({ middlewareMode: true }) 설정은 프레임워크 작성자의 영역입니다. 대부분의 앱은 Nuxt, Remix, SvelteKit, Astro 또는 TanStack Start를 대신 사용해야 합니다. 프레임워크 사용자로서 조정할 것은 의존성이 SSR에서 깨질 때 외부화 설정입니다:
// vite.config.ts — ssr 옵션
ssr: {
external: ['node-native-package'], // SSR 번들에서 require()로 유지
noExternal: ['esm-only-package'], // SSR 출력에 강제 번들링 (대부분의 SSR 오류 수정)
target: 'node', // 'node' 또는 'webworker'
}
의존성 사전 번들링
Vite는 의존성을 사전 번들링하여 CJS/UMD를 ESM으로 변환하고 요청 수를 줄입니다.
// vite.config.ts — optimizeDeps
optimizeDeps: {
include: [
'lodash-es', // 알려진 무거운 의존성 강제 사전 번들링
'cjs-package', // 상호 운용성 문제를 일으키는 CJS 의존성
'deep-lib/components/**', // 깊은 가져오기를 위한 glob
],
exclude: ['local-esm-package'], // 제외된 경우 유효한 ESM이어야 함
force: true, // 캐시 무시, 다시 최적화 (임시 디버깅)
}
일반적인 함정
개발과 빌드 불일치
개발은 변환에 esbuild/Rolldown을 사용하고, 빌드는 번들링에 Rolldown을 사용합니다. CJS 라이브러리는 둘 사이에서 다르게 동작할 수 있습니다. 배포하기 전에 항상 vite build && vite preview로 확인하세요.
배포 후 오래된 청크
새 빌드는 새 청크 해시를 생성합니다. 활성 세션이 있는 사용자는 더 이상 존재하지 않는 이전 파일 이름을 요청합니다. Vite에는 기본 제공 솔루션이 없습니다. 완화 방법:
- 배포 기간 동안 이전
dist/assets/파일을 유지 - 라우터에서 동적 가져오기 오류를 잡아서 페이지 새로고침 강제
Docker 및 컨테이너
Vite는 기본적으로 localhost에 바인딩되며 컨테이너 외부에서 접근할 수 없습니다:
// vite.config.ts — Docker/컨테이너 설정
server: {
host: true, // 0.0.0.0에 바인딩
hmr: { clientPort: 3000 }, // 리버스 프록시 뒤에 있는 경우
}
모노레포 파일 접근
Vite는 파일 제공을 프로젝트 루트로 제한합니다. 루트 외부의 패키지는 차단됩니다:
// vite.config.ts — 모노레포 파일 접근
server: {
fs: {
allow: ['..'], // 상위 디렉터리 허용 (워크스페이스 루트)
},
}
안티 패턴
// 나쁨: envPrefix를 ''로 설정하면 모든 환경 변수(비밀 포함)가 클라이언트에 노출됨
envPrefix: ''
// 나쁨: Vite 번들에서 require()가 작동한다고 가정