Micro Frontend Playground
Micro Frontend 환경을 구축해 보며 학습한 내용을 정리해 보려고 한다.
Codex와 함께 프로젝트 구조는 다음과 같이 잡았다.
apps/
shell/ # Vite + React
remote-workspace/ # Vite + React
remote-admin/ # Vite + React
packages/
ui/
config/
contracts/pnpm workspace와 turborepo를 사용해 모노레포를 구축했으며, apps 하위의 프로젝트들은 Vite + React 환경으로 구성했다.
corepack
corepack은 Node.js에 내장된 패키지 매니저 관리 도구로, pnpm, yarn, npm 등을 쉽게 설치하고 사용할 수 있게 해준다.
corepack enable이를 통해 여러 사람들이 각자 다른 패키지 매니저 버전을 사용하면서 벌어지는 의존성의 간극을 최소화해 줄 수 있다. 이는 곧, 개발 환경의 편차를 줄인다고 볼 수 있다.
pnpm workspace로 경계 만들기
모노레포 환경을 구축하기 위해 pnpm-workspace.yaml 파일에 패키지 정보를 설정한다.
packages:
- "apps/*"
- "packages/*"Turbo 설정
turbo.json 파일에 dev, build, lint 등의 스크립트를 설정한다.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"dev": {
"cache": false, // dev에서 캐싱 쓰지 않기
"persistent": true // watch mode
},
"build": {
"dependsOn": ["^build"], // 현재 패키지가 의존하는 상위 패키지들의 build 먼저 실행
"outputs": ["dist/**"]
},
"lint": {
"dependsOn": ["^lint"] // 현재 패키지가 의존하는 패키지들의 lint 먼저 실행
},
"typecheck": {
"dependsOn": ["^typecheck"] // 현재 패키지가 의존하는 패키지들의 typecheck 먼저 실행
},
"clean": {
"cache": false
}
}
}이렇게 각 스크립트마다 무엇이 먼저 실행되어야 하는지 명시적으로 또 자동으로 관리할 수 있다.
Typescript 설정
Vite + React 앱들을 먼저 만들고, packages/config에 공통된 typescript 설정을 적용하기 위해 tsconfig.base.json을 만들었다.
// packages/config/tsconfig.base.json
{
"compilerOptions": {
"target": "esnext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": []
}
}각 앱들의 tsconfig.json은 tsconfig.base.json을 확장한다.
이렇게 기준이 되는 config가 존재하기 때문에 새 앱을 추가하는 상황이라든가, 설정을 변경하고자 할 때 각 앱들에게 안전하게 전파될 수 있겠구나 싶었다.
// apps/*/tsconfig.json
{
"extends": ["../../packages/config/tsconfig.base.json"],
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"types": ["vite/client"]
},
"include": ["src"]
}NPM Scope 적용
packages/*에 있는 패키지들을 @repo/* 스코프로 선언해서 실제 npm 패키지처럼 사용할 수 있도록 정의했다.
// packages/*/package.json
{
"name": "@repo/contracts",
"version": "1.0.0",
"private": true,
...
}이렇게 설정해 두면 import도 스코프를 활용해 접근할 수 있어 가독성을 높여 주는 것 같다.
import { ShellContext } from '@repo/contracts'contracts로 shell-remote 결합 줄이기
root가 되는 shell은 각 remote 앱들이 필요한 정보를 모두 넘기는 것보다는 최소한의 정보만 전달하고, remote는 필요한 데이터를 자체적으로 조회하도록 구성했다.
예를 들면:
export type FeatureFlags = Record<string, boolean>;
export interface ShellContext {
userId: string;
featureFlags: FeatureFlags;
}이렇게 정의해둔 인터페이스를 기반으로 shell과 remote가 서로 구현 세부사항을 몰라도 되고, 결과적으로 변경의 영향 범위를 줄일 수 있겠다는 점에서 가장 중요한 포인트라고 느껴졌다.
중간 점검
pnpm dev 실행 시, apps/* 앱들이 모두 잘 띄워지는지 체크

여기서 shell이 각 remote 앱을 호출해서 렌더링하도록 해야 하기 때문에 본격적으로 Module Federation을 적용한다.
Module Federation
remote 설정
pnpm add -D @originjs/vite-plugin-federation -w를 통해 vite plugin을 추가하고, vite.config.ts에 관련 옵션을 추가했다.
// apps/remote-*/vite.config.ts
federation({
name: "remote_workspace",
filename: "remoteEntry.js",
exposes: {
"./WorkspaceApp": "./src/RemoteRoot.tsx",
},
shared: ["react", "react-dom"],
});- name: shell import prefix
- filename: shell이 런타임에 읽을 진입점
- exposes: 외부 공개 모듈
- shared: 공통 라이브러리 중복 번들링 방지
shell 설정
shell에서는 remote 앱들의 엔트리 포인트를 등록했다.
// apps/shell/vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import federation from "@originjs/vite-plugin-federation";
export default defineConfig({
plugins: [
react(),
federation({
name: "shell",
remotes: {
remote_workspace: "http://localhost:5173/assets/remoteEntry.js",
remote_admin: "http://localhost:5174/assets/remoteEntry.js",
},
shared: ["react", "react-dom"],
}),
],
});그리고 App.tsx에서 lazy import로 가져와 활용한다.
import { lazy, Suspense } from "react";
const WorkspaceApp = lazy(() => import("remote_workspace/WorkspaceApp"));
const AdminApp = lazy(() => import("remote_admin/AdminApp"));
function App() {
return (
<div>
<h1>Shell</h1>
<Suspense fallback={<div>Loading ...</div>}>
<WorkspaceApp />
<AdminApp />
</Suspense>
</div>
);
}
export default App;Remote 앱에 Context 주입
// src/shell/mfe/context.ts
import type { ShellContext, Navigation } from "@repo/contracts";
export function createShellContext(navigate: (to: string) => void): ShellContext {
return {
userId: "louie",
featureFlags: {
"workspace.newUI": true,
},
requestNavigation: (nav: Navigation) => {
switch (nav.type) {
case "go":
navigate(nav.to);
break;
case "back":
window.history.back();
break;
case "openExternal":
window.open(nav.url, "_blank", "noopener,noreferrer");
break;
default:
break;
}
},
};
}context를 생성하고
// packages/contracts/src/index.ts
export type FeatureFlags = Record<string, boolean>;
export type Navigation =
| { type: "go"; to: string }
| { type: "back" }
| { type: "openExternal"; url: string };
export interface ShellContext {
userId: string;
featureFlags: FeatureFlags;
requestNavigation: (nav: Navigation) => void;
}contract를 재정립한 뒤, 실제로 Context를 전달해 단일 렌더링에서 라우팅 기반으로 각 앱에 접근하도록 구성했다.
// apps/shell/src/App.tsx
import { lazy, Suspense, useMemo } from "react";
import { useNavigate, Routes, Route, Navigate } from "react-router-dom";
import { createShellContext } from "./mfe/context";
const WorkspaceApp = lazy(() => import("remote_workspace/WorkspaceApp"));
const AdminApp = lazy(() => import("remote_admin/AdminApp"));
function App() {
const navigate = useNavigate();
const context = useMemo(
() => createShellContext((to) => navigate(to)),
[navigate],
);
return (
<div>
<h1>Shell</h1>
<Suspense fallback={<div>Loading ...</div>}>
<Routes>
<Route path="/" element={<Navigate to="/workspace" replace/>} />
<Route path="/workspace/*" element={<WorkspaceApp context={context} />} />
<Route path="/admin/*" element={<AdminApp context={context} />} />
<Route path="*" element={<div>404 Not Found</div>} />
</Routes>
</Suspense>
</div>
);
}
export default App;

