Skip to content

동적 로딩과 라우팅

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 라우터 타입 분리의 핵심

라우터 유형사용 위치이유
BrowserRouterApp 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를 배치한다. 이 컴포넌트가 useLocationuseNavigate를 사용하여 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 Remotewebpack.config.js의 remotes에 Promise를 넣어 런타임에 서버 URL 결정
설정 API외부 JSON/API에서 scope와 remoteUrl을 가져와 동적 로드
서버 전환설정 파일 변경 + 새로고침만으로 대상 서버 교체, 재빌드 불필요
BrowserRouterApp Shell에서 사용, URL 바를 직접 제어
MemoryRouter마이크로앱에서 사용, Shell의 URL과 충돌 방지
라우팅 동기화Custom Event를 통한 Shell-마이크로앱 양방향 통신

다음 단계

  • 커리어 플랫폼 소개에서 지금까지 학습한 Module Federation 기술을 실제 프로젝트에 적용하는 설계를 시작한다