Skip to content

라우팅과 헤더 통합 - 프로젝트 간 네비게이션

react-router-dom과 baseName을 활용하여 프로젝트 간 라우팅과 공통 헤더를 구현한다

학습 목표

  1. 마이크로 프론트엔드에서 프로젝트 간 라우팅 구조를 설계할 수 있다
  2. baseName을 활용한 중첩 라우팅의 원리를 이해한다
  3. ANTD 기반(docs)과 Tailwind 기반(web) 헤더 컴포넌트를 구현할 수 있다
  4. loadScript 유틸리티를 통한 마이크로앱 동적 로딩을 구현할 수 있다

본문

1. 프로젝트 간 라우팅 아키텍처

docs(7001)와 web(7002)이 서로의 컴포넌트를 소비하면서, 각각 독립적인 라우팅 구조를 유지해야 한다. react-router-dombaseName props가 이를 가능하게 한다.

2. loadScript 유틸리티

다른 프로젝트의 컴포넌트를 소비하려면 해당 프로젝트의 JavaScript 번들을 동적으로 로드해야 한다.

MicroApp 인터페이스

typescript
// utils.ts
interface MicroApp {
  default: {
    render: (container: HTMLElement, props?: Record<string, any>) => void;
    unmount?: () => void;
  };
  [componentName: string]: {
    render: (container: HTMLElement, props?: Record<string, any>) => void;
    unmount?: () => void;
  };
}

loadScript 구현

typescript
interface LoadScriptParams {
  url: string;           // 마이크로앱 호스트 URL
  appName: string;       // UMD 라이브러리 이름
  main?: string;         // 엔트리 파일 경로
}

export function loadScript({
  url,
  appName,
  main = "main.js",
}: LoadScriptParams): Promise<MicroApp> {
  return new Promise((resolve, reject) => {
    // 이미 로드된 경우 즉시 반환
    const existing = (window as any)[appName] as MicroApp | undefined;
    if (existing) {
      return resolve(existing);
    }

    // 스크립트 엘리먼트 생성
    const script = document.createElement("script");
    script.src = `${url}/${main}`;

    script.onload = () => {
      const microApp = (window as any)[appName] as MicroApp | undefined;
      if (microApp) {
        resolve(microApp);
      } else {
        reject(new Error(`${appName} 을 로드할 수 없습니다`));
      }
    };

    script.onerror = (err) => reject(err);
    document.head.appendChild(script);
  });
}

3. webpack 설정 - 일관된 파일 이름

개발/프로덕션에서 해시값이 붙는 파일명 문제를 해결한다.

javascript
// config-overrides.js
module.exports = override((config) => {
  // 빌드 결과물을 항상 main.js로 고정
  config.output.filename = "main.js";

  // publicPath 설정
  config.output.publicPath = "http://localhost:7001/";

  // UMD 라이브러리로 Export
  config.output.library = "docs";
  config.output.libraryTarget = "umd";

  return config;
});

4. Header 컴포넌트 구현

Docs 프로젝트 - ANTD Header

tsx
import { Menu } from "antd";
import { Link } from "react-router-dom";

function Header({ shadowRoot }: { shadowRoot: ShadowRoot }) {
  const items = [
    { key: "/", label: <Link to="/">메일 목록</Link> },
    { key: "/shopping-list", label: <Link to="/shopping-list">쇼핑 목록</Link> },
    { key: "/web", label: <Link to="/web">Web 앱</Link> },
  ];

  return (
    <StyleProvider container={shadowRoot}>
      <Menu mode="horizontal" theme="dark" items={items} />
    </StyleProvider>
  );
}

Web 프로젝트 - Tailwind Header

tsx
import { Link, useLocation } from "react-router-dom";

function Header() {
  const location = useLocation();
  const currentPath = `/${location.pathname.split("/")[1] || ""}`;

  const items = [
    { key: "/", label: "쇼핑 목록" },
    { key: "/mail-list", label: "메일 목록" },
  ];

  return (
    <nav className="bg-black w-full flex h-14">
      {items.map((item) => (
        <Link
          key={item.key}
          to={item.key}
          className={`
            px-6 py-4 text-white text-center
            ${currentPath === item.key
              ? "bg-blue-600"
              : "hover:bg-blue-500"
            }
          `}
        >
          {item.label}
        </Link>
      ))}
    </nav>
  );
}

5. baseName을 활용한 중첩 라우팅

구현

tsx
// Web 프로젝트 App.tsx
interface AppProps {
  baseName?: string;
}

function App({ baseName = "" }: AppProps) {
  return (
    <BrowserRouter basename={baseName}>
      <Header />
      <Routes>
        <Route path="/" element={<ShoppingList />} />
        <Route path="/mail-list" element={<MailListConsumer />} />
      </Routes>
    </BrowserRouter>
  );
}
tsx
// Docs 프로젝트에서 Web 앱 전체를 소비할 때
<Route
  path="/web/*"
  element={
    <MicroComponent
      url="http://localhost:7002"
      appName="web"
      componentName="default"
      props=&#123;&#123; baseName: "/web" &#125;&#125;
    />
  }
/>

6. loadScript에 Props 전달 확장

typescript
// 기존 loadScript에 props 전달 기능 추가
export function loadScript({
  url,
  appName,
  componentName = "default",
  main = "main.js",
}: LoadScriptParams): Promise<MicroApp> {
  // ... (기존 구현)
}

// 마이크로 컴포넌트에서 props 전달
const microApp = await loadScript({ url, appName });
const component = microApp[componentName];

component.render(container, {
  baseName: "/web",
  shadowRoot: shadowRoot,
});

7. 마이크로앱 로딩 시퀀스

8. 프로젝트 간 라우팅 구조 정리

프로젝트포트독립 라우팅소비하는 컴포넌트
Docs7001/, /shopping-list, /webWeb의 ShoppingList, Web 전체 앱
Web7002/, /mail-listDocs의 MailList
Legacy7003/, /sns(아직 미소비)

각 프로젝트는 독립된 SPA이면서 동시에 다른 프로젝트의 컴포넌트를 소비할 수 있다. baseName을 통해 중첩 라우팅이 충돌 없이 동작한다.

핵심 정리

  1. loadScript 유틸리티: 다른 프로젝트의 번들을 Promise 기반으로 동적 로딩하고, window 객체에서 UMD Export를 참조한다
  2. 파일명 고정: webpack의 output.filenamemain.js로 고정하여 해시값 변경 문제를 해결한다
  3. baseName 중첩 라우팅: BrowserRouterbasename props로 호스트 앱과 마이크로앱의 라우팅 네임스페이스를 분리한다
  4. Header 통합: 각 프로젝트의 Header가 자체 기술 스택(ANTD/Tailwind)으로 구현되되, 동일한 네비게이션 구조를 제공한다

다음 단계

MFA 소비 컴포넌트 ->