Rolldown의 코드 스플리팅 — 비트셋 한 줄로 모듈의 소속을 결정하는 법

11 min read
RolldownVite번들러코드 스플리팅Rust
Rolldown의 코드 스플리팅 — 비트셋 한 줄로 모듈의 소속을 결정하는 법

Vite는 개발 서버에서는 esbuild를, 프로덕션 빌드에서는 Rollup을 사용해왔다. 도구가 둘로 나뉘어 있으니 개발 환경과 프로덕션 환경의 동작이 미묘하게 다른 문제가 있었다. Rolldown은 이 두 도구를 하나로 통합하기 위해 Rust로 작성된 차세대 번들러다. 현재 Vite 8 베타에 통합되어 있으며, Rollup과 API 호환을 유지하면서 성능은 esbuild 수준으로 끌어올리는 것이 목표다. 이 글에서는 Rolldown의 핵심 알고리즘인 비트셋 기반 코드 스플리팅의 동작 원리를 살펴본다.

[💡 잠깐! 이 용어는?] 코드 스플리팅(Code Splitting): 하나의 거대한 번들 파일을 여러 개의 작은 청크로 나누는 기법. 사용자가 현재 필요한 코드만 로드하여 초기 로딩 속도를 개선한다.


비트셋으로 모듈의 "지문"을 찍는다

Rolldown의 코드 스플리팅 알고리즘의 핵심은 **비트셋(BitSet)**이다. 엔트리 포인트가 3개인 프로젝트를 생각해보자.

엔트리비트 위치
pages/home.js0번째 비트
pages/about.js1번째 비트
pages/contact.js2번째 비트

모듈 utils/format.jshome.jscontact.js에서 import된다면, 이 모듈의 비트셋은 101이 된다. 0번째(home)와 2번째(contact) 비트가 켜져 있다는 뜻이다. 이 비트셋이 곧 모듈의 **"도달 가능성 지문(reachability fingerprint)"**이 된다.

비트셋 기반 모듈 분류 예시
modules/auth.js      → 111 (모든 엔트리에서 사용)  → 공유 청크
modules/format.js    → 101 (home + contact)        → 공유 청크
modules/animation.js → 100 (home에서만 사용)       → home 청크
modules/map.js       → 001 (contact에서만 사용)    → contact 청크

비유하면 학교에서 학생들이 소속 동아리를 팔찌 색으로 표시하는 것과 같다. 빨간 팔찌(비트 0)와 파란 팔찌(비트 2)를 동시에 차고 있으면 두 동아리에 모두 소속된 것이다. 같은 팔찌 조합을 가진 학생들은 같은 반에 배치한다 — 이것이 Rolldown의 청크 할당 로직이다.


4단계 스플리팅 프로세스

Phase 1: BFS로 도달 가능성 탐색

각 엔트리 포인트에서 출발하여 **너비 우선 탐색(BFS)**으로 모듈 그래프를 순회한다. 방문하는 모듈마다 해당 엔트리의 비트를 켠다.

Phase 1 — BFS 도달 가능성 마킹
Entry: home.js (비트 0)
  → import utils/format.js     → format.bits |= 001 → 001
  → import modules/auth.js     → auth.bits   |= 001 → 001
  → import modules/animation.js → anim.bits  |= 001 → 001
 
Entry: about.js (비트 1)
  → import modules/auth.js     → auth.bits   |= 010 → 011
 
Entry: contact.js (비트 2)
  → import utils/format.js     → format.bits |= 100 → 101
  → import modules/auth.js     → auth.bits   |= 100 → 111
  → import modules/map.js      → map.bits    |= 100 → 100

탐색이 끝나면 모든 모듈에 비트셋이 할당된다. auth.js111(모든 엔트리에서 도달 가능), format.js101(home과 contact에서 도달 가능)이다.

