Module Federation 버전별 변화(1.0, 1.5, 2.0)

커뮤니티가 이끌어온 5년 동안의 Module Federation 발전
2025년 05월 13일 /
#micro-frontends

웹 개발 생태계에서 Micro Frontend와 Module Federation 이야기를 시작한지도 꽤 시간이 지났다. flex 웹 제품도 전면 Micro Frontend 아키텍처로 이행한지 2년이 넘게 지났다.

그 기간 동안 Module Federation은 커뮤니티 드리븐으로 발전했다. 처음 해당 개념을 접하고 제품에 접목시키려 했을 때는 webpack에 구현된 것 정도만 쓸만했다. 기술사례는 없었다. 컨셉을 빈약하게만 구현해 놓은 구현체들이 많았다. 이제는 webpack을 포함하여 vite, rspack 등 다수의 번들러에서 쓸만한 인터페이스와 꽤 볼만한 설명을 제공하고 있다고 느껴진다. 물론 아직 충분하진 않다.

그 중 Module Federation 발전의 가장 큰 맥이라고 볼 수 있는 1.0, 1.5, 2.0 버전별 변화를 살펴보고자 한다. 버전의 구분은 Module Federation Community 의 기여자들이 제시하는 방식으로 각 번들러에 따라서 구현된 버전과, 같은 버전이라고 해도 구현된 수준이 다를 수 있다.

Module Federation 1.0

Module Federation 1.0은 Webpack 5에 추가되었다. 그것이 2020년 10월이었다.

new ModuleFederationPlugin({ name: 'host', filename: 'remoteEntry.js', remotes: { remote: 'remote@http://localhost:3002/remoteEntry.js', }, exposes: { './App': './src/App.tsx', }, shared: { react: { singleton: true, version: '18.2.0' }, 'react-dom': { singleton: true, version: '18.2.0' }, }, });

핵심 개념들이 정의되었다. Container, Container References, Omnidirectional Host, Shared Dependencies와 같은 개념이 그것이다.

그것과 관련해서는 이전에 블로그 글에 자세하게 작성했었다. 위 단어들이 무엇을 뜻하는지 모른다면 아래 포스트를 읽어야 다음에 나올 내용 이해가 수월하다.

  1. Module Federation의 컨셉과 작동 원리 이해하기: Container, Container References, Omnidirectional Host
  2. Module Federation Shared Dependency 작동 원리와 활용 방법: Shared Dependencies

1.5, 2.0에서도 이 개념들은 모두 유효하고 1.0의 플러그인 인터페이스를 확장하는 방식으로 구현되어 하위호환이 보장된다.

1.0은 Build Config 단에서, 즉 빌드하기 전에, 미리 런타임 통합할 모든 모듈들의 참조 관계를 나타내는 방식으로 앱을 운영하는 방식이 권장된다. "권장"이라는 게 누가 막 이렇게 쓰세요!! 라고 말했던 건 아니다. 런타임에서 동적으로, Build Config 모르게 모듈들을 통합하려면 webpack runtime chunk 사이의 코드를 직접 제어하는 해키한 방식으로만 가능하기 때문이다. 상대적으로 찝찝한 방식이라 권장하기 어렵다고 표현할 수 있다.

webpack의 runtime chunk를 직접 제어하여 모듈을 런타임 통합시킨다는 것은 결국 모든 Conatiner와 Container Reference가 webpack 기반으로 만들어져야 함을 뜻한다. 그래서, 1.0은 webpack에 강하게 결합되어 있다.

이제 1.0은 이터레이션이 없다. 1.5나 2.0을 쓰는 것이 권장된다.

Module Federation 1.5

Module Federation 1.5는 2024년 1월에 릴리즈된 rspack 0.5.0 버전에서부터 rspack에서 내장 plugin으로 지원한다. 해당 플러그인의 옵션을 보면 아래처럼 되어있는데, 1.0에서 확장된 기능이 다음 3가지라는 의미다.

export interface ModuleFederationPluginOptions extends Omit<ModuleFederationPluginV1Options, 'enhanced'> { runtimePlugins?: RuntimePlugins; implementation?: string; shareStrategy?: 'version-first' | 'loaded-first'; }

이 중 두 가지를 살펴볼만 하다. runtimePlugin, shareStrategy

runtimePlugin 은 런타임 모듈에 생명주기에 개입해 제어할 수 있는 인터페이스를 제공한다. 타입이 string인데 아래와 같은 파일을 만들고 경로를 넣으면 된다.

