마이크로 프론트엔드 실전 가이드 — Module Federation 2.0, 팀 5개 이상 환경에서의 아키텍처 설계
배포 당일 아침, Slack에 메시지가 하나 옵니다. "너 오늘 배포하냐?" 우리 팀이 7개로 늘어나고 하나의 프론트엔드 레포를 공유하던 시절, 이 메시지가 반복될 때마다 아키텍처를 다시 생각하게 됐습니다. 누군가 먼저 배포를 올리면 다른 팀이 코드를 못 올리고, 롤백 한 번이면 세 팀 작업이 함께 날아가는 상황이요.
마이크로 프론트엔드(MFE)는 단순히 "앱을 쪼개는 기술"이 아닙니다. 팀 간 의존성을 끊어내고, 각 도메인이 진짜 독립적인 소유권을 가질 수 있게 해주는 조직 설계에 가깝습니다. 이 글에서는 MFE의 핵심 개념부터 Module Federation 2.0 기반 실전 구성, 그리고 도입 전에 반드시 따져봐야 할 트레이드오프까지 한 번에 짚어드립니다.
Spotify, Walmart, 올리브영처럼 이미 도입해서 릴리즈 주기를 단축한 사례가 있는 반면, 공유 설정 실수로 평균 15% 번들이 증가하거나 운영 복잡도에 치이는 사례도 분명히 존재합니다. 어떤 상황에서 도입하면 효과적이고, 어떤 상황에서는 피하는 게 나은지 함께 살펴봅니다.
목차
핵심 개념
MFE 구조 먼저 파악하기
마이크로 프론트엔드는 하나의 거대한 SPA를 독립적으로 개발·빌드·배포 가능한 작은 단위로 분리하는 아키텍처 패턴입니다. 백엔드에서 이미 익숙한 마이크로서비스 아키텍처(MSA)의 철학을 프론트엔드 레이어에 그대로 이식한 거예요.
┌─────────────────────────────────────┐
│ Shell Application │
│ (공통 헤더 · 라우팅 · 인증) │
│ │
│ ┌───────────┐ ┌───────────────┐ │
│ │ Team A │ │ Team B │ │
│ │ (검색 MFE)│ │ (결제 MFE) │ │
│ └───────────┘ └───────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ Team C (추천 MFE) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘구성 요소는 크게 세 가지입니다.
- Shell(Container) App: 전체 레이아웃, 공통 네비게이션, 라우팅, 인증을 담당하는 호스트. 각 마이크로 앱의 마운트 포인트를 제공하고 생명주기를 관리합니다. 제가 경험상 Shell 앱이 점점 비대해지는 게 가장 먼저 생기는 문제더라고요 — "공통"이라는 이름 아래 뭐든 여기에 쑤셔 넣게 됩니다.
- Remote(Micro) App: 결제, 검색, 장바구니처럼 특정 도메인을 책임지는 독립 배포 단위. 자체 번들을 가지며 Shell이 런타임에 동적으로 불러옵니다.
- Shared Layer: 디자인 시스템, 공통 유틸, 상태 관리처럼 여러 앱이 공유하는 코드. 중복 로딩을 막는 게 핵심입니다.
통합 방식 3가지: ① 빌드 타임 통합(npm 패키지) — 가장 단순하지만 독립 배포 불가 ② 서버 사이드 컴포지션(ESI, SSI) — SSR 친화적 ③ 런타임 클라이언트 통합(Module Federation, single-spa, iframe) — 현재 주류
Module Federation 2.0이 왜 지금 중요한가
2026년 4월 기준, Webpack 5에서 시작된 Module Federation이 2.0 안정 릴리즈에 도달했습니다. 가장 큰 변화는 빌드 도구로부터 런타임이 완전히 분리된 것입니다. 기존에는 Webpack에 묶여 있었는데, 이제 Rspack·Rollup·Vite·Metro까지 동일한 런타임으로 동작합니다. Next.js, Storybook도 공식 지원하고 있어서 실무 진입 장벽이 많이 낮아졌어요.
// Remote 앱 webpack.config.js
const { ModuleFederationPlugin } = require('@module-federation/enhanced');
new ModuleFederationPlugin({
name: 'checkout',
filename: 'remoteEntry.js', // Shell이 이 파일을 참조해 모듈을 동적 로딩
exposes: {
'./CheckoutWidget': './src/CheckoutWidget',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
})// Shell 앱 webpack.config.js
new ModuleFederationPlugin({
name: 'shell',
remotes: {
checkout: 'checkout@https://checkout.cdn.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
})
remoteEntry.js란? Remote 앱이 어떤 모듈을 외부에 공개하는지 알려주는 런타임 모듈 레지스트리입니다. Module Federation을 처음 접한다면 이 파일을 "Remote 앱의 공개 API 목록"이라고 이해하면 됩니다. Shell은 이 파일 하나만 알면 런타임에 필요한 모듈을 꺼내 쓸 수 있습니다.
singleton: true 설정이 핵심인데요, 이걸 빠뜨리면 React 인스턴스가 여러 개 로드되어 Hook 오류가 발생하는 꽤 짜증스러운 상황을 마주치게 됩니다. 저도 처음엔 이 설정 없이 삽질을 제법 했습니다.
한 가지 더 — shared 설정에는 eager 옵션도 있습니다. eager: true로 설정하면 비동기 청크 없이 초기 번들에 라이브러리가 포함되어 첫 로딩이 빠르지만 번들 크기가 커집니다. 대부분의 경우 기본값(비동기 로딩)이 더 적합하고, eager는 초기 렌더에 반드시 필요한 라이브러리에만 제한적으로 사용하는 편이 좋습니다.
2026년 주목할 흐름 — Native ESM Federation
핵심 개념을 넘어, 지금 가장 주목받는 트렌드를 하나 소개합니다. 빌드 도구 없이 브라우저 네이티브 ESM과 Import Maps만으로 원격 모듈을 페더레이션하는 방식입니다. 복잡한 번들러 설정 없이도 Host 앱이 Remote 모듈을 조합할 수 있어요.
<!-- index.html - Import Map 방식 -->
<script type="importmap">
{
"imports": {
"checkout": "https://checkout.cdn.example.com/v2.1.0/index.js",
"search": "https://search.cdn.example.com/v3.0.0/index.js"
}
}
</script>
<script type="module">
import { CheckoutWidget } from 'checkout';
import { SearchBar } from 'search';
</script>아직 브라우저 지원 범위나 캐싱 전략에서 고려할 게 있습니다. Import Maps는 최신 브라우저에 한정되고, CDN 캐싱과 버전 관리 전략이 함께 설계되어야 합니다. 그래도 빌드 파이프라인 복잡도를 획기적으로 줄일 수 있다는 점에서 앞으로 빠르게 확산될 방향으로 보입니다.
실전 적용
아래 세 가지 예시는 각각 독립적인 선택지이기도 하지만, 실제 프로젝트에서는 함께 조합해서 쓰는 경우가 많습니다. Turborepo로 모노레포 뼈대를 잡고, Vercel로 통합 배포를 구성하고, EventBus로 앱 간 통신을 처리하는 식으로요.
예시 1: Turborepo 모노레포로 MFE 뼈대 잡기
실무에서 MFE를 시작할 때 모노레포 구조를 함께 가져가는 경우가 많습니다. 코드 공유는 쉽게, 배포는 독립적으로 유지할 수 있거든요. pnpm 모노레포가 처음이라면 Turborepo 공식 Getting Started를 먼저 살펴보시면 좋습니다.
# 프로젝트 구조
apps/
shell/ # 호스트 앱 (포트 3000)
checkout/ # 결제 MFE (포트 3001)
search/ # 검색 MFE (포트 3002)
packages/
design-system/ # 공유 UI 컴포넌트
utils/ # 공유 유틸리티
turbo.json
pnpm-workspace.yaml# pnpm-workspace.yaml
# pnpm에게 어디서 워크스페이스 패키지를 찾아야 하는지 알려주는 파일입니다
packages:
- 'apps/*'
- 'packages/*'// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}dependsOn: ["^build"]는 "이 앱을 빌드하기 전에 의존하는 패키지들을 먼저 빌드하라"는 의미입니다. packages/design-system이 apps/checkout보다 먼저 빌드되어야 할 때 이 설정 덕분에 순서를 신경 쓰지 않아도 됩니다.
| 명령어 | 설명 |
|---|---|
pnpm turbo dev |
전체 앱 동시 개발 서버 실행 |
pnpm turbo build --filter=checkout |
checkout 앱만 선택적 빌드 |
pnpm turbo build --affected |
변경된 앱만 빌드 (CI 최적화) |
예시 2: Next.js App Router + Vercel 기반 MFE 구성
모노레포 뼈대를 잡았다면, 이제 각 앱을 실제로 어떻게 조합할지가 문제입니다. Next.js App Router를 이미 사용하고 있다면 @vercel/microfrontends 패키지가 가장 빠른 진입로입니다. SEO가 중요한 서비스에도 적합합니다.
pnpm add @vercel/microfrontends// vercel.json
// 실제 동작 여부는 공식 문서(https://vercel.com/docs/microfrontends)에서 확인하세요
{
"microfrontends": {
"applications": {
"checkout": {
"development": { "origin": "http://localhost:3001" },
"production": { "origin": "https://checkout.my-app.com" }
},
"search": {
"development": { "origin": "http://localhost:3002" },
"production": { "origin": "https://search.my-app.com" }
}
}
}
}// shell/app/layout.tsx - 서버 컴포넌트에서 Remote 앱 컴포즈
import { Suspense } from 'react';
import { CheckoutWidget } from 'checkout/CheckoutWidget';
import { SearchBar } from 'search/SearchBar';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<header>
<Suspense fallback={<div>검색 로딩 중...</div>}>
<SearchBar />
</Suspense>
</header>
<main>{children}</main>
<Suspense fallback={<div>결제 위젯 로딩 중...</div>}>
<CheckoutWidget />
</Suspense>
</body>
</html>
);
}Suspense가 필수입니다. Remote 앱이 서버 컴포넌트로 컴포즈될 때 스트리밍 처리를 위해 반드시
<Suspense>로 감싸야 합니다. 생략하면 Remote 앱 로딩 지연이 전체 페이지 렌더를 블로킹하는 예상치 못한 문제를 만날 수 있습니다.
이 방식의 강점은 각 MFE가 서버 컴포넌트로 컴포즈되기 때문에 클라이언트 번들 다운로드 없이 빠른 First Contentful Paint를 확보할 수 있다는 점입니다.
예시 3: 앱 간 통신 — CustomEvent로 느슨하게 연결하기
런타임 통합은 해결했는데, 앱끼리는 어떻게 대화할까요? 공유 상태를 직접 import하는 방식은 강결합을 만들기 쉽습니다. 프레임워크 종속성 없이 브라우저 표준 API만으로 이벤트 버스를 구현할 수 있습니다.
// packages/utils/src/eventBus.ts
type EventMap = {
'cart:item-added': { productId: string; quantity: number };
'auth:logged-in': { userId: string; token: string };
'checkout:completed': { orderId: string };
};
export const eventBus = {
emit<K extends keyof EventMap>(event: K, detail: EventMap[K]) {
window.dispatchEvent(new CustomEvent(event, { detail }));
},
on<K extends keyof EventMap>(
event: K,
handler: (detail: EventMap[K]) => void
) {
const listener = (e: Event) => handler((e as CustomEvent).detail);
window.addEventListener(event, listener);
return () => window.removeEventListener(event, listener); // cleanup 반환
},
};// checkout/src/CheckoutWidget.tsx — 이벤트 발행 측
import { eventBus } from 'utils/eventBus';
async function handleOrderComplete(orderId: string) {
const response = await fetch(`/api/orders/${orderId}/confirm`, { method: 'POST' });
if (!response.ok) throw new Error('주문 처리 실패');
// 결제 MFE는 장바구니 MFE를 직접 알 필요가 없습니다
eventBus.emit('checkout:completed', { orderId });
}// search/src/SearchBar.tsx — 이벤트 구독 측 (React 컴포넌트)
import { useEffect } from 'react';
import { eventBus } from 'utils/eventBus';
function SearchBar() {
useEffect(() => {
const cleanup = eventBus.on('checkout:completed', ({ orderId }) => {
console.log(`주문 ${orderId} 완료 — 검색 히스토리 초기화`);
});
return cleanup; // 컴포넌트 언마운트 시 리스너 자동 해제
}, []);
// ...
}타입 안전성을 위해 EventMap으로 이벤트명과 페이로드를 중앙 관리하면 팀 간 인터페이스 계약서 역할도 겸하게 됩니다. "결제 MFE가 어떤 이벤트를 발행하는지 어디서 봐요?"라는 질문에 이 파일 하나로 답할 수 있습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 독립 배포 | 팀별 자체 CI/CD 파이프라인으로 다른 팀 일정에 영향받지 않고 배포 가능 |
| 기술 스택 자유 | 팀마다 React·Vue·Svelte 등 다른 프레임워크를 혼용할 수 있음 |
| 장애 격리 | 결제 MFE가 다운되어도 검색이나 메인 페이지는 정상 동작 |
| 팀 자율성 | 도메인별 완전한 소유권(Full Ownership) 부여로 의사결정 속도 향상 |
| 점진적 마이그레이션 | 레거시 모놀리식 앱을 전면 재작성 없이 파트별로 교체 가능 |
단점 및 주의사항
솔직히 장단점 표를 채우면서 "UX 파편화" 항목이 제일 아프더라고요. 팀마다 다른 폰트 굵기, 미묘하게 다른 버튼 애니메이션 타이밍 — 사용자는 모르는 척 넘어가지 않습니다.
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 번들 중복 | 공유 설정 실수 시 React를 여러 버전으로 로드, 평균 15% 번들 증가 사례 있음 | shared: { singleton: true } 설정 + 버전 고정 |
| 운영 복잡도 | 수십 개 앱의 CI/CD, 모니터링, 로그 추적을 각각 관리해야 함 | 전담 플랫폼 팀 구성, 중앙 관측 도구(OpenTelemetry 등) 도입 |
| UX 파편화 | 팀 분리 시 디자인 시스템 준수, 폰트, 애니메이션 타이밍이 팀마다 달라질 위험 | 공유 디자인 시스템 패키지 강제화, 플랫폼 팀 심사 프로세스 |
| E2E 테스트 복잡 | 각 MFE 유닛 테스트 통과해도 전체 플로우 통합 테스트 별도 필요 | Playwright 기반 통합 테스트 파이프라인 별도 구성 |
플랫폼 팀(Platform Team): MFE 환경에서 각 도메인 팀이 비즈니스 로직에 집중할 수 있도록 공통 인프라, 디자인 시스템, 배포 파이프라인을 전담 관리하는 팀. MFE를 성공적으로 운영한 기업들의 공통 조건입니다. 전담 팀 구성이 어렵다면 최소한 CI/CD·디자인 시스템을 책임질 수 있는 구성원이 1~2명은 있어야 출발할 수 있습니다.
실무에서 가장 흔한 실수
- 팀이 2~3개인데 MFE를 도입하는 경우 — 복잡도 비용이 독립 배포 이점을 훌쩍 넘어섭니다. 팀이 5개 이상이고 주당 독립 배포가 필요한 상황이 되었을 때 도입을 검토하는 것이 적절합니다.
- shared 설정 없이 시작하는 경우 — 나중에 고치면 된다고 생각하기 쉽지만, React 인스턴스 충돌은 런타임에서야 발견되고 디버깅이 꽤 까다롭습니다. 처음부터
singleton: true와 버전 범위를 명시해두면 이후 고생을 많이 덜 수 있습니다. - 앱 간 직접 import로 강결합을 만드는 경우 —
checkout이search의 내부 모듈을 직접 import하기 시작하면 분리한 의미가 사라집니다. EventBus나 공유 패키지를 통한 인터페이스 계약을 유지하는 것을 권장합니다.
마치며
마이크로 프론트엔드는 '기술 문제'보다 '팀 규모와 배포 독립성 문제'를 해결하는 아키텍처라는 점을 기억해두시면 좋겠습니다. 팀이 크고 도메인 경계가 명확하다면 Module Federation 2.0은 이미 실무 투입이 충분히 가능한 성숙도에 도달해 있습니다.
지금 바로 시작해볼 수 있는 3단계입니다.
- 도입 적합성부터 점검해보세요. 현재 팀 수, 주간 배포 빈도, 도메인 경계 명확성을 체크하고 "팀 5개 이상 + 주당 독립 배포 필요 + 공통 인프라 담당 가능" 세 조건이 맞는지 확인해보세요. 플랫폼 팀을 꾸리기 어렵더라도 CI/CD와 디자인 시스템을 담당할 수 있는 구성원이 있는지가 출발점입니다.
- 작은 POC부터 시작해보세요.
pnpm create mf-app으로 Module Federation 2.0 예제를 생성하고, Shell 하나 + Remote 하나만으로 런타임 통합이 동작하는지 체험해보시면 됩니다. 하루면 감이 옵니다. - 모노레포 + 공유 패키지 구조를 먼저 잡아두세요. Turborepo로
apps/shell,apps/[도메인],packages/design-system뼈대를 구성하고, 공유 컴포넌트와 이벤트 버스 타입 계약을 먼저 정의해두면 이후 팀이 늘어날 때 훨씬 수월합니다.
다음 글: Module Federation 2.0 심화 — Vite·Rspack 환경에서 Remote 앱을 CDN에 배포하고 버전 롤백과 A/B 테스트까지 구성하는 방법
참고 자료
- Micro Frontend Architecture: A Full 2026 Guide | ELITEX
- Module Federation 2.0 Reaches Stable Release | InfoQ (2026.04)
- Micro-Frontends 2026: Module Federation 3.0 & Native ESM Federation | WeskillBlog
- 대규모 프론트엔드 아키텍처의 새로운 패러다임 | 올리브영 Tech Blog
- 마이크로 프론트엔드의 이해 및 구현 | AWS 권장 가이드
- Micro-Frontends: Are They Still Worth It in 2025? | Feature-Sliced Design
- Micro-Frontend Netflix Case Study | DEV Community
- Building micro-frontends with webpack's Module Federation | LogRocket Blog
- The Truth Behind Micro Frontends: Insights from Real Case Studies | Bitovi
- 5 Pitfalls of Using Micro Frontends and How to Avoid Them | SitePoint
- Module Federation official docs | module-federation.io
- Micro Frontends with Native Federation | DEV Community