Module Federation 이란?

JavaScript 아키텍처 유형 중 하나로  단일 Webpack 빌드에 포함된 모듈뿐만 아니라 여러 서버에 배포되어 있는 원격 모듈을 하나의 애플리케이션에서 로딩할 수 있는 기능입니다

Webpack 5 부터 코어로 추가 되었습니다

  • 웹 애플리케이션을 여러 개의 독립적인 모듈로 나누고, 이러한 모듈을 동적으로 로드하거나 언로드할 수 있는 웹 개발 아키텍처의 한 형태입니다. 주로 웹 애플리케이션의 규모가 커지고 복잡해질 때 유용하게 사용됩니다.
  • 핵심 개념은 각 모듈이 독립적으로 개발되고 배포될 수 있다는 것입니다. 각 모듈은 자체적으로 독립적으로 동작하며, 필요에 따라 애플리케이션에 동적으로 통합될 수 있습니다. 이는 전체 애플리케이션을 더 작고 관리하기 쉬운 조각으로 나눌 수 있게 해줍니다.
  • 이 아키텍처의 장점 중 하나는 각 모듈이 독립적으로 개발될 수 있으므로, 다른 모듈의 변경이나 업데이트가 전체 애플리케이션에 미치는 영향을 최소화할 수 있다는 것입니다. 또한, 필요에 따라 모듈을 동적으로 로딩하고 언로딩할 수 있어서 초기 로딩 속도를 최적화하고 사용자 경험을 향상시킬 수 있습니다.
  • Module Federation은 웹 애플리케이션의 확장성과 유연성을 높일 수 있는 강력한 아키텍처 중 하나로 평가되고 있습니다.

용어

  • Host : 페이지 로드 중(onLoad 이벤트가 트리거될 때) 처음 초기화되는 Webpack 빌드. Runtime에 Remote를 load 할수 있습니다
  • Remote : 또 다른 Webpack 빌드로, 그 일부가 " 호스트 " 에 의해 사용됩니다.
  • Bidirectional-hosts (양방향 호스트) : 번들 또는 Webpack 빌드가 호스트 또는 원격으로 작동할 수 있는 경우입니다. 런타임 시 다른 애플리케이션을 사용하거나 다른 애플리케이션에 의해 사용됨니다.
  • Omnidirectional-host (전방향 호스트): 호스트 자체는 시작 시 호스트인지 원격인지 알 수 없습니다. 이를 통해 webpack은 호스트 자체 공급업체를 semver 규칙을 기반으로 하는 공급업체로 변경할 수 있습니다. 필요한 경우 여러 버전을 허용합니다.

Module federation 적용 시 기대효과

서비스를 개발하고 성공해 나가다보면 규모가 점점 커질 수 있다. 서비스 규모가 커질수록 작은 변경에도 영향 범위가 커질 수 있으며 영향 범위 예측도 점점 복잡하고 어려워진다. Module Federation은 컨테이너 빌드를 따로 하므로 빌드 시간, 영향 범위, 로딩 시간 측면에서 유리하다.

구분 기존 방식 Module Federation 적용 시 기대효과
빌드범위와 배포시간 작은 변경에도 전체 빌드를 하고 배포한다. 변경된 컨테이너만 빌드하고 배포해서 시간이 줄어든다.
영향도 전체 서비스를 대상으로 영향도를 검증한다. 변경 영향이 해당 컨테이너에 국한되므로 검증 범위도 줄어든다.
(단, 원격 모듈의 인터페이스를 변경했다면 호스트 앱도 검증이 필요하다.)
로딩시간 전체 빌드가 변경되었으므로 배포 직후 로딩 시간도 오래 걸린다.
(브라우저 캐시 적용 안됨)
배포한 컨테이너의 원격 모듈만 새로 로딩하므로 배포 직후 로딩 시간도 상대적으로 짧다.

구현

Remote

remote 는 React v18.2.0, Webpack 5 Tailwindcss, Typescript 기반으로 프로젝트 구성을 하였습니다

// Component

import '../../tailwind.css';

export interface NavbarProps {}