export default function () { return { name: 'logger', beforeInit(args) { console.log('beforeInit: ', args); return args; }, beforeLoadShare(args) { console.log('beforeLoadShare: ', args); return args; }, }; }

지원하는 생명주기 훅들은 눈치상 다음과 같아 보이는데, rspack에서 타입을 제공하고 있지는 않고 다음과 같은 예시 만 제공하고 있다. 어떤 타이밍에 콜백이 호출되는지는 직접 해봐야 알겠다.

1.0으로 대형 앱을 운영하다보면 Remote App 진입점 번들(흔히 remoteEntry.js라 말하는)을 입맛대로 커스텀해야하는 요구가 꽤 있다. 엔트리 번들을 검색하여 소스를 직접 수정하는 등 흑마법에 가까운 webpack plugin을 작성해야 하는데, 이런 기능이 제대로만 동작한다면 도움을 받을 수 있을 것이다.

shareStrategy 에는 공유 의존성에 대한 로딩 전략을 명시할 수 있다. 전략은 버전 우선(version-first), 로드 우선(loaded-first)두 가지이다. 버전 우선이 디폴트 동작이다.

rspack과 webpack으로 만든 Container를 공존시키는 커뮤니티발 예시가 존재한다. Host webpack Remote rspack 1.0 -> 1.5로 갈 때 다수의 마이크로앱들의 점진적 마이그레이션이 가능해 보인다.

Module Federation 2.0

@module-federation/enhanced 패키지의 rspack과 webpack plugin에서 Module Federation 2.0을 지원한다. @module-federation/vite 패키지에서는 vite plugin을 지원하는데, enhanced의 그것보다는 기능 구현이 조금 모자라 보인다. 공식적인 2.0의 발표는 2024년 4월에 있었다.

Federation Runtime 이 고안되었다. 위에서 언급했듯 동적으로 통합하는 방식이 없지는 않았지만, 1.0과 1.5에서는 런타임 통합에 필요한 코드 처리를 번들러의 빌드 시점 처리에 많이 의존했다. 2.0부터는 런타임 구현체를 직접 import해서 사용하는 방식 사용이 가능하다.

// 1.0, 1.5 Host import { lazy, Suspense } from 'react'; const Component = lazy(() => import('remoteA/Component')); const Component2 = lazy(() => import('remoteB/Component')); const Component3 = lazy(() => import('remoteB/Component2')); const App = () => { return ( <Suspense fallback={<div>Loading...</div>}> <Component /> <Component2 /> <Component3 /> </Suspense> ); }; export default App; // 2.0 Host import { lazy, Suspense } from 'react'; import { init, loadRemote } from '@module-federation/enhanced/runtime'; init({ name: 'host', remotes: [ { name: 'remoteA', entry: '/remotes/remoteA/mf-manifest.json', }, { name: 'remoteB', entry: '/remotes/remoteB/mf-manifest.json', }, ], }); const Component = lazy(() => loadRemote<any>('remoteA/Component')); const Component2 = lazy(() => loadRemote<any>('remoteB/Component')); const Component3 = lazy(() => loadRemote<any>('remoteB/Component2')); const App = () => { return ( <Suspense fallback={<div>Loading...</div>}> <Component /> <Component2 /> <Component3 /> </Suspense> ); }; export default App;

해당 기능을 설명하는 커뮤니티 문서에 Federation Runtime의 목적과 트레이드오프에 대한 중요한 설명이 있다. 이같이 런타임을 독립시키는 것은 런타임 통합 도구(위의 init, loadRemote)가 build config에서 자유롭게 하는데에 가치가 있으나, 런타임에서 번들러를 지원하는 구현이 필요하고 유연성은 떨어진다.

각각 다른 번들러를 사용하여 만든 모듈로 마이크로 프론트엔드를 구성하는 예제가 있다. 여기서의 런타임(@module-federation/runtime)은 여러개의 번들러가 제공하는 모듈의 런타임 통합을 지원하는 것처럼 보인다. 1.0에서처럼 webpack의 런타임 청크 구현에 강결합되어 써먹기 어려운 수준보다는 완화되어, 2.0에서는 이제 어떤 번들러를 쓰던 같은 런타임을 쓰는 경우 통합이 가능한 형태를 생각할 수 있게 되었다.

번들러와의 결합이 런타임으로 옮겨간 셈인데 결합 자체는 불가피하다. 다른 프로토콜을 사용하는 두 피어가 네트워크 통신을 할때 게이트웨이 장비가 필요한 것처럼 어디선가에서는 결합되어야 한다.

