MSA(Micro Service Architecture)를 배우면서 여러 JavaScript 어플리케이션 간에 코드와 리소스를 더 쉽게 공유할 수 있게 해주는 Module Federation에 대하여 알게 되었습니다.
Module Federation을 사용하여
Module Federation is an architectural pattern for the decentralization of JavaScript applications.
Multiple separate builds should form a single application. These separate builds act like containers and can expose and consume code among themselves, creating a single, unified application.
This is often known as Micro-Frontends, but is not limited to that.
Claude said that:
- Module Federation:
- Runtime integration
- Advantages:
1. Reduce the inital rendering speed
2. Independently develop and deploy
3. Reduce the code duplication and increase the reusablity
- Disadvantages:
1. High learning curve and complex settings
2. Increase the possibility of the version conflicts
3. Increase the possibility of runtime error
- Npm package
- Buildtime integration
- Advantages:
1. easier settings
2. stable version management
3. Easy type checking and static anxiety
- Disadvantages:
1. Dependencies must be installed separately for each app
2. Bundle size may increase
3. Deployment process is heavier
Actually, I felt that npm package and module federation were mostly similar. Eventhough, there is no advantages for using Module Federation. But, I think it's because I applied module federation to reusable components like inputs, buttons, etc.
In the documents of the webpack, it describes the goal of the module federation.
// 다양한 모듈 타입 예시
new ModuleFederationPlugin({
exposes: {
'./Component': './src/Component.js', // JavaScript
'./Style': './src/styles.css', // CSS
'./Image': './src/image.png', // 이미지
'./Worker': './src/worker.js', // Web Worker
'./Wasm': './src/module.wasm' // WebAssembly
}
})
// 병렬 로딩 예시
const Component = React.lazy(() => Promise.all([
import('remote/Component'),
import('remote/styles'),
import('remote/utils')
]).then(([Component]) => Component));
// Container
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js'
},
shared: ['react']
})
// Consumer에서 오버라이드
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button'
},
shared: ['react']
})
// Node.js 환경 설정
{
target: 'node',
plugins: [
new ModuleFederationPlugin({
name: 'server',
library: { type: 'commonjs-module' },
filename: 'remoteEntry.js',
exposes: {...}
})
]
}
new ModuleFederationPlugin({
shared: {
// 항상 제공되는 절대 경로
'/shared/utils': {
import: path.resolve(__dirname, 'src/utils'),
requiredVersion: false
},
// 상대 경로
'./shared/components': {
import: './src/components'
}
}
})
new ModuleFederationPlugin({
shared: {
'lodash': {
singleton: true,
requiredVersion: deps.lodash // package.json에서 자동 추출
},
// 중첩된 node_modules 허용
'@scope/package': {
singleton: false, // 다중 버전 허용
requiredVersion: false
}
}
})
new ModuleFederationPlugin({
shared: {
'@material-ui/': { // material-ui로 시작하는 모든 모듈 매칭
singleton: true,
requiredVersion: false
}
}
})
Webpack describe the use cases:
Each page of a Single Page Application is exposed from container build in a separate build. The application shell is also a separate build referencing all pages as remote modules. This way each page can be separately deployed. The application shell is deployed when routes are updated or new routes are added. The application shell defines commonly used libraries as shared modules to avoid duplication of them in the page builds.
Many applications share a common components library which could be built as a container with each component exposed. Each application consumes components from the components library container. Changes to the components library can be separately deployed without the need to re-deploy all applications. The application automatically uses the up-to-date version of the components library.
The problem that arose while working on Module Federation was that packages had to be imported asynchronously.
// react-componentkit
ㄴ host
ㄴ src
ㄴ App.tsx
ㄴ bootstrap.tsx
ㄴ index.ts
ㄴ package.json
ㄴ tsconfig.json
ㄴ webpack.config.js
ㄴ remote
ㄴ src
ㄴ App.tsx
ㄴ bootstrap.tsx
ㄴ index.ts
ㄴ package.json
ㄴ tsconfig.json
ㄴ webpack.config.js
// host/webpack.config.js
{
plugins: [
new ModuleFederationPlugin({
name: "service",
remotes: {
componentkit: "componentkit@http://localhost:3000/remoteEntry.js",
},
shared: { react: { singleton: true }, "react-dom": { singleton: true } },
}),
new ExternalTemplateRemotesPlugin(),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
}
// remote/webpack.config.js
{
plugins: [
new ModuleFederationPlugin({
name: "componentkit",
filename: "remoteEntry.js",
exposes: {
"./App": "./src/App",
},
shared: { react: { singleton: true }, "react-dom": { singleton: true } },
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
}
// src/index.ts
import("./bootstrap").catch((err) => {
console.error("Error loading the app:", err);
});
// src/bootstrap.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
// App.tsx
import React, { Suspense } from "react";
const RemoteApp = React.lazy(() => import("componentkit/App"));
const App = () => {
return (
<div>
<h1>App1</h1>
<Suspense fallback={"loading..."}>
<RemoteApp />
</Suspense>
</div>
);
};
export default App;
References :
- Module Federation
- Webpack (Module_federation)