[💡 잠깐! 이 용어는?] 비트 연산 OR (|=): 기존 비트셋에 새 비트를 추가하는 연산. 001 |= 100101이 된다. 기존에 켜진 비트는 유지하면서 새 비트만 추가하므로, 여러 엔트리에서의 도달 가능성을 누적할 때 사용한다.

Phase 2: 수동 스플리팅 (개발자 의도 반영)

개발자가 설정 파일에서 커스텀 청크 그룹을 지정할 수 있다. Rolldown은 이 설정을 최우선으로 처리한다.

rolldown.config.js — 수동 청크 그룹 설정
export default {
  output: {
    manualChunks: {
      vendor: ['react', 'react-dom'],
      icons: [/\.svg$/]
    }
  }
}

reactreact-dom은 비트셋과 무관하게 항상 vendor 청크에 들어간다. 알고리즘이 아무리 똑똑해도 "이건 이쪽에 넣어라"라는 개발자의 의도가 우선한다. 비유하면 자동 좌석 배치 시스템이 있더라도 "이 손님은 VIP석으로"라는 예약 지정이 먼저 적용되는 것과 같다.

Phase 3: 자동 할당 (비트셋 매칭)

수동 할당이 끝나면 나머지 모듈을 비트셋 기준으로 분류한다. 비트셋이 동일한 모듈들은 같은 청크에 들어간다.

비트셋의미청크
100home에서만 사용home 전용 청크
010about에서만 사용about 전용 청크
001contact에서만 사용contact 전용 청크
101home + contact 공유shared-101 청크
111모두 공유shared-111 청크 (common)

비트셋 비교는 정수 비교와 동일하므로 O(1) 시간에 수행된다. 모듈 수가 수천 개라도 분류 자체는 매우 빠르다.

Phase 4: 구조 최적화

마지막 단계에서는 불필요한 청크를 정리한다.

  • 퍼사드 청크 제거: 자체 코드 없이 다른 청크를 re-export만 하는 청크를 병합한다. HTTP 요청 수를 줄이기 위함이다
  • 소형 청크 병합: 너무 작은 청크는 가까운 청크에 합친다. 파일 수를 줄여 네트워크 오버헤드를 최소화한다
  • 네임스페이스 통합: 같은 청크 내에서 동일한 외부 모듈(예: React)을 여러 번 import하면 하나로 합친다

Rollup, esbuild와 뭐가 다른가

항목RollupesbuildRolldown
언어JavaScriptGoRust
코드 스플리팅 전략모듈 단위 분석단순 그래프 기반비트셋 기반 O(1) 분류
수동 스플리팅manualChunks 지원제한적Rollup 호환 + 우선순위 시스템
플러그인 호환성Rollup 플러그인 생태계자체 플러그인 APIRollup 플러그인 호환
Vite 통합프로덕션 빌드 담당개발 서버 담당개발 + 프로덕션 모두 담당

Rolldown의 핵심 장점은 Rollup의 유연성과 esbuild의 속도를 동시에 가져가는 것이다. Rust의 메모리 안전성과 병렬 처리 능력이 비트셋 연산과 맞물려 대규모 프로젝트에서도 빠른 빌드를 가능하게 한다.

[💡 잠깐! 이 용어는?] 퍼사드 청크(Facade Chunk): 자체 로직 없이 다른 모듈을 re-export만 하는 빈 껍데기 청크. HTTP 요청을 추가로 발생시키는 단점이 있어 Rolldown은 이를 자동으로 감지하여 제거한다.


Preserve 모드 vs Normal 모드

Rolldown은 두 가지 출력 모드를 제공한다.

항목Normal 모드Preserve 모드
용도웹 애플리케이션라이브러리
모듈:청크 관계다대일 (여러 모듈 → 하나의 청크)일대일 (모듈당 하나의 청크)
트리 쉐이킹번들러가 처리사용자(소비자)가 처리
Deep import불가import Button from 'ui/Button' 가능
캐시청크 단위파일 단위 (세분화)

