Rolldown codeSplitting과 Module Federation 2.0으로 마이크로 프론트엔드 LCP 41% 줄이기
마이크로 프론트엔드를 도입하고 나서 한동안은 뿌듯했습니다. 팀별 독립 배포가 되고, 서로의 코드를 건드리지 않아도 되니까요. 그런데 어느 순간 빌드 시간이 슬금슬금 길어지고, 번들 사이즈가 예상보다 훨씬 커져 있는 걸 발견했습니다. 특히 청크가 수백 개로 쪼개져서 네트워크 요청이 폭발하는 상황은 — 솔직히 마이크로 프론트엔드를 도입하기 전보다 LCP가 나빠진 아이러니였습니다.
이 글은 그 문제를 파고들다 마주친 두 가지 기술을 정리한 내용입니다. Rolldown의 codeSplitting(구 advancedChunks)과 Module Federation 2.0을 조합하면 마이크로 프론트엔드 빌드 파이프라인의 청크 구조를 세밀하게 제어할 수 있고, Framer는 이 조합으로 LCP를 41% 단축했습니다. 제가 직접 프로덕션에 풀 배포한 경험은 아니고, 그 사례를 분석하고 로컬에서 직접 실험해 본 결과물입니다. Vite와 번들러를 다뤄본 적 있는 프론트엔드 개발자라면 그대로 따라 해볼 수 있을 겁니다.
핵심 개념
Rolldown codeSplitting — 청크를 직접 설계하는 법
Rolldown은 Rust로 만든 JavaScript 번들러로, Rollup의 플러그인 API와 호환되면서 빌드 속도를 크게 끌어올리는 게 목표입니다. 공식 벤치마크 기준으로 Rollup 대비 10~30배 빠르며 esbuild에 준하는 속도를 보여줍니다. Vite 8 Beta가 이미 Rolldown을 기본 번들러로 채택했고, 정식 출시가 되면 Vite 생태계 전체가 사실상 Rolldown 기반으로 전환됩니다.
그런데 번들러가 빠른 것과, 번들을 잘 나누는 것은 다른 문제입니다. 기본 코드 스플리팅 알고리즘은 동적 임포트(import()) 경계를 기준으로 청크를 분리하는데, 이게 때로는 너무 많은 청크를 만들거나, 여러 엔트리가 공유하는 코드를 각 청크에 중복으로 포함시킵니다. 저도 처음엔 "그냥 vendor 분리만 하면 되는 거 아닌가?" 싶었는데, 빌드 결과물을 visualizer로 뜯어보니 컴포넌트 하나가 세 개의 청크에 그대로 복사돼 있더군요.
codeSplitting은 이 자동 알고리즘을 보완하는 수동 청킹 설정입니다. Rolldown 1.0 RC 이전에는 output.advancedChunks라는 이름이었는데, RC부터 output.codeSplitting으로 통합되었습니다. 설정 객체 구조는 동일해서 이름만 바꾸면 마이그레이션이 끝납니다.
minShareCount: 특정 모듈이 몇 개 이상의 엔트리 포인트에서 사용될 때 별도 청크로 분리할지를 결정합니다.minShareCount: 2로 설정하면 두 페이지 이상에서 쓰는 공통 컴포넌트가 자동으로 분리되어 중복 로딩을 막을 수 있습니다.
Module Federation 2.0 — 번들러에서 독립한 마이크로 프론트엔드 런타임
Module Federation(이하 MF)은 각각 독립 배포되는 앱들이 서로의 컴포넌트와 라이브러리를 런타임에 공유하는 아키텍처 패턴입니다. 1.0은 Webpack 전용이었지만, 2.0은 런타임을 빌드 도구로부터 완전히 분리했습니다.
Host App ←──── remoteEntry.js ──── Remote App A
│ (Vite + Rolldown)
└──── remoteEntry.js ──── Remote App B
(Rspack — Rust 기반 Webpack 호환 번들러)이 구조 덕분에 Host는 Vite 8(Rolldown 기반), Remote A는 Rspack(Webpack 설정을 거의 그대로 쓸 수 있는 Rust 기반 번들러), Remote B는 Webpack을 쓰더라도 @module-federation/core 런타임이 모듈 로딩을 통일된 방식으로 처리합니다. 레거시 팀이 번들러를 당장 바꾸지 않아도 신규 팀과 모듈을 공유할 수 있다는 게 실무에서 특히 유용한 지점입니다.
MF 용어로 Remote는 컴포넌트를 노출하는 앱이고, Host는 그 컴포넌트를 소비하는 앱입니다. 하나의 앱이 동시에 Host이면서 Remote가 될 수도 있습니다. 2026년 4월 Stable로 선언된 2.0은 ByteDance 내부 인프라 경험과 MF 원저자 협력을 바탕으로 프로덕션에서 충분히 검증된 상태입니다.
두 기술을 조합하면
codeSplitting과 MF 2.0을 함께 쓰면 이런 파이프라인이 완성됩니다.
[Host: Vite 8 + Rolldown]
└── @module-federation/vite
└── 런타임에 Remote 로드
[Remote: Vite 8 + Rolldown]
└── @module-federation/vite
└── remoteEntry.js 노출
└── output.codeSplitting으로 청크 최적화MF 2.0이 독립 배포 가능한 Remote 구조를 잡아주고, Rolldown의 codeSplitting이 각 Remote 내부 번들 경계를 세밀하게 제어하는 역할을 맡습니다.
실전 적용
예시 1: Framer가 LCP를 41% 줄인 청킹 전략
Framer는 esbuild를 쓰다가 청크 수가 너무 많아져 LCP가 크게 나빠지는 문제에 부딪혔습니다. VoidZero와 협력해 Rolldown의 advancedChunks(당시 명칭)를 도입한 결과, 청크 수 67% 감소, 대형 사이트 LCP 41% 단축이라는 수치를 얻었습니다. 현재 200,000개 이상의 Framer 페이지가 이 방식으로 빌드됩니다.
핵심은 minShareCount였습니다. 여러 엔트리가 공유하는 컴포넌트가 각 청크에 중복으로 포함되던 게 문제였는데, 이 옵션 하나로 공통 코드를 별도 청크로 뽑아낼 수 있었습니다.
// vite.config.ts — Rolldown 1.0 RC+ 기준 (RC 이전은 advancedChunks였던 옵션)
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
codeSplitting: {
groups: [
{
name: 'vendor-react',
test: /node_modules\/(react|react-dom)/,
priority: 20,
},
{
name: 'shared-components',
minShareCount: 2, // 2개 이상 엔트리가 공유하면 별도 청크
minSize: 20_000,
maxSize: 150_000,
priority: 10,
},
],
},
},
},
},
});빌드 후 dist/ 폴더를 확인하면 이런 구조가 나옵니다.
dist/
├── assets/
│ ├── vendor-react-[hash].js ← react/react-dom 전용 청크
│ ├── shared-components-[hash].js ← 공통 컴포넌트 청크
│ └── index-[hash].js
└── index.html| 옵션 | 역할 |
|---|---|
name |
생성될 청크 파일의 이름 prefix |
test |
이 그룹에 포함할 모듈을 정규식으로 매칭 |
priority |
여러 그룹에 매칭될 때 우선순위 (높을수록 먼저 적용) |
minShareCount |
몇 개 이상의 엔트리가 공유할 때 분리할지 기준 |
minSize / maxSize |
청크 크기 범위 제한 (bytes 단위) |
priority를 빠뜨리면 여러 그룹 규칙이 동일한 모듈에 겹쳐 매칭될 때 어떤 그룹으로 들어갈지 예측하기 어렵습니다. react 관련 모듈에 priority: 20을 명시해두면 디버깅이 훨씬 수월해집니다.
예시 2: MF 2.0 + Rolldown으로 Remote 앱 구성하기
실제 마이크로 프론트엔드 환경에서 Remote 앱에 @module-federation/vite(2.x 버전)를 붙이고, codeSplitting으로 vendor 청크를 최적화하는 패턴입니다.
처음 보면 shared에 왜 singleton: true를 넣는지 의아할 수 있는데, React는 여러 인스턴스가 공존하면 Hook 규칙 위반 에러가 나기 때문에 반드시 하나의 인스턴스만 살아있어야 합니다.
// remote 앱 vite.config.ts
import { defineConfig } from 'vite';
import { federation } from '@module-federation/vite';
export default defineConfig({
server: { port: 5001 },
plugins: [
federation({
name: 'order-remote',
filename: 'remoteEntry.js',
exposes: {
'./OrderList': './src/components/OrderList.tsx',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
build: {
rollupOptions: {
output: {
codeSplitting: {
groups: [
{
name: 'vendor',
test: /node_modules/,
minSize: 100_000,
maxSize: 250_000,
priority: 10,
},
],
},
},
},
},
});// host 앱 vite.config.ts
import { defineConfig } from 'vite';
import { federation } from '@module-federation/vite';
export default defineConfig({
server: { port: 5000 },
plugins: [
federation({
name: 'shell',
remotes: {
// 로컬 개발: http://localhost:5001/remoteEntry.js
// 프로덕션: https://cdn.example.com/order/remoteEntry.js
orderRemote: 'order-remote@http://localhost:5001/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
});Host는 [name]@[remoteEntry URL] 형태로 Remote를 등록합니다. 로컬 개발 시에는 http://localhost:5001/remoteEntry.js로, 프로덕션에서는 CDN URL로 교체하는 방식을 환경 변수로 분기해두는 게 나중에 편해집니다.
예시 3: 크로스 번들러 시나리오 — 레거시 팀과의 공존
실무에서 자주 맞닥뜨리는 상황인데, 레거시 팀은 Rspack/Rsbuild를 쓰고 신규 팀은 Vite/Rolldown을 쓰는 경우입니다. MF 2.0 런타임 덕분에 번들러가 달라도 Remote 모듈을 서로 소비할 수 있습니다.
// 레거시 팀 — rsbuild.config.ts
import { defineConfig } from '@rsbuild/core';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
export default defineConfig({
plugins: [
pluginModuleFederation({
name: 'legacy-remote',
exposes: {
'./LegacyHeader': './src/LegacyHeader',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
});// 신규 팀 Host — vite.config.ts (Rolldown 기반)
import { defineConfig } from 'vite';
import { federation } from '@module-federation/vite';
export default defineConfig({
plugins: [
federation({
name: 'new-shell',
remotes: {
legacyRemote: 'legacy-remote@https://cdn.example.com/legacy/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
});두 팀이 서로 다른 번들러를 쓰지만, @module-federation/core 런타임이 공통 레이어 역할을 하므로 remoteEntry.js URL만 알면 소비가 가능합니다. Zephyr Cloud 같은 플랫폼을 활용하면 이 혼합 아키텍처의 배포·버전 오케스트레이션까지 자동화할 수 있습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 빌드 성능 | Rolldown은 Rollup 대비 10~30배 빠르고, esbuild에 근접한 속도를 제공합니다 |
| 세밀한 청크 제어 | codeSplitting 그룹 규칙으로 도메인별 번들 크기와 로딩 순서를 직접 최적화할 수 있습니다 |
| 빌드 도구 독립성 | MF 2.0 런타임이 번들러에서 분리되어 Webpack·Rspack·Vite·Rolldown을 혼용할 수 있습니다 |
| TypeScript 타입 자동 공유 | MF 2.0이 Remote 모듈 타입을 개발 중 자동 생성·로드해 npm link 없이도 타입 안전성을 유지합니다 |
| 독립 배포 | 팀별 독립 릴리스 사이클을 유지하면서 공유 모듈을 런타임에 동기화할 수 있습니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 복잡성 세금 | 독립 레포·CI 파이프라인·버전 관리가 모노레포 대비 훨씬 복잡해집니다 | 소규모 팀이라면 모노레포 + MF를 먼저 검토해보는 게 나을 수 있습니다 |
| 기본값의 미흡함 | 기본 청킹 전략만 쓰면 수천 개의 소형 청크가 생성될 수 있습니다 | groups 설정으로 vendor, shared 청크를 명시적으로 정의해두는 게 낫습니다 |
| Rolldown RC 단계 | Rolldown 1.0 RC는 프로덕션 사용이 가능하지만 일부 엣지케이스가 존재할 수 있습니다 | Framer처럼 충분히 테스트 후 점진적으로 적용해볼 수 있습니다 |
| 런타임 오버헤드 | MF 런타임의 추가 네트워크 요청과 런타임 재조합이 초기 로드 시간에 영향을 줍니다 | preload 힌트나 CDN 캐싱 전략으로 완화할 수 있습니다 |
실무에서 가장 흔한 실수
-
shared에singleton: true없이 React를 등록하는 경우 — Host와 Remote가 각각 다른 React 인스턴스를 갖게 되어 Hook 규칙 위반 에러가 발생합니다.react와react-dom은 반드시singleton: true로 선언해두는 게 좋습니다. -
codeSplitting그룹에priority를 빠뜨리는 경우 — 여러 그룹 규칙이 동일한 모듈에 겹쳐 매칭될 때 결과를 예측하기 어렵습니다. 그룹마다 명시적인priority값을 지정해두면 디버깅이 훨씬 수월해집니다. -
Remote URL을 하드코딩으로 관리하는 경우 — Remote가 많아질수록 URL 관리가 복잡해집니다. 환경 변수나 Zephyr Cloud 같은 오케스트레이션 레이어를 통해 URL을 동적으로 주입하는 구조를 처음부터 잡아두면 나중에 훨씬 편해집니다.
마치며
Rolldown과 MF 2.0을 조합하기 전에는 "번들러를 바꾼다고 얼마나 달라지겠어?"라는 생각이 있었습니다. 그런데 Framer 사례를 뜯어보면서 깨달은 건, 번들러의 속도보다 청크 구조를 얼마나 정밀하게 제어할 수 있느냐가 LCP에 더 큰 영향을 미친다는 점이었습니다. codeSplitting은 그 제어를 가능하게 해주는 도구이고, MF 2.0은 팀 간 번들러가 달라도 그 구조를 흔들지 않게 해주는 런타임입니다.
지금 바로 시작해볼 수 있는 3단계:
-
vite.config.ts의build.rollupOptions.output에codeSplitting.groups를 추가하고pnpm build를 실행해볼 수 있습니다.rollup-plugin-visualizer로 청크 구조를 시각화하면minShareCount: 2하나만 추가해도 중복 청크가 눈에 띄게 줄어드는 걸 확인할 수 있습니다. -
pnpm add @module-federation/vite로 플러그인을 설치하고, 로컬에서 Remote 앱(port 5001)과 Host 앱(port 5000)을 각각 띄워 Remote 컴포넌트를 Host에서 렌더링하는 최소 예제를 만들어볼 수 있습니다.@module-federation/vite공식 예제 레포에 스타터가 준비되어 있습니다. -
Remote 앱에
federation()플러그인과codeSplitting.groups를 함께 적용해보면, vendor 청크가 CDN 캐시에 남아 있는 동안 Remote 업데이트 시 재다운로드 없이 변경분만 갱신됩니다. Chrome DevTools 네트워크 탭에서 바로 확인해볼 수 있습니다.
참고 자료
- Rolldown 공식 문서 - advancedChunks
- Rolldown 공식 문서 - codeSplitting
- Rolldown 공식 문서 - Manual Code Splitting
- Announcing Rolldown 1.0 RC | VoidZero
- How Framer reduced LCP using Rolldown | VoidZero Case Study
- Bundling at Framer — Rolldown for faster sites | Framer Blog
- Module Federation 2.0 Stable Release | InfoQ
- MF 2.0 Stable Release 공식 블로그 | module-federation.io
- Module Federation 2.0 공식 릴리스 토론 | GitHub
- @module-federation/vite | npm
- Module Federation in Rolldown — What It Means for Vite? | Medium
- advancedChunks API 설계 토론 | GitHub Discussion #2118
- Module Federation | Rspack 공식 문서
- Vite + Webpack + Rspack with Module Federation | Zephyr Cloud Docs
- Code Splitting Algorithms | DeepWiki