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

Polymorphic component란?

  • 다양한 Semantic을 표현할 수 있는 UI 컴포넌트
  • 다양한 속성을 가질 수 있는 UI 컴포넌트
  • TypeScript를 사용하면 정적 유형 지정을 활용하여 강력한 유형의 다형성 구성 요소를 생성하고 유형 안전성을 제공하고 잠재적인 런타임 오류를 방지

개발

버튼 컴포넌트를 만든다고하면 다음과 같이 구성될수 있고 anchor 태그와 button 태그가 사용될 수 있습니다

interface ButtonProps {
    children: ReactNode;
    onClick(e: HTMLButtonElement | HTMLAnchorElement)?: void;
    href?: string;
    target?: HTMLAttributeAnchorTarget;
    className?: string;
    size?: 'xlarge' | 'large' | 'medium' | 'small';
    color?: 'primary' | 'secondary';
}
 
const Button = ({ children, onClick, href, target, className, size, color }: ButtonProps) => {
    const StyledButton = (
        <span className={`button__box--${size} button__box--${color}`}>
            <Typography>{children}</Typography>
        </span>
    );
 
    if (href) {
        return (
            <a className="button" href={href} target={target}>
                {StyledButton}
            </a>
        );
    }
    return (
        <button className="button" onClick={onClick}>
            {StyledButton}
        </button>   
    );
};

 

위와 같은 방식으로 제작시 다음과 같은 문제가 발생합니다

  1. anchor 태그 또는 button 태그에 필요한 attr이 생길때마다 ButtonProps를 정의해주어야 하고 추가 해주어야한다.
  2. Next에서 사용시 anchor태그 대신 Link 컴포넌트를  사용해야한다.
  3. props에 href를 미사용하였을때는 onClick props는 필수값이지만 현재는 무조건 optional로 되어있다.

따라서 사용성이 더 용이할수있게 컴포넌트를 설계하여야합니다

generic을 활용하여 다형성 컴포넌트를 개발할수있습니다

type PolymorphicComponentProps<T extends React.ElementType, PropsType = Record<string, unknown>> = 
  { component?: T; } & PropsType &
    React.ComponentPropsWithoutRef<T> & { ref?: React.ComponentPropsWithRef<T>['ref']; };

interface ButtonProps {
  children: React.ReactNode;
  size?: 'xlarge' | 'large' | 'medium' | 'small';
  color?: 'primary' | 'secondary' | 'black';
}

const Button = <T extends React.ElementType = 'button'>({ component, children, size, color, ...props }: PolymorphicComponentProps<T, ButtonProps>) => {
  const Component = component ?? 'button';
  
  return (
    <Component className="button" {...props}>
        <span className={`button__box--${size} button__box--${color}`}>
            <Typography>{children}</Typography>
        </span>
    </Component>
  )
};

 

사용예시 및 타입추론

PolymorphicComponentProps  type 은 component props에 element를 받아

React.ElementType과 PropsType을 제네릭으로 해당 element의 attr 타입 및 PropsType을  합쳐 Intersection type으로 추론하게 됩니다

 

사용 예시는 다음과 같습니다

// Next에서 사용
<Button component={Link} href="https://leeyc924.tistory.com" prefetch={false}>버튼</Button>
// React, react-router에서 사용
<Button component={Link} to="https://leeyc924.tistory.com">버튼</Button>
// button으로 사용
<Button onClick={handleClick} color="secondary">버튼</Button>

 

component  props 에 anchor 를 넣었을때 onClick의 타입은  React.MouseEventHandler<HTMLAnchorElement>로 추론되는것을 확인할수 있습니다

component  props 에 button을 넣었을때 href 타입은 에러가 발생합니다

component props에 Nextjs의 Link 를 넣으면 prefetch 와 같은 props가 추론되는것을 확인할수있습니다

참고자료

+ Recent posts