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

이번에는 지난 포스팅 글에서 React 컴포넌트를 동적으로 사용하는 것에 이어서 동적으로 지정하는 부분을 이어서 진행해보고자 한다.

 

main-app에서 다른 서버의 컴포넌트 동적으로 지정하기

먼저 App.tsx에서 dynamic 경로로 Button을 import하는 코드를 작성해보도록 하자.

// @ts-ignore
const Button = React.lazy(() => import("dynamic/Button"));

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>
        <Suspense fallback={<div>Loading Button</div>}>
          <Button />
        </Suspense>
      </div>
    </div>
  );
};

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

아직까지 dynamic 경로에 대한 타입을 명확하게 지정할 수 없으므로 `@ts-ignore` 타입 오류를 처리해준다.

 

그 다음 module federation 설정에 dynamic과 관련된 경로를 추가해주도록 하자.

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

export const mfConfig = {
  name: "main_app",
  filename: "remoteEntry.js",
  remotes: {
    dynamic: "component_app1@http://localhost:3001/remoteEntry.js",
  },
  shared: {
    ...deps,
    react: {
      singleton: true,
      requiredVersion: deps.react,
    },
    "react-dom": {
      singleton: true,
      requiredVersion: deps["react-dom"],
    },
  },
};

 

이렇게 설정한 후 main-app을 실행하면 다음과 같이 component-app1 버튼이 잘 나오게 된다.

 

 

api-server 앱 추가하기

먼저 apps 내에 api-server 디렉토리를 만들고 pnpm init 명령어로 프로젝트를 초기화해주도록 하자.

 

그리고 serve 패키지를 설치해주도록 한다.

pnpm --filter api-server add serve

 

그리고 public 디렉토리에 remote.json을 추가해주도록 하자.

{
  "scope": "component_app1",
  "remoteUrl": "http://localhost:3001/remoteEntry.js"
}

 

마지막으로 package.json에 dev 스크립트를 추가해주도록 한다.

"dev": "serve public -p 4000 --cors"

 

Promise Based Dynamic Remotes 설정하기

module federation 설정에서 remote를 동적으로 가져와서 지정할 수 있는 Promised Based Dynamic Remotes를 설정하도록 하자.

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

export const mfConfig = {
  name: "main_app",
  filename: "remoteEntry.js",
  remotes: {
    dynamic: `promise new Promise(resolve => {
      fetch("http://localhost:4000/remote.json")
        .then((res) => res.json())
        .then(({remoteUrl, scope}) => {
          console.log(remoteUrl, scope);

          const script = document.createElement('script')
          script.src = remoteUrl;
          script.onload = () => {
            const proxy = {
              get: (request) => window[scope].get(request),
              init: (...arg) => {
                try {
                  return window[scope].init(...arg)
                } catch(e) {
                  console.log('remote container already initialized')
                }
              }
            }
            resolve(proxy)
          }

          document.head.appendChild(script);
        });
      })
    `,
  },
  shared: {
    ...deps,
    react: {
      singleton: true,
      requiredVersion: deps.react,
    },
    "react-dom": {
      singleton: true,
      requiredVersion: deps["react-dom"],
    },
  },
};

 

이렇게 설정한 뒤 main-app을 실행해보면 다음과 같이 다른 서버의 컴포넌트를 받아올 수 있다.

 

그리고, api-server의 remote.json을 app2로 변경하더라도 잘 반영됨을 볼 수 있다.

{
  "scope": "component_app2",
  "remoteUrl": "http://localhost:3002/remoteEntry.js"
}

 

결론

  • Promised Base Dynamic Routes 방식으로 클라이언트의 코드를 고정한 상태에서 대상 서버를 동적으로 변경할 수 있다.
  • scope와 remoteUrl 값이 맞으면 규격에 맞는 컴포넌트를 로드할 수 있다.
  • 특정 remoteUrl이 장애 상황일 경우 다른 서버로 접속하도록 하는 기능 등을 구현하기에 용이하다.

 

출처

- https://webpack.kr/concepts/module-federation/#promise-based-dynamic-remotes