Vite 8이 빌드를 최대 97% 단축한 이유: Rolldown이 esbuild·Rollup 이중 구조를 대체하기까지
프론트엔드 개발을 하다 보면 한 번쯤은 이런 상황을 겪어봤을 겁니다. 로컬에서는 분명히 잘 동작하는데, 프로덕션 빌드를 올리고 나면 무언가 조용히 깨져 있는 것. 저도 처음엔 코드 문제인 줄 알고 몇 시간을 헤맸는데, 알고 보니 개발 서버와 프로덕션 빌드가 서로 다른 번들러를 쓰기 때문에 발생하는 동작 불일치였습니다.
사실 이건 Vite의 설계상 어쩔 수 없는 트레이드오프였습니다. Vite가 처음 등장할 때, 빠른 개발 서버를 위해서는 Go로 작성된 esbuild가 최선이었고, 트리쉐이킹과 청크 분할 같은 프로덕션 최적화는 Rollup이 훨씬 성숙해 있었습니다. 두 도구를 조합한 건 당시로서는 최선의 선택이었지만, 동시에 "환경 의존적인 버그"의 씨앗이기도 했죠.
2026년 3월 12일 정식 출시된 Vite 8은 이 구조를 드디어 해소했습니다. Rust로 작성된 Rolldown 하나가 개발과 프로덕션 전체 파이프라인을 담당하게 되었고, 그 결과 Plaid의 실제 모노레포 환경에서는 빌드 시간이 97%까지 단축되었습니다. 이 글을 읽고 나면 Rolldown이 왜 빠른지, 업그레이드 시 어디서 막히는지, 그리고 지금 당장 팀에서 검토할 체크리스트를 손에 쥘 수 있습니다.
핵심 개념
Vite의 이중 번들러 구조가 문제였던 이유
Vite 7까지의 구조를 간단히 정리하면 이렇습니다.
| 환경 | 번들러 | 역할 |
|---|---|---|
| 개발 서버 | esbuild (Go) | 의존성 사전 번들링, 빠른 HMR |
| 프로덕션 빌드 | Rollup (JavaScript) | 최적화, 트리쉐이킹, 청크 분할 |
두 번들러가 각자의 방식으로 모듈을 해석하다 보니, 엣지 케이스에서 결과물이 달라지는 경우가 생겼습니다. 플러그인도 두 종류의 API를 신경 써야 했고, 소스맵 일관성 같은 미묘한 문제들도 여기서 비롯됐습니다. 환경 의존적인 버그의 온상이었죠.
이 문제를 구조적으로 해결하려면 두 번들러를 하나로 합쳐야 했는데, 선택지는 두 가지였습니다. 기존 JavaScript 기반 도구를 재사용하거나, 처음부터 Rust로 새로 만들거나. VoidZero 팀이 선택한 건 후자였습니다.
왜 Rolldown이 빠른가
Rolldown은 VoidZero 팀이 Rust로 개발한 JavaScript/TypeScript 번들러입니다. 이름에서 보이듯 Rollup의 API 호환성을 유지하면서, 실행 엔진을 통째로 Rust로 바꾼 것이 핵심입니다.
속도 차이가 나는 이유는 세 가지입니다.
첫째, GC(가비지 컬렉션)가 없습니다. JavaScript 런타임 기반의 번들러는 큰 모듈 그래프를 처리할 때 GC가 작동하면서 일시적으로 멈추는 순간(stop-the-world pause)이 발생합니다. Rust는 소유권 모델로 메모리를 관리하기 때문에 이런 정지가 없습니다.
둘째, 진정한 멀티스레드 병렬 처리가 가능합니다. Node.js의 싱글 스레드 제약 없이 모듈 그래프 전체를 병렬로 처리할 수 있습니다. 19,000개 모듈을 기준으로 한 공식 합성 벤치마크에서 Rollup(40.10초) 대비 Rolldown(1.61초)이 약 25배 빠르게 나온 게 이 때문입니다.
셋째, Oxc 단일 파이프라인입니다. 파싱, 변환, 최소화 전 단계를 Oxc라는 동일한 Rust 기반 컴파일러가 처리합니다. 기존 툴체인에서는 Babel이 변환하고, esbuild가 다시 파싱해서 최소화하는 식으로 여러 도구가 AST를 재파싱했는데, 이 중복 비용이 사라집니다.
Oxc(Oxidation Compiler)란? Rust로 작성된 JavaScript 파서·트랜스파일러·미니파이어 통합 도구입니다. 기존 JavaScript 툴체인에서 Babel(변환)과 esbuild(미니파이)가 담당하던 역할을 단일 Rust 바이너리로 처리합니다.
단일 팀의 수직 통합 툴체인
Vite 8에서 또 하나 주목할 점은 구조적 통합입니다.
VoidZero 조직
├── Vite — 빌드 도구, 개발 서버
├── Rolldown — 번들러 엔진 (esbuild + Rollup 대체)
└── Oxc — 파서/변환기/미니파이어 (Babel + esbuild 변환 역할 대체)오픈소스 생태계에서 서로 다른 팀이 만든 도구를 엮으면 "A는 지원하는데 B가 아직 안 된다"는 버전 미스매치 문제가 자주 생깁니다. 세 도구가 같은 조직에서 함께 발전하면 이 문제가 내부화됩니다. 실제로 Rolldown 1.0 안정 버전이 Vite 8 출시에 맞춰 동시에 나온 것도 이 구조 덕분이죠.
이제 이 구조가 실제 프로젝트에서 어떻게 적용되는지 살펴볼게요.
실전 적용
예시 1: Vite 7에서 Vite 8으로 업그레이드
솔직히 말하면, 표준적인 Vite 설정을 쓰고 있다면 업그레이드 자체는 예상보다 훨씬 간단합니다. Vite 8에는 기존 rollupOptions와 esbuild 설정을 자동으로 변환하는 호환성 레이어가 내장되어 있어서입니다.
# 패키지 업그레이드
pnpm add -D vite@8
# React 사용 중이라면 플러그인도 함께 업그레이드해주시면 좋습니다
pnpm add -D @vitejs/plugin-react@latest
# Vue라면
pnpm add -D @vitejs/plugin-vue@latest@vitejs/plugin-react처럼 Vite 버전과 맞춰 올라가야 하는 프레임워크 플러그인들이 있습니다. 업그레이드 후 피어 의존성 경고가 뜬다면 이 부분을 먼저 확인해보시는 게 좋습니다.
// vite.config.ts — 기존 설정 그대로 동작 (호환성 레이어가 처리)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
// 기존 rollupOptions 그대로 유지 가능 — Rolldown이 내부적으로 변환
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
})// vite.config.ts — Vite 8 네이티브 설정으로 명시적 전환 시
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
// rolldownOptions로 전환하면 Rolldown 고유 기능 활용 가능
rolldownOptions: {
output: {
// advancedChunks는 manualChunks와 달리 패턴 기반으로 여러 그룹을 조합하거나
// 크기 기반 자동 분할과 병행 사용이 가능해 유연도가 높습니다
advancedChunks: {
groups: [
{ name: 'react-core', test: /node_modules\/(react|react-dom)/ },
{ name: 'vendor', test: /node_modules/ },
],
},
},
},
},
})| 설정 키 | Vite 7 | Vite 8 | 비고 |
|---|---|---|---|
build.rollupOptions |
Rollup에 직접 전달 | 호환성 레이어로 자동 변환 | 기존 설정 그대로 동작 |
build.rolldownOptions |
없음 | Rolldown 네이티브 설정 | 새 기능 활용 시 사용 |
optimizeDeps.esbuildOptions |
esbuild에 직접 전달 | 대부분 자동 변환, 일부 옵션 제외 | 하단 주의사항 참고 |
예시 2: 플러그인 마이그레이션 체크포인트
커스텀 플러그인보다 먼저 확인할 게 있습니다. package.json의 devDependencies에서 vite-plugin-*, rollup-plugin-* 항목을 찾아보고, 각 플러그인의 npm 페이지나 GitHub에서 Vite 8 지원 여부를 확인해보시면 됩니다. 이 작업이 선행되지 않으면 빌드는 되는데 런타임에서 이상하게 동작하는 상황이 생길 수 있습니다.
그 다음으로, transformWithEsbuild를 직접 쓰는 커스텀 플러그인이 있다면 확인이 필요합니다. 이 경우 두 가지 선택지가 있습니다.
// 기존 방식 — esbuild 직접 의존
import { transformWithEsbuild } from 'vite'
const myPlugin = {
name: 'my-plugin',
async transform(code, id) {
if (id.endsWith('.tsx')) {
const result = await transformWithEsbuild(code, id)
return result
}
},
}// 옵션 A — Oxc 기반으로 마이그레이션 (Vite 8 권장 방향)
// API명은 Vite 8 공식 마이그레이션 가이드에서 최신 버전을 확인하시는 것을 권장합니다
import { transformWithOxc } from 'vite'
const myPlugin = {
name: 'my-plugin',
async transform(code, id) {
if (id.endsWith('.tsx')) {
const result = await transformWithOxc(code, id)
return result
}
},
}// 옵션 B — esbuild 별도 설치 후 유지 (점진적 마이그레이션 전략)
import { transform } from 'esbuild'
const myPlugin = {
name: 'my-plugin',
async transform(code, id) {
if (id.endsWith('.tsx')) {
const result = await transform(code, { loader: 'tsx' })
return { code: result.code }
}
},
}그리고 실제로 터지는 케이스 하나를 짚어드릴게요. mangleProps를 써서 번들 크기를 최적화하고 있었다면, Oxc 미니파이어는 이 옵션을 지원하지 않습니다.
// 이 설정은 Vite 8에서 조용히 무시됩니다
export default defineConfig({
build: {
minify: 'oxc', // Vite 8 기본값
esbuildOptions: {
// mangleProps, mangleCache는 Oxc에서 지원하지 않아 동작하지 않습니다
// 결과적으로 내부 프로퍼티 이름이 축약되지 않아 번들 크기가 예상보다 크게 나옵니다
mangleProps: /^_/,
mangleCache: {},
},
},
})에러가 나거나 경고가 뜨는 게 아니라 그냥 무시되기 때문에, 원인을 찾기까지 시간이 걸릴 수 있습니다. 이 기능에 의존하고 있다면 별도 후처리 플러그인을 구성해야 합니다.
프로퍼티 맹글링(Property Mangling)이란? 번들 크기를 줄이기 위해
myLongPropertyName을a처럼 짧게 바꾸는 최적화 기법입니다. 내부 프로퍼티 이름을 외부에 노출하지 않는 라이브러리에서 주로 활용합니다. esbuild의mangleProps옵션이 이 역할을 했는데, Oxc 미니파이어는 현재 이를 지원하지 않습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 빌드 속도 | 19,000 모듈 합성 벤치마크 기준 Rollup 대비 25배, Plaid 실제 모노레포 환경에서 97% 단축 |
| 개발/프로덕션 일관성 | 동일 번들러 사용으로 환경 의존적 버그 구조적 제거 |
| 단일 플러그인 API | esbuild + Rollup 이중 생태계 대신 단일 API, 호환성 문제 감소 |
| 소스맵 일관성 | 단일 변환 파이프라인으로 개발/프로덕션 소스맵 불일치 감소 |
| 지속 캐싱 | 모듈 수준 Persistent Cache 지원으로 증분 빌드 향상 |
| Module Federation | Rolldown 지원으로 Vite 프로젝트에서 공식 활용 가능 (마이크로 프론트엔드 구성 시 유용) |
단점 및 주의사항
| 항목 | 내용 |
|---|---|
| 설치 크기 증가 | 기존 대비 약 +15MB (lightningcss +10MB, Rolldown 바이너리 +5MB) |
| 프로퍼티 맹글링 미지원 | Oxc가 mangleProps, mangleCache 등 미지원 |
| 커스텀 플러그인 검증 필요 | esbuild 내부 API 의존 플러그인은 동작 확인 필요 |
| SSR 복잡 설정 | SSR 빌드 + 커스텀 청크 전략 조합은 엣지 케이스 존재 |
대응 방안:
- 설치 크기 증가 — CI 캐시 레이어를 잘 나눠두면 체감이 크지 않습니다. Docker를 쓴다면 레이어 분리로 충분히 흡수 가능합니다.
- 프로퍼티 맹글링 — 별도 후처리 플러그인 또는 terser 사용을 검토해볼 수 있습니다.
- 커스텀 플러그인 — 스테이징 환경에서 사전 검증 후 프로덕션 배포하는 것을 권장합니다.
- SSR 엣지 케이스 — 로컬에서 멀쩡했던 게 스테이징에서 터지는 경우가 있습니다. 실제 서버 환경과 유사한 스테이징 검증을 꼭 거치는 것을 권장합니다.
실무에서 가장 흔한 실수
- 서드파티 플러그인 호환성 확인 없이 바로 프로덕션 배포 —
vite-plugin-*목록을 먼저 점검하지 않으면 빌드는 되는데 런타임에서 이상하게 동작하는 경우가 생깁니다. mangleProps의존 설정을 그대로 이관 — Oxc가 이 옵션을 조용히 무시한다는 사실을 모르면, 번들 크기가 기대보다 크게 나왔을 때 원인을 찾기가 쉽지 않습니다.- SSR 환경을 로컬에서만 검증 — 클라이언트 빌드는 별 이상 없어도, SSR과 커스텀 청크 전략이 맞물리는 부분은 실제 서버 환경에서 예상과 다르게 동작하는 경우가 있습니다.
마치며
지금까지 살펴본 것처럼, Vite 8의 핵심은 단순한 속도 향상이 아니라 개발과 프로덕션이 다른 번들러를 쓰던 이중 구조를 아키텍처 수준에서 해소한 것입니다. 그 결과로 환경 의존적 버그가 구조적으로 제거됐고, 성능 향상은 덤으로 따라왔습니다.
표준적인 Vite 설정을 쓰는 프로젝트라면 생각보다 훨씬 부드럽게 전환이 됩니다. 지금 프로젝트에서 가장 먼저 확인할 것은 커스텀 플러그인의 esbuild 의존 여부와 mangleProps 사용 여부입니다. 이 두 가지가 없다면 거의 바로 올릴 수 있습니다.
지금 바로 시작해볼 수 있는 3단계:
package.json의devDependencies에서vite-plugin-*,rollup-plugin-*항목과mangleProps사용 여부를 확인해보시면 됩니다.- 별도 브랜치를 따서
pnpm add -D vite@8로 업그레이드해보시면 됩니다.pnpm build와pnpm dev두 가지를 모두 실행해보시고, SSR이 있다면 서버 빌드도 함께 검증하는 것을 권장합니다. time pnpm build로 빌드 전후 시간을 비교해보시면 실제 효과를 직접 확인할 수 있습니다. Plaid의 97%나 Linear의 87%만큼이 아니더라도, 프로젝트 규모에 따라 체감할 수 있는 차이가 나올 가능성이 높습니다.
참고 자료
- Vite 8.0 is out! | vite.dev
- Vite 8 Beta: The Rolldown-powered Vite | vite.dev
- Announcing Rolldown 1.0 | VoidZero
- Migration from v7 | Vite 공식 마이그레이션 가이드
- How PLAID Cut Build Times by 97% Migrating From Rollup To Rolldown | VoidZero
- Vite team claims 10-30x faster builds with Rolldown | The Register
- Vite 8, Rolldown, and Oxc: Rust Is Taking Over the JavaScript Toolchain | DEV Community
- Announcing Rolldown 1.0 RC | VoidZero
- VoidZero's Rolldown Library: Rollup Compatible API with the Speed of Rust | InfoQ