테마
동적 로딩과 라우팅
Module Federation에서 리모트 서버 URL을 빌드 타임에 고정하지 않고 설정 파일이나 API를 통해 런타임에 동적으로 지정하면, 클라이언트 코드 변경 없이 대상 서버를 교체할 수 있다. React Router와 결합하면 여러 마이크로앱의 페이지를 하나의 통합 앱으로 구성할 수 있다.
학습 목표
- Promise-based Dynamic Remote 방식으로 리모트 서버 URL을 런타임에 동적으로 지정하는 방법을 이해한다
- 외부 설정 파일(JSON API)에서 리모트 정보를 가져와 Module Federation에 적용하는 패턴을 구현할 수 있다
- React Router와 Module Federation을 결합하여 멀티 페이지 마이크로앱을 통합하는 구조를 설계할 수 있다
- App Shell 패턴에서 각 마이크로앱의 라우터를 연결하는 방법을 익힌다
1. 동적 리모트 로딩의 필요성
기본 Module Federation 설정은 리모트 서버 URL이 webpack.config.js에 하드코딩되어 있다. 이는 환경(개발/스테이징/프로덕션)마다 설정을 바꿔야 하고, 장애 시 대체 서버로 전환하기 어렵다.
2. Promise-based Dynamic Remote
Webpack Module Federation의 remotes 설정에는 문자열 대신 Promise를 반환하는 코드를 넣을 수 있다. 이 Promise가 resolve되면 해당 리모트 모듈을 사용할 수 있게 된다.
2.1 외부 설정 파일 구성
json
// api-server/public/remote.json
{
"scope": "componentApp1",
"remoteUrl": "http://localhost:3001/remoteEntry.js"
}2.2 webpack.config.js에서 동적 리모트 설정
js
// apps/main-app/webpack.config.js
new ModuleFederationPlugin({
name: "mainApp",
remotes: {
dynamic: `promise new Promise((resolve, reject) => {
fetch("http://localhost:4000/remote.json")
.then(response => response.json())
.then(data => {
const { scope, remoteUrl } = data;
// 스크립트 태그를 동적으로 생성하여 remoteEntry.js 로드
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("already initialized");
}
},
};
resolve(proxy);
};
script.onerror = reject;
document.head.appendChild(script);
})
.catch(reject);
})`,
},
shared: {
react: { singleton: true },
"react-dom": { singleton: true },
},
});2.3 Host 코드에서 사용
tsx
// apps/main-app/src/App.tsx
import React, { Suspense } from "react";
// "dynamic"은 webpack.config.js remotes의 키
const Button = React.lazy(() => import("dynamic/Button"));
function App() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<Button>동적 리모트 버튼</Button>
</Suspense>
);
}2.4 동적 서버 전환
설정 파일만 변경하면 클라이언트 코드나 Webpack을 재빌드하지 않고 대상 서버를 교체할 수 있다.
json
// 장애 시 대체 서버로 전환
// api-server/public/remote.json
{
"scope": "componentApp2",
"remoteUrl": "http://localhost:3002/remoteEntry.js"
}| 변경 전 | 변경 후 | 클라이언트 영향 |
|---|---|---|
| componentApp1 (localhost:3001) | componentApp2 (localhost:3002) | 새로고침만으로 전환 |
3. React Router와 Module Federation 통합
여러 마이크로앱이 각자 라우터를 가지고 있을 때, App Shell이 이들을 하나의 통합 네비게이션으로 연결한다.
3.1 라우터 타입 분리의 핵심
| 라우터 유형 | 사용 위치 | 이유 |
|---|---|---|
| BrowserRouter | App Shell (Host) | URL 바를 직접 제어, 브라우저 히스토리 사용 |
| MemoryRouter | 각 마이크로앱 (Remote) | URL 바에 영향 없이 내부 라우팅, Shell과 충돌 방지 |
마이크로앱이 독립 실행될 때는 BrowserRouter를, App Shell 안에서 실행될 때는 MemoryRouter를 사용하도록 분기한다.
3.2 마이크로앱의 injector와 라우터
tsx
// apps/app-jobs/src/injector.tsx
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { createAppRouter } from "./router";
export const inject = ({ routerType, rootElement, basePath = "/" }) => {
const router = createAppRouter(routerType, basePath);
const root = createRoot(rootElement);
root.render(<RouterProvider router={router} />);
return () => { queueMicrotask(() => root.unmount()); };
};tsx
// apps/app-jobs/src/router.tsx
import { createBrowserRouter, createMemoryRouter, Navigate } from "react-router-dom";
const routes = [{
path: "/",
element: <RoutingManager />,
children: [
{ index: true, element: <Navigate to="1" /> },
{ path: "1", element: <div>Jobs Page 1</div> },
{ path: "2", element: <div>Jobs Page 2</div> },
],
}];
export function createAppRouter(type: "browser" | "memory", basePath: string) {
return type === "browser"
? createBrowserRouter(routes, { basename: basePath })
: createMemoryRouter(routes, { initialEntries: [basePath || "/"] });
}3.3 App Shell에서 마이크로앱 통합
tsx
// apps/app-shell/src/App.tsx - MicroAppLoader 핵심
function MicroAppLoader({ appName, basePath }) {
const ref = useRef<HTMLDivElement>(null);
const location = useLocation();
useEffect(() => {
let unmount = () => {};
import(`${appName}/injector`).then(({ inject }) => {
unmount = inject({
routerType: "memory",
rootElement: ref.current!,
basePath: location.pathname.replace(basePath, "") || "/",
});
});
return () => unmount();
}, [location.pathname]);
return <div ref={ref} />;
}
// App Shell 라우트 구성
<BrowserRouter>
<nav>
<Link to="/jobs">Jobs</Link>
<Link to="/network">Network</Link>
</nav>
<Routes>
<Route path="/jobs/*" element={<MicroAppLoader appName="appJobs" basePath="/jobs" />} />
<Route path="/network/*" element={<MicroAppLoader appName="appNetwork" basePath="/network" />} />
</Routes>
</BrowserRouter>4. Shell과 마이크로앱 간 라우팅 동기화
App Shell의 URL이 바뀔 때 마이크로앱의 MemoryRouter도 동기화해야 하고, 반대로 마이크로앱 내부 네비게이션 시 Shell의 URL도 업데이트해야 한다. 이를 Custom Event로 처리한다.
4.1 RoutingManager 핵심 구현
마이크로앱의 routes 배열에서 루트 요소에 RoutingManager를 배치한다. 이 컴포넌트가 useLocation과 useNavigate를 사용하여 Shell 이벤트를 수신하고, 내부 네비게이션 변경을 Shell에 알린다.
tsx
// apps/app-jobs/src/RoutingManager.tsx (핵심 로직)
export const RoutingManager: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
// Shell → 마이크로앱: Shell 네비게이션 수신
useEffect(() => {
const handler = (e: Event) => {
const pathname = (e as CustomEvent).detail?.pathname;
if (pathname && pathname !== location.pathname) navigate(pathname);
};
window.addEventListener("appshell:navigated", handler);
return () => window.removeEventListener("appshell:navigated", handler);
}, [navigate, location.pathname]);
// 마이크로앱 → Shell: 내부 네비게이션 알림
useEffect(() => {
window.dispatchEvent(
new CustomEvent("microapp:navigated", { detail: { pathname: location.pathname } })
);
}, [location.pathname]);
return <Outlet />;
};프로덕션 적용 시 필수 사항:
| 항목 | 설명 |
|---|---|
| Bootstrap 패턴 | 비동기 모듈 로딩을 위해 index.ts에서 bootstrap.tsx를 import |
| 에러 처리 | 동적 리모트 로드 실패 시 ErrorBoundary로 격리 |
| 라우팅 동기화 | Custom Event로 Shell-마이크로앱 간 양방향 동기화 |
| 마운트/언마운트 | inject의 반환 함수로 정리하여 메모리 누수 방지 |
핵심 정리
| 항목 | 내용 |
|---|---|
| Promise-based Dynamic Remote | webpack.config.js의 remotes에 Promise를 넣어 런타임에 서버 URL 결정 |
| 설정 API | 외부 JSON/API에서 scope와 remoteUrl을 가져와 동적 로드 |
| 서버 전환 | 설정 파일 변경 + 새로고침만으로 대상 서버 교체, 재빌드 불필요 |
| BrowserRouter | App Shell에서 사용, URL 바를 직접 제어 |
| MemoryRouter | 마이크로앱에서 사용, Shell의 URL과 충돌 방지 |
| 라우팅 동기화 | Custom Event를 통한 Shell-마이크로앱 양방향 통신 |
다음 단계
- 커리어 플랫폼 소개에서 지금까지 학습한 Module Federation 기술을 실제 프로젝트에 적용하는 설계를 시작한다