const Navbar = () => {
  return (
    <ul className="flex gap-1">
      <li className="flex justify-center items-center border-solid border-b-2">
        <a href="https://www.naver.com">네이버</a>
      </li>
      <li className="flex justify-center items-center border-solid border-b-2">
        <a href="https://google.com">구글</a>
      </li>
      <li className="flex justify-center items-center border-solid border-b-2">
        <a href="https://www.daum.net">다음</a>
      </li>
    </ul>
  );
};

export default Navbar;
// webpack.config.js

const path = require('path');
const { EnvironmentPlugin } = require('webpack');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = (_, argv) => ({
  devServer: {
    static: { directory: path.resolve(__dirname) },
    port: 3000,
    hot: true,
  },
  module: {
    rules: [
      {
        test: /\.m?js/,
        type: 'javascript/auto',
        resolve: {
          fullySpecified: false,
        },
      },
      {
        test: /\.(css|s[ac]ss)$/i,
        use: ['style-loader', 'css-loader', 'postcss-loader'],
      },
      {
        test: /\.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-typescript',
              ['@babel/preset-react', { runtime: 'automatic' }],
              '@babel/preset-env',
            ],
            plugins: ['@babel/transform-runtime'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote',
      filename: 'remoteEntry.a350ed3e.js',
      exposes: {
        './Navbar': './src/components/Navbar/index.tsx',
      },
      shared: {
        ...deps,
      },
    }),
    new HtmlWebPackPlugin({
      template: './public/index.html',
    }),
  ],
  output: {
    chunkFilename: '[id].a350ed3e.bundle.js',
    publicPath: 'auto',
    clean: true,
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
    modules: [path.resolve(__dirname, 'src'), 'node_modules'],
    alias: {
      '@components': path.resolve(__dirname, 'src/components/index.ts'),
    },
  },
});

 

Host

host는 Nextjs SCSS, Typescript 기반으로 구성되어있으며 Remote app을 integration 해보았습니다

// nextjs에서 module federation을 사용하기 위해선 @module-federation/nextjs-mf를 이용해야 합니다
pnpm add -D @module-federation/nextjs-mf
//next.config.js

const NextFederationPlugin = require('@module-federation/nextjs-mf');

/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack(config, options) {
    const { isServer } = options;
    if(!isServer) {

      config.plugins.push(
        new NextFederationPlugin({
          name: 'host',
          remotes: {
            remote: `remote@http://localhost:4000/remoteEntry.a350ed3e.js`,
          },
          filename: 'static/chunks/remoteEntry.a350ed3e.js',
        }),
        );
        
      }
      return config;
  },
};

module.exports = nextConfig;
// Component

import dynamic from "next/dynamic";

const Navbar = dynamic(() => import('remote/Navbar'), { ssr: false, loading: () => <>...loading</>})

export default function Home() {
  return (
    <div>
      hello world
      <Navbar />
    </div>
  );
}

동작방식

  • main.js: 최초에 앱을 초기화하는 역할을 합니다. 가장 상위의 Micro App A의 HTML에서 가장 먼저 불러와지는 청크가기도 합니다. Micro App A의 Container Reference를 가진 청크로, 해당 처리를 해줍니다. 이 외에도 createRoot등, 브라우저에서도 앱을 최초로 초기화하는데 필요한 코드들을 가지고 있습니다.
  • remoteEntry.js: Container을 가진 Micro App을 초기화하는 청크입니다. 특정 마이크로 앱에서 다른 Micro App을 import할 때 가장 먼저 불리는 청크가기도 합니다. Micro App B, C의 Container를 가진 청크로, 해당 처리를 해줍니다.
  • XXX.js : 이외 청크입니다. 공유 의존성 청크과 Micro App 본문에 대한 청크입니다. main.js, remoteEntry.js내부의 런타임 처리를 통해 로드가 제어됩니다.

Remote 를 빌드한 이후 Host에서 호출하면 다음과 같이 Runtime Integration 을 하게 됩니다

Host
http://localhost:3000
Remote
http://localhost:4000
Remote build file & Host network call

 

참고자료

'Frontend' 카테고리의 다른 글

[React] vite를 이용한 react library 만들기  (4) 2024.12.30
[React] UnControlled Component  (3) 2024.12.24
[React] ReactFiber와 렌더링 과정  (6) 2024.12.24
[WEB] 브라우저 렌더링 과정  (0) 2024.12.23
[React] Polymorphic component  (5) 2024.01.28

+ Recent posts