일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- 아키텍처
- react
- 합성 컴포넌트
- 웹워커
- TypeScript
- MicroFrontEnd
- CRA
- sharedworker
- Web
- 이것저것
- CustomHook
- Function Region
- 회고
- vite
- 티스토리챌린지
- 프론트엔드
- virtaullist
- 클린코드
- JavaScript
- 리액트
- 오블완
- context.api
- Webworker
- MFA
- radixui
- 에세이
- frontend
- 자바스크립트
- 리팩토링
- provider 패턴
- Today
- Total
Lighthouse of FE beginner
[MFA] Micro App의 라우팅 본문
들어가며
Micro Frontend는 하나의 Host App과 여러 개의 Remote App으로 구성되어 있습니다.
개별 Remote App의 라우팅은 당연하며 Host App을 통해 통합된 여러 App의 라우팅도 자연스럽게 발생해야 합니다.
각 App의 라우팅이 자연스럽게 발생하기 위해 각 App은 독자적인 라우터가 존재하며, 이 라우터들을 통합 하기 위해 Host App의 라우터가 필요하고, Host App은 통합된 App의 라우팅을 이어줄 수 있는 브릿지 역할을 해야합니다.
추상적으로 통합이지만 물리적으로는 각 App이 각자의 라우터를 소유하고 있습니다.
4개의 MFA 애플리케이션을 예시를 들어보면 다음과 같습니다.
Host App은 CMP, Openstackit, PaaS, App을 통합하고 있습니다. 그림을 살펴보면 각 App 에서의 라우팅이 발생하고 App 간의 라우팅 (eg. cmp/monitoring > paas/pod)도 발생하고 있습니다.
독자적인 App(eg. CMP App)의 라우팅은 기존의 SPA의 방식 (react-router-dom)으로 가능합니다. 하지만 Host App은 Remote App의 라우터를 알 수 없고, 알 필요도 없습니다.
Host App은 단순히 아키텍처 내의 라우팅 요청을 처리만 해주는 브릿지 역할만 담당할 뿐 입니다.
이벤트 기반의 라우팅
시나리오
Host App은 Remote App의 통합을 위한 App 입니다. Host App은 Shell App으로 명명합니다.
- Shell App의 라우터는 Browser Router 입니다.
- 통합된 App 내부의 Remote App의 라우터는 Memory Router 입니다.
- App을 구동합니다.
- Shell App에 Remote App을 통합하는 경우 Remote App은 Memory Router로 구동 됩니다.
- Remote App이 Standalone 인 경우, Browser Router로 구동 됩니다.
- Shell App의 라우팅을 위해 이벤트를 구독(리스닝)하고 있습니다. 이 이벤트의 이름은 [shell] navigate 입니다.
- Remote App은 Shell-Remote의 라우팅을 위해 [app-shell] navigated 이벤트를 구독(리스닝) 하고 있습니다. 해당 이벤트가 발생할 경우 Remote 앱 내부에서 라우팅이 발생합니다.
- location이 변경된 경우 Remote App에서 이벤트를 Dispatch 합니다. 이벤트는 ${type} navigated 입니다.
- Host App에서 Remote App을 래핑한 컴포넌트에서 해당 이벤트를 구독하고 있습니다. Remote App의 Memory Router 에서 발생한 라우팅(이벤트)을 실제 Path와 동기화(Browser Router에서의 라우팅) 합니다.
시퀀스 다이어그램으로 살펴보기
- Remote App에서 [shell]navigate 이벤트를 발생 (useShellNavigate)
- Shell App에서 [shell]navigate 이벤트를 구독 (useShellNavigateListener)
- Shell App에서 Event를 통해 전달 받은 pathname으로 navigate
- Shell App의 Remote App을 래핑한 컴포넌트에서 해당 Remote App의 주소로 navigation이 발생했는지 체크
- 만약 해당 Remote App의 주소로 navigation이 발생했다면 [app-shell] navigated 이벤트 발생
- Remote App에서 [app-shell] navigated 이벤트 구독
- Shell App에서 발생한 [app-shell] navigated 이벤트를 통해 Event를 통해 전달 받은 pathname과 메모리 라우터에서 관리하는 pathname이 일치하는지 판단
- pathname이 일치하면 return
- 일치하지 않다면 Remote App (Memory Router)에서 navigate 발생
- Shell App에서 발생한 [app-shell] navigated 이벤트를 통해 Event를 통해 전달 받은 pathname과 메모리 라우터에서 관리하는 pathname이 일치하는지 판단
- Remote App에서 location이 변경될 경우 [${type}] navigated 이벤트 발생
- Shell App의 Remote App을 래핑한 컴포넌트에서 [${type}] navigated 이벤트 구독
- Event를 통해 전달받은 pathname과 Shell App의 Browser Router의 location과 비교
- pathname이 일치할 경우 return
- 일치하지 않을 경우 Shell App(Browser Router)에서 navigate (실제 url path 변경)
- Event를 통해 전달받은 pathname과 Shell App의 Browser Router의 location과 비교
- Shell App의 Remote App을 래핑한 컴포넌트에서 [${type}] navigated 이벤트 구독
이벤트 정의
이벤트 명 | Listening Hook | 설명 |
[shell] navigate Shell App의 navigate를 요청 |
use-shell-navigator-listener | Shell App에서 해당 이벤트를 구독하고 있습니다. Shell App의 라우팅을 담당합니다. 이벤트가 발생 시 Shell App의 Browser Router에서 라우팅 (navigate)가 발생합니다. |
[app-shell] navigated Shell App의 특정 Remote App의 location이 변경(navigate)이 발생함 |
use-app-events | Remote App에서 해당 이벤트를 구독하고 있습니다. Shell App의 location이 변경되는 경우 이벤트가 dispatch 됩니다. 이벤트가 발생한 pathname과 Remote App의 location의 pathname을 비교합니다. 다르다면 이벤트가 발생한 pathname으로 Remote App에서 라우팅 합니다. MFA로 통합된 Remote App은 Memory Router로 주소의 변경 없이 메모리 상에서 라우팅이 발생하고, Standalone Remote App은 Browser Router로 실제 주소가 변경되며 라우팅 됩니다. |
[${type}] navigated Remote 앱의 navigate가 발생함 |
use-shell-events | Shell App의 Remote App을 래핑한 컴포넌트에서 해당 이벤트를 구독하고 있습니다. Remote App에 위치한 useAppEvent Hook에서 location이 변경되면 해당 이벤트를 dispatch 합니다. 각각의 Remote App은 /로 라우팅이 시작되기에 이벤트를 통해 전달 받은 pathname에 basename을 prefix로 붙입니다. prefix를 붙인 경로가 Shell App의 location.pathname과 일치하지 않다면 새로운 경로로 navigate 합니다. (화면의 Memory Router의 경로와 주소 창의 path를 동기화 시킵니다.) |
구현
shell-router 패키지
injectFactory
팩토리 함수를 반환합니다. routes 객체를 받아 Router을 만들고 rootElement를 만들어 랜더링 합니다.
function injectFactory({ routes }: { routes: RouteObject[] }) {
return ({
rootElement,
basePath,
routerType,
}: {
rootElement: HTMLElement;
basePath?: string;
routerType: RouterType;
}) => {
const router = createRouter({
type: routerType,
routes,
basePath,
});
const root = createRoot(rootElement);
root.render(<RouterProvider router={router} />);
return () => queueMicrotask(() => root.unmount());
};
}
createRouter
injectFactory 함수에서 사용하는 라우터 Factory 함수 입니다.
- Remote App이 MFA로 통합되는 경우 Memory Router로 생성합니다.
- Remote App이 Standalone 일 경우 Browser Router로 생성합니다.
import { createBrowserRouter, createMemoryRouter } from "react-router-dom";
import { type CreateRouterProps } from "./types";
type Router =
| ReturnType<typeof createBrowserRouter>
| ReturnType<typeof createMemoryRouter>;
export function createRouter({
type,
routes,
basePath,
}: CreateRouterProps): Router {
switch (type) {
case "browser":
return createBrowserRouter(routes);
case "memory":
return createMemoryRouter(routes, { initialEntries: [basePath || "/"] });
}
}
useShellNavigateListener
Shell App에서 [shell] naviagte 이벤트를 구독합니다. Shell App의 Browser Router에서 라우팅을 통해 주소를 변경합니다.
export default function useShellNavigateListener() {
const navigate = useNavigate();
useEffect(() => {
const shellNavigateListener = (event: Event) => {
const pathname = (event as CustomEvent).detail;
navigate(pathname);
};
window.addEventListener("[shell] navigate", shellNavigateListener);
return () => {
window.removeEventListener("[shell] navigate", shellNavigateListener);
};
}, [navigate]);
}
AppRoutingManager
Remote App의 최상위 Path / 의 Element 입니다. type은 Remote App의 Basename을 넣어주면 됩니다.
interface AppRoutingManagerProps {
type: string;
}
const AppRoutingManager: React.FC<AppRoutingManagerProps> = ({ type }) => {
useAppEvent(type);
return <Outlet />;
};
export default AppRoutingManager;
export const routes: RouteObject[] = [
{
path: "/",
element: (
<Suspense>
<Layout>
<AppRoutingManager type="remote2" />
</Layout>
</Suspense>
),
errorElement: <div>error</div>,
children: [
{ index: true, element: <Navigate to="home" /> },
{ path: "home", element: <Home /> },
{ path: "list", element: <List /> },
],
},
];
useAppEvent
AppRoutingManager에서 참조하는 Hook 입니다.
[app-shell] navigated 이벤트를 구독하고 있으며, 이벤트에 바인딩 된 path로의 라우팅을 담당합니다.
Remote App 에서 location이 변경된 경우 [${type}] navigated 이벤트를 dispatch 합니다.
[app-shell] navigated 이벤트는 Shell App에서 location이 변경됐을 때 해당 Remote App의 basename으로 라우팅이 된 경우 발생합니다.
export default function useAppEvent(type: string) {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
function shellNavigationHandler(event: Event) {
const pathname = (event as CustomEvent<string>).detail;
if (location.pathname === pathname) {
return;
}
navigate(pathname);
}
window.addEventListener(`[app-shell] navigated`, shellNavigationHandler);
return () => {
window.removeEventListener(
`[app-shell] navigated`,
shellNavigationHandler
);
};
}, [location, navigate]);
useEffect(() => {
window.dispatchEvent(
new CustomEvent(`[${type}] navigated`, { detail: location.pathname })
);
}, [location, type]);
}
useShellEvent
Shell App에서 Remote App을 래핑하는 컴포넌트에서 사용하는 Hook 입니다.
[${type}] navigated 이벤트를 구독하며 Remote App에서 발생한 라우팅을 Shell App의 Browser Router와 동기화 합니다.
Shell App의 location이 바뀌었을 때 location의 pathname이 파라미터로 전달 받은 basename으로 시작할 경우 [app-shell] navigated 이벤트를 dispatch 해 Shell App의 location이 변경 됐다는 것을 Remote App에 알립니다.
import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
export default function useShellEvent(type: string, basename: string) {
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
const appNavigationEventHandler = (event: Event) => {
const pathname = (event as CustomEvent<string>).detail;
const newPathname =
pathname === "/" ? basename : `${basename}${pathname}`;
if (newPathname === location.pathname) {
return;
}
navigate(newPathname);
};
window.addEventListener(`[${type}] navigated`, appNavigationEventHandler);
return () => {
window.removeEventListener(
`[${type}] navigated`,
appNavigationEventHandler
);
};
}, [basename, location, navigate, type]);
useEffect(() => {
if (location.pathname.startsWith(basename)) {
window.dispatchEvent(
new CustomEvent("[app-shell] navigated", {
detail: location.pathname.replace(basename, ""),
})
);
}
}, [basename, location]);
}
useShellNavigate
Remote App에서 Shell App의 라우팅을 발생 시켜 다른 Remote App으로 라우팅 하고자 할 경우 사용하는 훅 입니다.
export default function useShellNavigate() {
const navigate = (pathname: string) => {
window.dispatchEvent(
new CustomEvent("[shell] navigate", {
detail: pathname,
})
);
};
return navigate;
}
Use Case
handleClickList 함수는 Remote App 내부의 라우팅을, handleClickRemote 함수는 Shell App의 라우팅 이벤트를 발생 시킵니다.
import { useShellNavigate } from "@mfa/shell-router";
import { useNavigate } from "react-router-dom";
const Home = () => {
const shellNavigate = useShellNavigate();
const navigate = useNavigate();
const handleClickList = () => {
navigate("/list");
};
const handleClickRemote = () => {
shellNavigate("remote");
};
return (
<div>
home
<button onClick={handleClickList}>go to list</button>
<button onClick={handleClickRemote}>go to remote</button>
</div>
);
};
마치며
Micro App의 라우팅 통합 과정은 굉장히 복잡합니다.
이번 포스팅에서는 이벤트 기반의 라우팅 통합 과정에 대해서 살펴봤습니다.
MFA에 대해서 궁금하시다면 아래 포스팅을 참고해주세요.
MFA (Micro Frontend Architecture)
MFA (Micro Frontend Architecture)
Overview해당 포스팅은 MFA 강좌를 수강하고 MFA에 대해서 공부한 내용을 기록하기 위한 포스팅 입니다.MFA가 무엇인지 살펴보고 조직에서 MFA 도입을 고려하면 좋은 시점에 대해서 고민해보도록 하
kangs-develop.tistory.com
또 MFA에 대해서 자세하게 알고 싶으시면 아래 후기를 통해 강의를 참고해보시면 좋을 것 같습니다.
MFA 강의를 수강하고
MFA 강의를 수강하고
지난 7월 21일 MFA 강의 수강을 시작하면서 글을 작성했다.MFA, Monorepo 강의 수강을 시작하며 두 달이 지난 시점에서 강의를 완강했다. 부록 한 파트가 남긴 했는데 레거시 환경에서의 점진적 전환
kangs-develop.tistory.com
'[WEB] 프론트엔드' 카테고리의 다른 글
Rollup manualChunks를 활용한 브라우저 캐싱 전략 (9) | 2025.08.13 |
---|---|
Vercel 배포 시 Function Region (6) | 2025.08.08 |
React, Memoization (1) | 2025.06.18 |
[React] 제어 컴포넌트, 비제어 컴포넌트, 그리고 폼 다루기 (1) | 2025.06.01 |
Vite의 사전 번들링, HMR (1) | 2025.05.23 |