테마
라우팅과 헤더 통합 - 프로젝트 간 네비게이션
react-router-dom과 baseName을 활용하여 프로젝트 간 라우팅과 공통 헤더를 구현한다
학습 목표
- 마이크로 프론트엔드에서 프로젝트 간 라우팅 구조를 설계할 수 있다
- baseName을 활용한 중첩 라우팅의 원리를 이해한다
- ANTD 기반(docs)과 Tailwind 기반(web) 헤더 컴포넌트를 구현할 수 있다
- loadScript 유틸리티를 통한 마이크로앱 동적 로딩을 구현할 수 있다
본문
1. 프로젝트 간 라우팅 아키텍처
docs(7001)와 web(7002)이 서로의 컴포넌트를 소비하면서, 각각 독립적인 라우팅 구조를 유지해야 한다. react-router-dom의 baseName 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={{ baseName: "/web" }}
/>
}
/>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. 프로젝트 간 라우팅 구조 정리
| 프로젝트 | 포트 | 독립 라우팅 | 소비하는 컴포넌트 |
|---|---|---|---|
| Docs | 7001 | /, /shopping-list, /web | Web의 ShoppingList, Web 전체 앱 |
| Web | 7002 | /, /mail-list | Docs의 MailList |
| Legacy | 7003 | /, /sns | (아직 미소비) |
각 프로젝트는 독립된 SPA이면서 동시에 다른 프로젝트의 컴포넌트를 소비할 수 있다. baseName을 통해 중첩 라우팅이 충돌 없이 동작한다.
핵심 정리
- loadScript 유틸리티: 다른 프로젝트의 번들을 Promise 기반으로 동적 로딩하고,
window객체에서 UMD Export를 참조한다 - 파일명 고정: webpack의
output.filename을main.js로 고정하여 해시값 변경 문제를 해결한다 - baseName 중첩 라우팅:
BrowserRouter의basenameprops로 호스트 앱과 마이크로앱의 라우팅 네임스페이스를 분리한다 - Header 통합: 각 프로젝트의 Header가 자체 기술 스택(ANTD/Tailwind)으로 구현되되, 동일한 네비게이션 구조를 제공한다