마이크로 프론트엔드 - 다른 서버의 React 컴포넌트를 동적으로 사용하기

main-app에서 특정 조건에 따라 다른 서버에의 컴포넌트를 동적으로 사용하는 프로젝트를 만들어보도록 하자.

루트 프로젝트 세팅

pnpm init
corepack use pnpm@8.10.0

 

pnpm-workspace.yaml 생성

packages:
  - "apps/*"

 

마이크로 앱 세팅

apps 디렉토리에서 다음 명령어를 통해 마이크로 앱을 설치해주도록 하자.

pnpm create mf-app

 각 마이크로 앱의 이름을 main-app, component-app1, component-app2으로 짓도록 하자.

 

component-app1, component-app2 세팅

먼저 components 디렉토리에 Button.tsx를 생성하고 App.tsx에서 이 버튼을 사용하도록 하자..

// components/Button.tsx
export default function Button() {
  return <button>component-app1 버튼</button>;
}

// App.tsx
import React from "react";
import ReactDOM from "react-dom/client";

import "./index.css";
import Button from "./components/Button";

const App = () => (
  <div className="container">
    <div>Name: component-app1</div>
    <div>Framework: react-18</div>
    <Button />
  </div>
);

const root = ReactDOM.createRoot(document.getElementById("app") as HTMLElement);
root.render(<App />);

 

 

그다음 module federation 설정을 다음과 같이 변경해주도록 하자.

const deps = require("./package.json").dependencies;

export const mfConfig = {
  name: "component_app1",
  filename: "remoteEntry.js",
  exposes: {
    "./Button": "./src/components/Button",
  },
  shared: {
    ...deps,
    react: {
      singleton: true,
      requiredVersion: deps.react,
    },
    "react-dom": {
      singleton: true,
      requiredVersion: deps["react-dom"],
    },
  },
};

 

component-app2도 component-app1과 동일하게 세팅을 해주면 된다.

 

main-app 세팅

이제 특정 조건(어떤 url, 어떤 스코프, 어떤 모듈인지)에 따라 component-app1과 component-app2를 동적으로 띄우도록 해보자.

 

동적으로 가져온다는 의미는 module federation에 명시적으로 설정하지 않고도 가져올 수 있도록 하는 것이다. 이를 쉽게 해줄 수 있는 라이브러리를 설치해보도록 하자.

 pnpm --filter main-app add @module-federation/utilities

 

그리고 각 모듈의 앱에 대한 remote 정보를 state로 관리하여 동적으로 가져올 수 있도록 코드를 작성해야 한다.

// App.tsx
import React, { useState } from "react";
import ReactDOM from "react-dom/client";

import "./index.css";
import DynamicButton from "./components/DynamicButton";

const App = () => {
  const [button, setButton] = useState<{
    url?: string;
    scope?: string;
    module?: string;
  }>({});

  function setButtonFromComponentApp1() {
    setButton({
      // url: "http://localhost:3001/remoteEntry.js",
      url: "http://localhost:3001",
      scope: "component_app1",
      module: "./Button",
    });
  }

  function setButtonFromComponentApp2() {
    setButton({
      url: "http://localhost:3002",
      scope: "component_app2",
      module: "./Button",
    });
  }

  return (
    <div className="container">
      <div>Name: main-app</div>
      <div>Framework: react-18</div>
      <div>
        <button onClick={setButtonFromComponentApp1}>
          Load Component App 1 Button
        </button>
        <button onClick={setButtonFromComponentApp2}>
          Load Component App 2 Button
        </button>
      </div>
      <div>
        <DynamicButton button={button} />
      </div>
    </div>
  );
};

const root = ReactDOM.createRoot(document.getElementById("app") as HTMLElement);
root.render(<App />);



// components/DynamicButton.tsx
import React, { Suspense } from "react";
import { importRemote } from "@module-federation/utilities";

type DynamicButtonProps = {
  button: { url?: string; scope?: string; module?: string };
};

export default function DynamicButton({
  button: { url, scope, module },
}: DynamicButtonProps) {
  if (!url || !scope || !module) return null;

  // Load
  const Component = React.lazy(() => importRemote({ url, scope, module }));

  return (
    <Suspense fallback={<div>Loading Button</div>}>
      <Component />
    </Suspense>
  );
}

 

이렇게 되면 버튼을 클릭할 때마다 동적으로 remote를 가져올 수 있게 된다.

 

 

결론

  • 동적으로 다른 서버의 컴포넌트를 사용하는 경우, module federation에서 설정하지 않고 소스코드에서 특정 조건에 맞추어 원격 로드할 대상을 동적으로 컨트롤 할 수 있다.
  • A/B 테스트와 같은 곳에서 이용할 수 있고 호스트의 설정에 따라 다른 버전의 UI를 리모트에서 가져올 수 있다.