테마
앱쉘과 라우팅
App Shell이 React Router를 통해 마이크로앱들의 최상위 라우팅을 관장하고, 코드 스플리팅으로 각 앱을 지연 로드하는 구조를 구현한다.
학습 목표
- App Shell(
apps/shell)의 역할과 마이크로프론트엔드에서의 위치를 이해한다. packages/shell-router공유 패키지를 생성하여 라우팅 로직을 추상화한다.- React Router를 이용한 최상위 라우트 등록과 마이크로앱별 경로 매핑을 구현한다.
React.lazy와 코드 스플리팅으로 마이크로앱을 지연 로드하는 방법을 익힌다.- Navigation 컴포넌트를 활용한 마이크로앱 간 이동 흐름을 완성한다.
본문
1. App Shell의 역할
App Shell은 마이크로프론트엔드 아키텍처에서 오케스트레이터 역할을 한다. 개별 마이크로앱은 비즈니스 기능에 집중하고, App Shell은 앱들을 조합하는 인프라를 담당한다.
App Shell이 담당하는 핵심 기능은 다음과 같다.
| 기능 | 설명 |
|---|---|
| 최상위 라우팅 | URL 경로에 따라 어떤 마이크로앱을 렌더할지 결정 |
| 공통 레이아웃 | Header, Sidebar, Footer 등 모든 페이지에 공통으로 노출되는 UI |
| 인증/인가 | 로그인 상태 관리, 토큰 저장, 비인증 사용자 차단 |
| 네비게이션 | 마이크로앱 간 이동을 위한 메뉴와 링크 |
| 글로벌 상태 | 알림, 사용자 정보 등 앱 전반에 걸친 상태 관리 |
2. shell-router 패키지 구성
라우팅 관련 로직을 packages/shell-router에 분리하면 App Shell이 가벼워지고, 라우팅 추상화를 여러 곳에서 재사용할 수 있다.
bash
cd packages
pnpm create vite shell-router --template react-swc-ts
cd ..
pnpm installpackage.json 설정은 UI 라이브러리와 동일한 패턴을 따른다.
json
{
"name": "@career-up/shell-router",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.umd.cjs",
"import": "./dist/index.js"
}
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-router-dom": "^6.0.0"
}
}react-router-dom도 peerDependencies로 선언하여 앱의 React Router 인스턴스를 공유한다.
vite.config.ts에서 external 목록에 react-router-dom을 추가한다.
ts
rollupOptions: {
external: ["react", "react-dom", "react-router-dom"],
output: {
globals: {
react: "React",
"react-dom": "ReactDOM",
"react-router-dom": "ReactRouterDOM",
},
},
},패키지 내부 구조
packages/shell-router/
src/
types.ts # RouterType, CreateRouterProps 타입
router.ts # createRouter 팩토리 함수
injector.tsx # injectFactory (마이크로앱 마운트/언마운트)
hooks/
useShellEvent.ts # 라우팅 변경 이벤트 훅
index.ts # 엔트리3. createRouter 팩토리 함수
마이크로앱마다 사용하는 Router 유형이 다를 수 있다. App Shell은 BrowserRouter, Module Federation으로 주입되는 앱은 MemoryRouter를 사용하는 경우가 많다. createRouter 함수는 이 차이를 추상화한다.
ts
// packages/shell-router/src/types.ts
import type { RouteObject } from "react-router-dom";
export type RouterType = "browser" | "memory";
export interface CreateRouterProps {
type: RouterType;
routes: RouteObject[];
basePath?: string;
}ts
// packages/shell-router/src/router.ts
import {
createBrowserRouter,
createMemoryRouter,
} from "react-router-dom";
import type { CreateRouterProps } from "./types";
export type Router = ReturnType<typeof createBrowserRouter>;
export function createRouter({ type, routes, basePath }: CreateRouterProps) {
switch (type) {
case "browser":
return createBrowserRouter(routes);
case "memory":
return createMemoryRouter(routes, {
initialEntries: basePath ? [basePath] : ["/"],
});
}
}BrowserRouter는 실제 URL을 조작하며 전체 앱의 라우팅을 담당한다. MemoryRouter는 URL을 건드리지 않고 메모리 내에서만 라우팅하므로, 하위 마이크로앱이 상위 앱의 URL을 오염시키지 않는다.
4. injectFactory와 마이크로앱 마운트
injectFactory는 라우트 설정을 받아서 inject 함수를 반환하는 고차 함수다. 이 패턴 덕분에 마이크로앱마다 라우트만 다르게 주입하면 동일한 마운트/언마운트 로직을 재사용할 수 있다.
tsx
// packages/shell-router/src/injector.tsx
import React from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import type { RouteObject } from "react-router-dom";
import { createRouter } from "./router";
import type { RouterType } from "./types";
export function injectFactory(routes: RouteObject[]) {
return function inject(
rootElement: HTMLElement,
basePath?: string,
routerType: RouterType = "browser"
) {
const router = createRouter({
type: routerType,
routes,
basePath,
});
const root = createRoot(rootElement);
root.render(<RouterProvider router={router} />);
// 언마운트 함수 반환
return () => root.unmount();
};
}5. React Router를 이용한 최상위 라우트 등록
App Shell에서 각 마이크로앱의 경로를 등록한다. 커리어 플랫폼의 라우트 구조는 다음과 같다.
tsx
// apps/shell/src/router.tsx
const Home = React.lazy(() => import("posting/App"));
const Posting = React.lazy(() => import("posting/App"));
const Education = React.lazy(() => import("edu/App"));
const Networking = React.lazy(() => import("network/App"));
const Jobs = React.lazy(() => import("job/App"));
export const routes: RouteObject[] = [
{
path: "/",
element: <Layout />,
children: [
{ index: true, element: <Suspense fallback={<Loading />}><Home /></Suspense> },
{ path: "posting/*", element: <Suspense fallback={<Loading />}><Posting /></Suspense> },
{ path: "education/*", element: <Suspense fallback={<Loading />}><Education /></Suspense> },
{ path: "networking/*", element: <Suspense fallback={<Loading />}><Networking /></Suspense> },
{ path: "jobs/*", element: <Suspense fallback={<Loading />}><Jobs /></Suspense> },
],
},
];각 라우트의 와일드카드(/*)는 마이크로앱 내부의 하위 라우팅을 허용한다. /posting/123 같은 경로는 Posting 앱 내부 라우터가 처리한다.
6. React.lazy와 코드 스플리팅
React.lazy는 동적 import()를 감싸 컴포넌트를 지연 로드한다. Module Federation 환경에서는 import("posting/App")이 런타임에 리모트 앱의 번들을 가져온다.
코드 스플리팅의 핵심 이점은 초기 번들 크기 감소다. 사용자가 /posting만 방문하면 education, networking, job 앱의 코드는 다운로드하지 않는다.
7. Navigation 컴포넌트로 마이크로앱 간 이동
App Shell의 Layout 컴포넌트 안에 Navigation을 배치한다. react-router-dom의 NavLink를 사용하면 현재 활성 경로에 스타일을 자동 적용할 수 있다.
Layout 컴포넌트에서 NavLink 배열로 메뉴를 데이터 드리븐 방식으로 렌더한다. Outlet은 현재 매칭된 자식 라우트의 컴포넌트를 렌더하는 React Router의 슬롯으로, URL이 바뀌면 Outlet 안의 내용만 교체되고 Header와 Navigation은 유지된다.
tsx
const navItems = [
{ to: "/", icon: <Icon.Home />, label: "홈" },
{ to: "/posting", icon: <Icon.Home />, label: "게시글" },
{ to: "/networking", icon: <Icon.UserFriends />, label: "인맥" },
{ to: "/education", icon: <Icon.LaptopCode />, label: "교육" },
{ to: "/jobs", icon: <Icon.Briefcase />, label: "채용공고" },
];8. 앱 구성 상수와 경로 관리
마이크로앱의 경로와 이름을 상수 파일(constants.ts)에서 { prefix, basename, label } 객체로 중앙 관리한다. 라우트 등록과 Navigation 렌더에서 동일 상수를 참조하면 오타를 방지하고 경로 변경에 유연하게 대응할 수 있다.
핵심 정리
| 항목 | 내용 |
|---|---|
| App Shell 역할 | 라우팅 오케스트레이터 + 공통 레이아웃 + 인증 컨텍스트 |
| shell-router 패키지 | createRouter, injectFactory 등 라우팅 유틸 추상화 |
| 라우터 유형 분리 | Shell은 BrowserRouter, 마이크로앱은 MemoryRouter |
| 코드 스플리팅 | React.lazy + Suspense로 마이크로앱 지연 로드 |
| Navigation | NavLink로 활성 경로 자동 스타일링, Outlet으로 동적 콘텐츠 교체 |
| 경로 관리 | 상수 파일로 prefix/basename 중앙 관리 |
다음 단계
다음 문서 03-레이아웃과-스타일-격리.md에서는 공통 레이아웃의 구체적인 구조(Header, Sidebar, Content)를 완성하고, CSS 충돌을 방지하기 위한 다양한 격리 전략(CSS Modules, CSS-in-JS, Tailwind prefix, BEM)을 비교하며 적용한다.