트러블 슈팅
모든 앱을 dev 환경으로 실행했을 때, shell에서 앱들을 불러올 때 404 오류가 발생해서 보니, federation 플러그인이 vite 7을 제대로 지원하지 않는 것 같다는 이슈 가 있다고 한다.
내가 활용한 버전은 다음과 같았다.
"packageManager": "pnpm@10.16.0"
"@originjs/vite-plugin-federation": "^1.4.1",
"vite": "^7.3.1"vite 버전을 낮추는 법도 있지만, 최신 버전에서 동작시키기 위해 remote 앱들을 build한 후, preview로 띄우도록 해서 해결했다.
# build
pnpm --filter remote-workspace build
pnpm --filter remote-admin build
# serve preview
pnpm --filter remote-workspace preview
pnpm --filter remote-admin preview
# shell dev
pnpm --filter shell dev마무리
이렇게 찍먹 수준으로 Micro Frontend 환경을 구축해 보았는데, 생각보다 많이 복잡하고 개념을 잡기 어려웠다.
하지만
- 패키지 매니저 및 빌드 순서 같은 운영 방식 통일
- contracts 중심의 인터페이스 설계
- shell, remote 간의 책임 분리
이 규칙들 덕분에 앞으로 기능을 추가할 때 어떻게 추가하면 될지 감을 어느정도 잡을 수 있었고, micro frontend는 여러 앱들의 경계를 어떻게 설계하는지에 가깝다는 것을 느끼며 대규모 프로젝트에 적용하면 효용이 느껴지겠다고 생각했다.
2026 © nimuseel.RSS