Vite 8 Rolldown 마이그레이션: 기존 Rollup 플러그인을 수정 없이 살리면서 빌드를 10~30배 빠르게
저도 처음엔 "또 생태계가 뒤집히나" 싶어서 꽤 긴장했습니다. Vite 8이 Rolldown을 기본 번들러로 채택한다는 소식이 나왔을 때, 이미 잘 돌아가는 Rollup 플러그인들을 전부 다시 써야 하는 건 아닌지부터 걱정이 됐거든요. 그런데 직접 마이그레이션해보고 나서는 생각이 많이 바뀌었습니다.
이 글을 읽으면 기존 프로젝트를 Vite 8로 올릴 때 어떤 부분이 걸리는지 미리 파악할 수 있고, 새 플러그인을 작성할 때 처음부터 성능을 챙기는 패턴을 가져가실 수 있습니다. Rollup 훅이 생소하신 분을 위해 한 줄만 짚고 넘어가자면, 훅이란 빌드 과정의 특정 시점(파일 로딩, 변환, 청크 생성 등)에 실행되는 함수입니다. 플러그인은 이 훅들을 구현해 번들러의 동작을 확장하고요. 또한 이 글은 프로덕션 빌드 타임 성능에 집중합니다. 개발 서버 HMR 성능 변화는 별도 주제입니다.
Rolldown은 기존 Rollup 플러그인 API를 그대로 구현하면서도, Rust 레이어에서 동작하는 훅 필터로 대규모 프로젝트에서 10~30배의 빌드 성능 향상을 이끌어냅니다. 그리고 이 두 가지가 대부분의 경우 코드 수정 없이 가능합니다.
Rolldown이 플러그인 호환성을 보장하는 두 가지 방식
첫 번째: Rollup Plugin API를 그대로 구현한다
Rolldown은 Rust로 작성된 고성능 JavaScript/TypeScript 번들러로, VoidZero(Evan You가 설립)에서 개발하고 있습니다. 2026년 초 1.0 정식 출시와 함께 SemVer를 준수하고, 공개 API(옵션명·타입·플러그인 훅 시그니처)가 안정화되었습니다.
Rolldown의 설계 원칙 중 가장 중요한 건 단순합니다. "기존 Rollup 플러그인이 수정 없이 동작해야 한다." 만약 동작하지 않는다면, 그건 Rolldown의 버그로 취급합니다.
실제로 resolveId, load, transform, renderChunk, generateBundle 같은 Rollup의 핵심 훅 인터페이스를 동일하게 구현하고 있어요. 공식 호환성 트래킹 이슈(GitHub #819) 기준으로, Vite 8에서 기존 Rollup 플러그인의 90% 이상이 코드 수정 없이 호환됩니다.
두 번째: 훅 필터(Hook Filter) — Rust가 먼저 걸러준다
솔직히 이 부분이 저한테 가장 흥미로웠습니다. 기존 Rollup 방식에서는 플러그인 훅이 호출되면 훅 내부에서 직접 조건을 검사합니다. 예를 들어 transform 훅에서 .vue 파일인지 확인하는 패턴이 대표적이죠.
// 기존 Rollup 방식: 훅 내부에서 조건 검사
{
name: 'vue-transform',
transform(code, id) {
if (!id.endsWith('.vue')) return null; // JS → Rust 컨텍스트 전환이 이미 발생한 뒤
// 실제 처리 로직
}
}문제는 이 과정에서 매번 Rust → JavaScript → Rust로의 컨텍스트 전환이 발생한다는 점입니다. 쉽게 말해, Rust로 짜인 번들러 코드가 JS 함수를 호출하는 데 비용이 드는데, 파일 수백 개를 처리하는 동안 이 전환이 반복되면서 오버헤드가 쌓입니다.
Rolldown의 훅 필터는 이 조건을 Rust 레이어에서 먼저 평가합니다. 조건에 맞지 않으면 JS 컨텍스트 전환 자체가 일어나지 않습니다.
// Rolldown 방식: Rust 레이어에서 사전 평가
{
name: 'vue-transform',
transform: {
filter: { id: /\.vue$/ }, // Rust에서 평가 — JS 진입 없음
handler(code, id) {
// 여기까지 왔다면 무조건 .vue 파일
// 실제 처리 로직
}
}
}훅 필터는 Rollup 4.38.0+, Vite 6.3.0+, Rolldown 전 버전에서 지원됩니다. 생태계 전반으로 표준화가 진행 중이므로, 새 플러그인을 만들 때는 처음부터 이 방식으로 작성해보시면 좋습니다.
withFilter — 남의 플러그인에 필터 주입하기
제가 실무에서 자주 맞닥뜨리는 상황인데, 서드파티 플러그인이 훅 필터를 지원하지 않을 때가 있습니다. 이때 withFilter 유틸리티를 활용하면 플러그인 소스를 건드리지 않고도 외부에서 필터를 주입할 수 있습니다.
import { withFilter } from 'rolldown'
import somePlugin from 'some-rollup-plugin'
export default {
plugins: [
withFilter(somePlugin(), {
transform: { filter: { moduleType: 'ts' } }
})
]
}플러그인 제작자가 업데이트하지 않아도 사용자 쪽에서 바로 성능 이점을 챙길 수 있다는 게 꽤 실용적인 접근입니다.
실전 적용
예시 1: 기존 Vite 7 프로젝트를 Vite 8로 올리기
대부분의 경우 vite.config.ts를 거의 손댈 필요가 없습니다. Vite 8은 Rolldown을 기본 번들러로 채택했기 때문에 별도 설정 없이 전환됩니다.
// vite.config.ts (Vite 8) — 기존 Rollup 플러그인 그대로 사용 가능
import { defineConfig } from 'vite'
import legacy from '@vitejs/plugin-legacy'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue(), legacy()],
// 추가 Rolldown 설정 불필요
})| 항목 | 설명 |
|---|---|
@vitejs/plugin-vue |
수정 없이 호환 |
@vitejs/plugin-legacy |
수정 없이 호환 |
| CJS 기반 레거시 패키지 | legacy: { inconsistentCjsInterop: true } 옵션 필요 |
예시 2: Rolldown 네이티브 플러그인 직접 작성하기
새 플러그인을 만들 때는 처음부터 훅 필터를 활용하는 방식으로 작성하면 됩니다. JSON 파일을 처리하는 간단한 플러그인 예시입니다.
import { readFile } from 'node:fs/promises'
import type { Plugin } from 'rolldown'
const myJsonPlugin: Plugin = {
name: 'my-json-plugin',
load: {
filter: { id: /\.json$/ },
async handler(id) {
const data = await readFile(id, 'utf-8')
return {
code: `export default ${data}`,
moduleType: 'js' // JS 이외 형식 변환 시 반드시 명시
}
}
}
}
moduleType: 'js'— Rolldown에서 JSON, CSS 등 JS가 아닌 모듈을 처리한 뒤 반환할 때 반드시 명시해야 합니다. 빠뜨리면 Rolldown이 모듈 타입을 파악하지 못해Cannot parse module같은 불명확한 파싱 에러가 발생하거나, 조용히 빈 모듈로 처리됩니다. 에러 메시지가 불명확해서 원인 찾는 데 시간이 꽤 걸립니다.
예시 3: 점진적 마이그레이션 — rolldown-vite로 먼저 검증하기
복잡한 프로젝트라면 Vite 7 환경에서 rolldown-vite를 먼저 적용해 호환성 문제를 격리하는 2단계 접근을 권장합니다.
# 1단계: Vite 7 + Rolldown 실험적 통합
pnpm add -D rolldown-vite// vite.config.ts (1단계)
import { defineConfig } from 'rolldown-vite' // vite 대신 rolldown-vite에서 import
export default defineConfig({
plugins: [/* 기존 플러그인 그대로 */],
})이 상태로 빌드를 돌려보면서 터미널에 unsupported hook 경고나 플러그인 관련 에러가 뜨는지 확인합니다. 이 단계에서 걸리는 플러그인이 있다면, Vite 8 전환 전에 미리 대응할 수 있습니다.
# 2단계: 호환성 확인 후 Vite 8 정식 업그레이드
pnpm add -D vite@latest// vite.config.ts (2단계)
import { defineConfig } from 'vite' // 다시 vite로 복원| 단계 | 패키지 | 목적 |
|---|---|---|
| 1단계 | rolldown-vite |
기존 환경에서 호환성 문제 사전 확인 |
| 2단계 | vite@8 |
정식 Rolldown 빌드 환경으로 전환 |
예시 4: CJS 레거시 패키지 임시 대응
CommonJS 모듈을 사용하는 오래된 패키지가 문제를 일으킬 때는 아래 옵션으로 임시 대응할 수 있습니다.
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
legacy: {
inconsistentCjsInterop: true // CJS/ESM 혼용 패키지 임시 호환
}
})이 옵션은 임시 해결책입니다. 장기적으로는 해당 패키지가 ESM을 지원하도록 업데이트되거나, ESM 대체 패키지로 교체하는 방향이 자연스럽습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 빌드 속도 | Rust 기반으로 기존 Rollup 대비 10~30배 빠름 (공식 벤치마크 기준) |
| 높은 호환성 | Rollup 플러그인 API를 그대로 구현해 기존 생태계 자산 재활용 가능 |
| 훅 필터 | Rust 레이어 사전 평가로 대형 프로젝트에서 성능 이점이 극대화 |
| Vite 8 통합 | 별도 설정 없이 Vite 8 업그레이드만으로 혜택 수혜 |
| Stable API | 1.0 이후 SemVer 보장으로 플러그인 생태계 안정성 확보 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 부분 지원 훅 | renderChunk, augmentChunkHash 등 일부 복잡한 훅 미완성 |
공식 호환성 트래킹 이슈 #819 확인 후 판단 |
| TS/JSX 처리 | Rolldown이 내부적으로 변환하므로 transform 훅이 TS/JSX 입력을 받을 수 있어야 함 | 플러그인 로직에서 TS/JSX 입력 처리 가능 여부 검토 |
| esbuild 의존 플러그인 | transformWithEsbuild 사용 플러그인 미지원 |
transformWithOxc API로 마이그레이션 |
| 다중 출력 동작 차이 | Rollup은 단일 프로세스, Rolldown은 출력별 분리 처리 | 출력 간 상태 공유 로직 재검토 필요 |
| Composable Filters | @rolldown/pluginutils의 고급 필터 조합은 Vite/unplugin 미지원 |
현재는 단순 필터만 사용, 추후 업데이트 대기 |
Oxc — Rolldown이 내부적으로 사용하는 Rust 기반 JavaScript/TypeScript 파서 및 트랜스파이머입니다. 기존 Vite가 esbuild를 사용하던 자리를 대체하며, Vite 8에서는
transformWithOxcAPI가 권장됩니다.
실무에서 가장 흔한 실수
-
moduleType: 'js'누락: 저는 JSON 플러그인 작성 중에 이걸 빠뜨리고 30분을 날렸습니다.Cannot parse module에러가 뜨는데 원인이 플러그인인지 파일인지 한참 헷갈렸거든요. JSON이나 CSS를 처리하는load/transform훅에서 반환값에moduleType: 'js'를 항상 명시하는 습관을 들이시면 좋습니다. -
esbuild 의존 플러그인 그대로 사용:
transformWithEsbuild를 내부적으로 사용하는 플러그인은 Vite 8에서 동작하지 않습니다. 플러그인 README나 소스에서esbuild의존성이 있는지 미리 확인하는 것을 권장합니다. 대부분의 경우transformWithOxc기반 대안 플러그인이 이미 존재합니다. -
다중 출력 플러그인에서 전역 상태 공유: Rollup 방식처럼 모든 출력을 단일 프로세스에서 처리한다고 가정하고 플러그인에서 전역 상태를 공유하면, Rolldown의 출력별 분리 처리 방식과 충돌할 수 있습니다.
renderChunk같은 훅에서 전역 맵을 유지하는 패턴이 특히 주의가 필요한데, 저도 이 부분에서 한 번 트러블슈팅을 거쳤습니다.
마치며
저도 처음엔 "생태계가 또 크게 흔들리나" 싶었는데, 막상 해보니 생각보다 훨씬 순탄했습니다. Rolldown은 기존 Rollup 생태계를 대체하는 것이 아니라, 그 위에 Rust의 성능을 얹는 방식으로 설계되어 있어서 마이그레이션 부담이 실제로 낮습니다. 기존 30초 빌드가 5초 이하로 줄어드는 경우도 흔하게 목격했습니다.
지금 바로 시작할 수 있는 3단계:
- 현재 프로젝트에서
rolldown-vite로 호환성을 먼저 점검합니다.pnpm add -D rolldown-vite후vite.config.ts의 import 경로만 변경하면 기존 Vite 7 환경에서 Rolldown으로 빌드됩니다. 이 단계에서 문제 있는 플러그인을 미리 파악할 수 있습니다. - 호환성 확인 후
pnpm add -D vite@latest로 Vite 8로 업그레이드합니다. import 경로를 다시vite로 되돌리면 됩니다. 기존 30초 빌드가 5초 이하로 줄어드는 체감을 바로 확인할 수 있습니다. - 새로 작성하는 플러그인에는 훅 필터 패턴을 처음부터 적용합니다. 기존
if (!id.endsWith('.vue')) return null패턴을filter: { id: /\.vue$/ }로 바꾸는 것만으로 Rolldown, Rollup 4.38+, Vite 6.3+ 모두에서 성능 최적화가 자동으로 적용됩니다.
참고 자료
플러그인 API 전체 목록이 필요하면 첫 번째 링크, 훅 필터 상세 스펙은 두 번째, 마이그레이션 전 호환성 현황 확인은 열두 번째 링크(GitHub 이슈 #819)가 가장 실용적입니다.
- Plugin API | Rolldown
- Plugin Hook Filters | Rolldown
- withFilter 함수 | Rolldown
- Why Plugin Hook Filters? | Rolldown
- Announcing Rolldown 1.0 | VoidZero
- Announcing Rolldown 1.0 RC | VoidZero
- Vite 8.0 is out! | Vite
- Vite 8 Beta: The Rolldown-powered Vite | Vite
- Rolldown Integration | Vite 7
- Migration from v7 | Vite
- Vite 8 Rolldown Migration Guide | byteiota
- Rollup Plugin Compat Status (Tracking Issue) | GitHub
- VoidZero's Rolldown Library | InfoQ
- unplugin - Unified plugin system | GitHub
- tsdown - The elegant bundler for libraries | GitHub
- Rolldown GitHub Repository