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

seohyun Kang·2025년 1월 15일
0

React

목록 보기
10/11

Module Federation이란?

Module Federation은 JavaScript 애플리케이션의 분산화를 위한 아키텍처 패턴입니다.

Motivation

"빌드 시간이 아닌 런타임에 여러 독립적인 빌드 간에 코드를 공유할 수 있게 하는 것"

모놀리틱 아키텍처는 서비스의 확장이 유연하지 않고 서비스를 확장할 수록 속도가 느려지는 문제가 존재합니다. Module Federation은 여러 개의 개별 빌드가 하나의 애플리케이션을 구성합니다. 이러한 개별 빌드는 컨테이너처럼 작동하여 서로 코드를 노출하고 사용할 수 있으므로, 하나의 통합된 애플리케이션을 만들 수 있습니다. 또한, 이를 Micro Frontend라고 부르지만, 이에 국한되지는 않습니다.

Monolithic Archtecture와 Module Federation의 차이점?

Claude의 응답:

- Monolithic Architecture
- 장점
	1. 단순한 개발 환경: 모든 코드가 하나의 프로젝트에 있어 개발 설정이 간단합니다.
	2. 쉬운 디버깅: 전체 애플리케이션이 하나의 프로세스로 실행되어 디버깅이 상대적으로 용이합니다.
	3. 간편한 배포: 단일 애플리케이션으로 배포 프로세스가 단순합니다.
	4. 로컬 함수 호출: 컴포넌트 간 통신이 로컬 함수 호출을 통해 이루어져 성능상 이점이 있습니다.
	5. 초기 개발 속도: 작은 팀이나 프로젝트 초기에는 개발 속도가 빠를 수 있습니다.
	6. 일관된 개발 경험: 모든 개발자가 동일한 코드베이스에서 작업하므로 일관된 개발 경험을 제공합니다.

- 단점
	1. 확장성 제한: 애플리케이션의 특정 부분만 독립적으로 확장하기 어렵습니다.
	2. 기술 스택 제한: 전체 애플리케이션이 동일한 기술 스택을 사용해야 합니다.
	3. 코드베이스 복잡성: 시간이 지남에 따라 코드베이스가 커지고 복잡해지면서 유지보수가 어려워집니다.
	4. 긴 빌드 및 배포 시간: 작은 변경사항에도 전체 애플리케이션을 다시 빌드하고 배포해야 합니다.
	5. 팀 확장성 제한: 대규모 팀에서는 코드 충돌과 조정 문제가 발생할 수 있습니다.
	6. 장애 전파: 한 부분의 문제가 전체 애플리케이션에 영향을 미칠 수 있습니다.

- Module Federation
- 장점
	1. 독립적 개발: 각 팀이 자신의 모듈을 독립적으로 개발, 테스트, 배포할 수 있습니다.
	2. 기술 다양성: 각 모듈이 서로 다른 기술 스택을 사용할 수 있습니다.
	3. 점진적 업데이트: 전체 애플리케이션을 중단하지 않고 개별 모듈을 업데이트할 수 있습니다.
	4. 선택적 확장: 필요한 모듈만 선택적으로 확장할 수 있습니다.
	5. 병렬 개발: 여러 팀이 동시에 독립적으로 작업할 수 있어 개발 속도가 향상됩니다.
	6. 장애 격리: 한 모듈의 문제가 다른 모듈에 영향을 주지 않을 수 있습니다.

- 단점
	1. 설정 복잡성: 초기 설정과 구성이 복잡하고 학습 곡선이 가파릅니다.
	2. 런타임 의존성: 모듈 간 런타임 의존성을 관리해야 하는 추가 복잡성이 있습니다.
	3. 버전 충돌 가능성: 공유 라이브러리의 버전 충돌 문제가 발생할 수 있습니다.
	4. 네트워크 오버헤드: 런타임에 모듈을 로드하는 과정에서 네트워크 오버헤드가 발생할 수 있습니다.
	5. 디버깅 어려움: 분산된 모듈 간 문제를 디버깅하는 것이 더 복잡합니다.
	6. 테스트 복잡성: 통합 테스트가 더 복잡해질 수 있습니다.      

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
    }
  }
})

Module Federation은 언제 사용해야하는가?

Webpack describe the use cases:

1. 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.

2. 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 설정

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.

디렉토리 구조

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

코드

  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;

결론

간단한 프로젝트를 두 개 생성하여 Module Federation을 사용하여 통합하는 과정을 테스트했습니다.

Module Federation을 사용해보면서 분명 특정 화면 혹은 컴포넌트를 모듈화하여 기존 프로젝트에 통합할 수 있는 부분은 장점이 있어보였습니다. 특히, 위의 장단점에서 언급된 바와 같이 독립적 개발이 가능한 부분이 큰 장점으로 보였습니다.

일반적으로 규모가 큰 개발을 하게 되면 히스토리를 찾을 수 없는 레거시 혹은 데드 코드가 발생하고 이 부분이 일정 수준을 넘어가게 되면 Refactoring을 하게 되는데, Module Federation을 통해 개발 영역을 축소할 수 있고 문제가 되는 부분만 별도 개발하여 선택적으로 서비스에 통합이 가능해 보였습니다. 또한, 일반적으로 큰 프로젝트를 수정하다보면 수정한 코드가 다른 코드에 영향을 주는 문제가 발생하는데 Module Federation을 사용하면 해당 문제가 다른 영역으로 확장되지 않고 해당 모듈에서만 처리되는 부분도 장점으로 생각되었습니다 (설계를 잘 했을 때).

다만, 위와 같은 장점에도 불구하고 모듈 간의 결합도 등의 설계 단계에서 잘못만들어질 경우 오히려 구조가 복잡한 Monolithic Architecture가 되어 버릴 수도 있다는 생각이 들었습니다.

이론적, 개념적, 관습적으로 사용하고 알고 있는 결합도, 응집도 등의 개념을 실제 설계에 굉장히 디테일하게 논의해야할 필요성이 있어 보였습니다. 이러한 이유로 상용 서비스에서는 결제 프로세스부터는 다른 모듈로 동작하도록 하는 등의 서비스 단위를 분리하는 것으로 보입니다.


References :
- Module Federation
- Webpack (Module_federation)
- Youtube-Zack Jackson

0개의 댓글