라이브러리를 배포할 때는 Preserve 모드가 유리하다. 소비자의 번들러가 트리 쉐이킹을 할 수 있도록 모듈 경계를 유지해주기 때문이다. 비유하면 Normal 모드는 반찬을 도시락통 하나에 다 담는 것이고, Preserve 모드는 반찬별로 따로 포장해서 원하는 것만 꺼내 먹을 수 있게 하는 것이다.


정리

  • Rolldown은 Vite 8에 통합된 Rust 기반 차세대 번들러로, esbuild와 Rollup을 하나로 대체한다
  • 비트셋 기반 코드 스플리팅으로 모듈의 엔트리 도달 가능성을 O(1)에 판별한다
  • 4단계 프로세스: BFS 탐색 → 수동 할당 → 자동 분류 → 구조 최적화
  • 개발자가 지정한 manualChunks가 알고리즘보다 항상 우선한다
  • Rollup 플러그인과 호환되어 기존 Vite 프로젝트에서 마이그레이션이 용이하다
  • Normal 모드(앱)와 Preserve 모드(라이브러리)로 출력 전략을 선택할 수 있다

Vite를 사용 중이라면 Rolldown은 자연스럽게 만나게 될 도구다. 코드 스플리팅의 결과가 기존과 달라질 수 있으므로, Vite 8 베타에서 빌드 결과물의 청크 구성을 미리 확인해보는 것을 권한다. 비트셋 알고리즘이 내 프로젝트의 모듈을 어떻게 분류하는지 이해하면, manualChunks 설정을 더 정밀하게 제어할 수 있다.


참고:

관심 있을 만한 포스트

Vinext — Vite 위에서 Next.js를 1주일 만에 다시 만든 이야기

Cloudflare가 AI와 함께 단 일주일, $1,100의 API 비용으로 Next.js 호환 프레임워크를 Vite 위에 구축한 과정.

VinextNext.js

Docfind — Rust와 WebAssembly로 만든 서버 없는 브라우저 검색 엔진

Microsoft VS Code 문서 팀이 Rust와 WebAssembly로 구현한 클라이언트 사이드 검색 엔진 Docfind의 내부 설계를 파헤친다.

RustWebAssembly

뱀의 탈피에서 배운 서버 재시작 — Rust로 커넥션 제로 로스를 구현하는 ecdysis

Cloudflare가 5년간 프로덕션에서 검증한 Rust 무중단 재시작 라이브러리 ecdysis를 오픈소스로 공개했다.

RustCloudflare

번들러(Bundle)란 뭐고, 왜 필요할까? — 요즘 번들러/빌드 툴 비교 가이드

번들러의 역할(모듈/의존성/트랜스파일/최적화)을 쉽게 설명하고, Vite·Rollup·esbuild·Webpack·Rspack·Turbopack 같은 도구를 상황별로 비교합니다.

BundlerVite

OOM이 터지고 나서야 깨달은 것들 — Webpack4에서 Vite로 갈아탄 5년 묵은 CMS

CI 빌드가 OOM으로 터진 뒤, 5년 동안 방치된 Webpack4 기반 CMS를 Vite로 전환하며 빌드 시간 48%, 번들 크기 81%를 줄인 과정.

ViteWebpack

AI 코딩의 맹점 — Artifacts 없이 에이전트는 기억을 잃는다

PRD, ADR, TDD가 AI 코딩 워크플로우에서 왜 선택이 아닌 필수인지, 실전 구조와 함께 살펴본다.

AI 코딩Artifacts

Next-Translate 3.0 — Turbopack과 App Router를 위한 i18n 재건

1년간 공백 후 돌아온 Next-Translate 3.0이 Turbopack 지원, 비동기 params, App Router 안정화를 한 번에 처리하는 방법.

Next.jsi18n

V8 WasmGC 투기적 최적화 — 가상 메서드를 인라인으로 만드는 법

V8이 WasmGC의 가상 메서드 디스패치에 투기적 인라이닝을 도입해 Dart와 Java 앱에서 최대 8% 성능을 끌어낸 방법.

V8WebAssembly