Manifest Protocol이 도입되었다. 빌드시 mf-minfests.json 을 진입점 모듈과 함께 생성할 수 있다. 위의 2.0 예제에서 remoteA 에 대한 런타임 모듈 등록은 /remotes/remoteA/mf-manifests.jsonmanifest 파일을 가리키거나, 혹은 /remotes/remoteA/remoteEntry.js 로 진입접 번들을 가리키는 2가지 방식을 모두 사용할 수 있다.

2.0으로 직접 앱을 빌드했을 때 생성되는 mf-manifests 파일은 다음과 같은 모습이다.

{ "id": "remoteA", "name": "remoteA", "metaData": { "name": "remoteA", "type": "app", "buildInfo": { "buildVersion": "1.0.0", "buildName": "@shine-muscat-example/remote-1" }, "remoteEntry": { "name": "remoteEntry.js", "path": "", "type": "global" }, "types": { "path": "", "name": "", "zip": "@mf-types.zip", "api": "@mf-types.d.ts" }, "globalName": "remoteA", "pluginVersion": "0.12.0", "prefetchInterface": false, "publicPath": "/remotes/remoteA/" }, "shared": [ { "id": "remoteA:react", "name": "react", "version": "19.1.0", "singleton": true, "requiredVersion": "*", "assets": { "js": { "async": [], "sync": [ "vendors-_yarn_cache_react-npm-19_1_0-9804a7da5b-d018068982_zip_node_modules_react_index_js.js" ] }, "css": { "async": [], "sync": [] } } } ], "remotes": [], "exposes": [ { "id": "remoteA:Component", "name": "Component", "assets": { "js": { "sync": [ "vendors-_yarn_cache_react-npm-19_1_0-9804a7da5b-d018068982_zip_node_modules_react_jsx-dev-run-7cacbe.js", "__federation_expose_Component.js" ], "async": [] }, "css": { "sync": [], "async": [] } }, "path": "./Component" } ] }

메타데이터 중에서는 assets 필드가 상당히 유용해보이는데, remoteEntry.js 만으로 모듈을 런타임 통합했을 때에는 어떤 청크에 어떤 exposed 모듈이 있는지 알기 어렵기 때문에 청크에 대한 통제가 거의 불가능하기 때문이다.

build config 단의 정보를 런타임에서도 쉽게 참조할 수 있게 된 것도 유용해보인다. 기능 소개에서도 언급됐지만 메타 정보를 토대로 좀 더 심화된 기능을 구현할 수 있게 된다. 2.0의 또다른 기능인 실시간 타입 지원도 이 Manifest Protocol에 기반하는데, types 필드에 각 마이크로앱이 빌드시에 만든 d.ts 파일의 경로를 제공한다.

2.0은 전반적으로 상당히 고수준이라는 인상이 있다. 에러 코드까지 정의해놨다. 2.0 예제를 실제로 만들어 보았을 때 아직 문서만 참고해서는 사용이 직관적이지 않은 기능들도 있었다. 써보면서 알거나 코드를 뜯어보았을 때 파악할 수 있는 부분도 있었다.

개인적인 생각

1.0, 1.5, 2.0으로 발전해오며 Module Federation은 아키텍처의 발전과 확장에 블로커가 되는 것들을 많이 치웠다. 번들러나 빌드 설정에 강결합되고 한정되는 구조를 극복하려 했고, 타입을 지원하기 시작했으며, 원격 모듈의 메타데이터를 확보하기도 쉬워졌다.

그러나 이러한 업데이트들은 제품 안정성이 중요하여 Module Federation을 제한된 유스케이스로 활용하는 Micro Frontend 플랫폼이라면 이점을 모두 누리기가 힘들다. 보수적인 플랫폼이라면 번들러는 팀 전체에서 하나만 써야 할 수 있고, 타입 통합도 실시간이 아닌 정적으로 해야할 수 있다. 커뮤니티 드리븐의 데브툴이 아니라 팀의 입맛에 맞는 개발 도구가 필요할 수 있다.

그래서 결국, 현재 내가 회사 제품에 써먹으면 좋겠다고 생각하는 것은 manifests와, Module Federation 지원이 된다고 하는 webpack이 아닌 다른 번들러 정도인것 같다. 물론 변경을 주도하는 것이 커뮤니티인만큼 계속 변화를 주시하고 구현체도 탐구해보고 기여할 수 있는 부분도 모색해야 한다.


Written by 김맥스
Copyright © 2025 Jonghyuk Max Kim. All Right Reserved