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를 리모트에서 가져올 수 있다.