[React] Module Federation 따라잡기 (1)

seohyun Kang·2025년 1월 15일
0

React

목록 보기
10/11

MSA(Micro Service Architecture)를 배우면서 여러 JavaScript 어플리케이션 간에 코드와 리소스를 더 쉽게 공유할 수 있게 해주는 Module Federation에 대하여 알게 되었습니다.

Module Federation을 사용하여

What is the Module Federation?

Module Federation is an architectural pattern for the decentralization of JavaScript applications.

Motivation

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.

what is the difference between package and module federation?

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.

How do I use the Module Federation?

In the documents of the webpack, it describes the goal of the module federation.

  1. It should be possible to expose and consume any module type that webpack supports.
// 다양한 모듈 타입 예시
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
  }
})
  1. Chunk loading should load everything needed in parallel (web: single round-trip to server).
    • 필요한 모든 의존성을 단일 서버 요청으로 로드
    • 네트워크 효율성 최적화
// 병렬 로딩 예시
const Component = React.lazy(() => Promise.all([
  import('remote/Component'),
  import('remote/styles'),
  import('remote/utils')
]).then(([Component]) => Component));
  1. Control from consumer to container
    • Overriding modules is a one-directional operation.
    • Sibling containers cannot override each other's modules.
// 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']
})
  1. Concept should be environment-independent.
    • Usable in web, Node.js, etc.
// Node.js 환경 설정
{
  target: 'node',
  plugins: [
    new ModuleFederationPlugin({
      name: 'server',
      library: { type: 'commonjs-module' },
      filename: 'remoteEntry.js',
      exposes: {...}
    })
  ]
}
  1. Relative and absolute request in shared
    • Will always be provided, even if not used.
    • Will resolve relative to config.context.
    • Does not use a requiredVersion by default.
new ModuleFederationPlugin({
  shared: {
    // 항상 제공되는 절대 경로
    '/shared/utils': {
      import: path.resolve(__dirname, 'src/utils'),
      requiredVersion: false
    },
    // 상대 경로
    './shared/components': {
      import: './src/components'
    }
  }
})
  1. Module requests in shared
    • Are only provided when they are used.
    • Will match all used equal module requests in your build.
    • Will provide all matching modules.
    • Will extract requiredVersion from package.json at this position in the graph.
    • Could provide and consume multiple different versions when you have nested node_modules.
new ModuleFederationPlugin({
  shared: {
    'lodash': {
      singleton: true,
      requiredVersion: deps.lodash  // package.json에서 자동 추출
    },
    // 중첩된 node_modules 허용
    '@scope/package': {
      singleton: false,  // 다중 버전 허용
      requiredVersion: false
    }
  }
})
  1. Module requests with trailing / in shared will match all module requests with this prefix.
new ModuleFederationPlugin({
  shared: {
    '@material-ui/': {  // material-ui로 시작하는 모든 모듈 매칭
      singleton: true,
      requiredVersion: false
    }
  }
})

When do I use the Module Federation?

Webpack describe the use cases:

Separate builds per page

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.

Components library as container

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.

Module Federation Settings

The problem that arose while working on Module Federation was that packages had to be imported asynchronously.

  • Host : Service that bring the Module.
  • Remote : Service host the Module.

Directory architecture

// 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    

Source code

  1. 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",
    }),
  ],
}
  1. index.ts, bootstrap.tsx, App.tsx
// 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)

0개의 댓글