모듈은 어떻게 번들이 되나 #3 — 왜 번들러가 이렇게 많은가
webpack·Rollup·esbuild·Vite·Rspack·Turbopack·Rolldown — 왜 이렇게 많을까. 이름을 외우는 대신 각 도구가 어떤 문제를 풀려고 태어났는지 계보를 따라가면 지형도가 저절로 그려진다. Browserify부터 Rust 세대까지, 그리고 2026년의 수렴 지점.
webpack, Rollup, esbuild, Vite, Rspack, Turbopack, Rolldown. 왜 이렇게 많고, 뭘 골라야 할까. 이름을 외우는 대신, 각자가 어떤 문제를 풀려고 태어났는지를 따라가면 하나의 이야기가 된다.
1. 번들러는 정확히 무슨 일을 하나
먼저 이 질문부터 정리해야 지형이 보인다. 번들러가 하는 일은 사실 두 가지다.
- 변환(transform) — 브라우저가 모르는 걸 아는 것으로 바꾼다. TypeScript → JS, JSX → JS, 최신 문법 → 구형 문법.
- 번들(bundle) — 흩어진 모듈 수백 개를 의존성 그래프로 엮어 몇 개 파일로 합친다. 이 과정에 tree-shaking(1·2편), 코드 분할, 압축이 붙는다.
지금 쏟아지는 도구들의 차이는 대부분 “이 둘 중 무엇을, 무슨 언어로, 언제(dev냐 prod냐) 하느냐” 의 조합이다. 이 프레임만 손에 쥐면 나머지는 계보로 풀린다.
애초에 왜 필요했나. 브라우저는 오랫동안 모듈을 몰랐다(1편 주제). 그런데 npm 생태계는 CommonJS(require)로 폭발했고, 브라우저는 require를 실행할 수 없었다. 모듈마다 <script> 태그를 다는 방식은 수백 개 단위에서 관리 불가능한 데다 HTTP 요청 폭포를 만들었다. “npm 코드를 브라우저에서 돌리자” 가 번들러의 출발점이다.
2. 계보 — 세대마다 푼 문제
① Browserify (2013) — “npm 코드를 브라우저로”
- 문제: CommonJS
require()가 브라우저에서 안 돎. - 해법: entry 파일에서
require그래프를 순회해 하나의.js로 합침. - 한계: JS/CJS만 다룸. CSS·이미지 같은 자산은 관심 밖.
② webpack (2015) — “모든 것을 하나의 그래프로”
- 문제: 앱이 커지며 JS뿐 아니라 CSS·이미지·폰트도 의존성으로 다뤄야 했고, 코드 분할과 HMR(수정 즉시 반영)이 필요해졌다.
- 해법:
loader(변환) +plugin(확장) + 모든 자산의 의존성 그래프 + code splitting + HMR. React 생태계가 채택하며 표준이 됐다. - 공격 대상: “앱 전체를 하나의 그래프로 관리.”
- 한계: 설정이 복잡하고, JS로 작성돼 대형 앱에서 느리다(cold start·HMR 지연). 이 느림이 다음 세대 전부의 출발점이 된다.
③ Rollup (2014~) — “라이브러리를 위한 깔끔한 번들”
webpack과 같은 시기, 다른 문제를 공격했다.
- 문제(Rich Harris): 기존 번들러가 모듈마다 wrapper와 module loader boilerplate를 씌워 번들을 부풀리고, 모바일 로딩이 느렸다.
- 해법: ESM 기반 tree-shaking(“죽은 코드를 빼는” 게 아니라 “살아있는 코드만 포함”이라는 관점의 전환) + flat output(사람이 손으로 쓴 것처럼 wrapper 없는 결과물). ESM의 정적 성질이 있어야 가능한 기법이라 Rollup이 ESM을 밀었다.
- 공격 대상: webpack이 앱에 강하다면, Rollup은 라이브러리에 강하다.
- 지금도 유효: 라이브러리 저자의 표준. 정밀한 tree-shaking, 다중 포맷 출력, 읽기 쉬운 산출물.
④ esbuild (2020) — “번들러를 JS로 짠 게 문제다”
- 문제: webpack·Rollup이 느린 근본 원인은 번들러 자체가 JavaScript로 작성됐다는 것. 명령줄 도구는 JIT 언어에 최악의 무대다 — Node가 번들러 코드를 파싱하는 동안에도 시간이 샌다.
- 해법: Go로 작성 → 네이티브 병렬 실행 + 스레드 간 공유 메모리 + single-pass(파싱·변환·최적화를 한 번에). 기존 대비 10~100배.
- 공격 대상: “번들링을 JS 런타임 위에서 하는 것 자체가 병목.”
- 한계: 고급 코드 분할·플러그인 생태계 성숙도가 낮다. 그래서 esbuild는 최종 번들러라기보다 “변환 엔진·하부 도구”로 자리잡았다 — 다른 도구 안에서 돌아간다.
여기까지가 “번들러란 무엇인가”의 1막이다. 2막은 dev와 prod를 분리해서 생각한 Vite에서 시작한다.
3. Vite — dev와 prod를 아예 다르게 대하다
Vite는 번들러라기보다 “dev를 어떻게 빠르게 할까”에 대한 답이다.
- 문제: webpack류는 dev 서버를 켜기 전에 앱 전체를 미리 번들해야 첫 화면이 뜬다 → 앱이 클수록 시작이 느려진다.
- Vite의 통찰:
- dev에선 번들하지 않는다. 브라우저 native ESM으로 지금 이 페이지에 필요한 모듈만 on-demand로 서빙. 앱 크기와 무관하게 시작이 즉각적이다.
- 단,
node_modules의존성은 esbuild로 한 번 pre-bundle해서 캐시한다(수백 개 요청을 줄이고 CJS를 ESM으로 변환하려고). 이 캐시가node_modules/.vite/이고, 낡으면 그 유명한does not provide an export유령 에러가 난다. - prod에선 여전히 번들한다. 번들 안 한 ESM을 그대로 배포하면 nested import가 네트워크 요청 폭포를 만들기 때문이다.
- 그래서 두 엔진: dev = esbuild(빠른 변환), prod = Rollup(성숙한 tree-shaking·코드 분할·플러그인 생태계). Vite가 성공한 큰 이유가 Rollup의 플러그인 API를 그대로 채택한 것이다.
- 대가 = 2편에서 본 그 간극: dev와 prod가 다른 엔진 → 두 플러그인 시스템 → 둘을 동기화하는 glue code → “dev에선 되는데 prod에선 깨짐”. 대표적으로 CommonJS default import가 dev(esbuild)에선 잘 되다가 prod(Rollup) 번들에선
undefined로 나오던 것 — 두 도구의 interop 처리가 달라서 생긴 일이다.
4. Rust 세대 (2023~) — 속도를 다시, 그리고 “옮기는 비용”을 공격
esbuild가 “네이티브로 짜면 빠르다”를 증명하자, 다음 세대는 각자 다른 제약을 안고 Rust로 몰려왔다.
Rspack — “webpack을 버리지 않고 빠르게”
- 문제: webpack은 느린데, 대규모 조직은 이미 webpack config·plugin·Module Federation에 깊이 묶여 있어 Vite로 갈아탈 수 없다.
- 해법: webpack의 API를 Rust로 재구현 = drop-in 교체. 기존 설정을 약 95% 유지하고 로더·플러그인 대부분이 그대로 동작한다. 5~10배 빨라진다. 마이그레이션이 “며칠”이 아니라 “몇 시간.”
- 공격 대상: 성능이 아니라 마이그레이션 비용.
Turbopack — Next.js 전용
- 만든 사람이 webpack 창시자 본인이다.
- 해법: Rust 기반, Next.js에 깊이 통합, HMR이 초고속(수십 ms).
- 한계 두 가지: (1) standalone 번들러가 아니다 — Next.js 밖에선 못 쓴다. (2) prod 빌드가 아직 experimental — dev 서버만 안정적이고, prod는 여전히 webpack으로 fallback하는 경우가 많다.
Rolldown + Vite 8 — Vite의 두 엔진을 하나로
- 문제: §3에서 본 Vite의 dev(esbuild) ≠ prod(Rollup) 간극이 5년짜리 버그의 원천이었다.
- 해법: Rolldown(Rust, Rollup 호환 API) 단일 엔진이 dev와 prod를 모두 담당하고, Oxc(Rust 파서/트랜스포머)가 파싱·변환을 맡는다. 개발과 빌드가 같은 엔진을 쓰니 “dev OK, prod 깨짐” 부류가 구조적으로 사라진다.
- 숫자(2026-03-12 Vite 8 stable): Rollup 대비 빌드 10~30배, Linear는 46s→6s, Ramp는 57% 감소를 보고했다.
곁가지지만 중요한 구분: 변환 엔진도 세대교체 중이다. Babel(JS) → SWC(Rust) → esbuild(Go) → Oxc(Rust, Vite 8이 채택). 번들러 지형과 나란히 “트랜스파일러 지형”이 따로 있고, 최신 번들러는 대부분 이 네이티브 트랜스포머를 안에 품고 있다.
5. 2026년 지형도 한 장
성능 전쟁은 사실상 끝났다 — “이제 다 충분히 빠르고, 선택은 벤치마크가 아니라 네 출발점이 결정한다”가 업계 합의다. cold start가 5초를 넘지 않으면 굳이 옮길 이유도 없다는 말까지 나온다.
한 축으로 요약하면: 앱이냐 라이브러리냐(Vite/Rspack vs Rollup), 신규냐 마이그레이션이냐(Vite vs Rspack), 최종 번들러냐 하부 변환 엔진이냐(Rolldown/Rollup vs esbuild/Oxc).
6. 이 지형이 실무에서 부딪히는 곳
지형도는 추상적이지 않다. 규모 있는 모노레포에서 곧장 세 가지로 나타난다.
한 저장소에 번들러가 둘 이상 공존한다. 앱마다 사정이 달라 어떤 앱은 Vite로, 어떤 앱은 Rspack(webpack 계열)으로 빌드하는 일이 흔하다. 문제는 여러 앱에 인라인되는 공용 라이브러리다. 이 코드는 두 빌드 도구 양쪽 모두와 호환돼야 한다. 예를 들어 worker나 정적 자산의 경로(base path)를 알아내려 할 때 —
// webpack/Rspack 계열에서만 정의되는 전역
const base = typeof __webpack_public_path__ !== 'undefined'
? __webpack_public_path__
: '/';
이 한 줄은 Rspack 빌드에선 잘 돌지만 Vite 빌드에선 __webpack_public_path__가 없어 항상 '/'로 fallback한다. 그 결과 base가 /가 아닌 앱에서 worker URL이 어긋나 404 → worker가 조용히 죽고 → 화면은 무한 로딩. 해법은 양쪽 규약을 모두 만족시키는 것이다.
const base =
// Vite 표준
(typeof import.meta !== 'undefined' && import.meta.env?.BASE_URL) ||
// webpack/Rspack 전역
(typeof __webpack_public_path__ !== 'undefined' ? __webpack_public_path__ : '/');
§1~2에서 본 “webpack 전역 vs Vite의 import.meta.env 규약 차이”가 그대로 버그가 된 사례다.
dev 서버는 prod와 다른 물건이다(§3). webpack 계열 dev 서버의 error overlay는 빌드 자체는 통과하는 무관한 타입 경고에도 화면 전체를 덮는 iframe을 띄운다. 이게 자동화 테스트(E2E)의 클릭을 가로채 엉뚱한 실패를 만든다 — prod 번들엔 아예 없는 코드가.
pre-bundle 캐시가 낡는다(§3). Vite의 node_modules/.vite 캐시가 의존성 구조 변경 후에도 갱신되지 않으면 유령 에러가 난다. --force나 캐시 삭제로 푼다.
세 증상 모두 “번들러가 뭘 하는가”가 아니라 “어떤 번들러가, dev냐 prod냐에서, 어떤 규약으로 하는가” 를 알아야 풀린다.
정리
한 문장으로 줄이면 이렇다.
각 번들러는 “느리다”는 같은 문제를 저마다 다른 제약(라이브러리냐 앱이냐, 새로 짜냐 옮기냐, JS냐 네이티브냐) 아래에서 풀었고, 2026년의 종착점은 “dev와 prod를 같은 네이티브 엔진 하나로”(Vite 8 / Rolldown)로 수렴 중이다.
- 번들러는 변환과 번들, 두 일을 한다. 도구의 차이는 무엇을·무슨 언어로·언제 하느냐의 조합이다.
- 계보: Browserify(npm을 브라우저로) → webpack(모든 자산을 그래프로) / Rollup(라이브러리·tree-shaking) → esbuild(네이티브로 빠르게) → Vite(dev는 번들 안 함) → Rust 세대.
- Vite의 dev≠prod는 두 엔진 탓이었고, Rolldown 통합이 그 간극을 없앤다.
- 도구 선택은 벤치마크가 아니라 출발점이 정한다 — 앱이냐 라이브러리냐, 신규냐 마이그레이션이냐.
- 한 저장소에 번들러가 둘이면, 공용 코드는 양쪽 규약을 모두 만족해야 한다.
참고
- Why Vite — 공식 문서
- Vite 8.0 is out! — 공식 발표
- InfoQ — Vite v8: Unified Rust-Based Bundler
- Rich Harris — Tree-shaking versus dead code elimination
- esbuild Architecture: How is it So Fast? — Feature-Sliced Design
- esbuild FAQ — Why is esbuild fast
- 8th Light — A History of JavaScript Modules and Bundling
- JavaScript Bundlers in 2026 — DEV
- Turbopack in 2026 — Pockit