이 글은 누구를 위한 것인가
- 대규모 프론트엔드 앱을 팀별로 독립 배포하고 싶은 아키텍트
- Module Federation을 도입했는데 공유 의존성 충돌 문제를 겪는 팀
- 마이크로프론트엔드의 장단점과 적합한 상황을 이해하고 싶은 개발자
들어가며
마이크로프론트엔드는 모든 팀의 해결책이 아니다. 단일 팀이 운영하는 중소 규모 앱이라면 오버엔지니어링이 될 수 있다. 하지만 5개 이상의 팀이 하나의 프론트엔드 앱을 함께 개발하고 독립적으로 배포해야 한다면, 마이크로프론트엔드는 매우 실용적인 선택이다.
Module Federation 2.0(2024년 출시)은 기존 1.0의 한계를 크게 개선했다. 타입 공유, 동적 원격 설정, 더 나은 에러 처리가 주요 개선사항이다.
이 글은 bluefoxdev.kr의 마이크로프론트엔드 설계 가이드 를 참고하고, Module Federation 2.0 실전 구현 관점에서 확장하여 작성했습니다.
1. 마이크로프론트엔드 적합성 판단
[마이크로프론트엔드가 적합한 경우]
✅ 5개 이상 팀이 독립적으로 기능 개발
✅ 팀별로 다른 배포 주기 (한 팀이 다른 팀을 기다리지 않아야 함)
✅ 기술 스택이 다양 (React + Vue + Angular 혼합)
✅ 대규모 레거시를 점진적으로 현대화
✅ 확장 가능한 플랫폼 아키텍처 (마켓플레이스, SaaS)
[마이크로프론트엔드가 과한 경우]
❌ 팀이 1~3개로 조율이 쉬움
❌ 모든 기능이 밀접하게 연관됨
❌ 성능이 최우선 (초기 로딩 오버헤드)
❌ 팀에 DevOps 역량이 부족함
2. Module Federation 2.0 설정
2.1 Shell App (Host) 설정
// shell-app/webpack.config.js (Webpack 5 + MF 2.0)
const { ModuleFederationPlugin } = require('@module-federation/webpack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
// 동적 원격 설정 (URL을 빌드 타임이 아닌 런타임에 결정)
catalog: 'catalog@[catalogUrl]/remoteEntry.js',
checkout: 'checkout@[checkoutUrl]/remoteEntry.js',
auth: 'auth@[authUrl]/remoteEntry.js',
},
shared: {
react: {
singleton: true, // 하나만 로드 (필수!)
requiredVersion: '^19.0.0',
eager: true, // shell에서 먼저 로드
},
'react-dom': {
singleton: true,
requiredVersion: '^19.0.0',
eager: true,
},
// 디자인 시스템도 singleton으로 공유
'@company/design-system': {
singleton: true,
requiredVersion: '^2.0.0',
},
},
}),
],
};
2.2 Remote App (Catalog) 설정
// catalog-app/webpack.config.js
const { ModuleFederationPlugin } = require('@module-federation/webpack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'catalog',
filename: 'remoteEntry.js',
exposes: {
// 외부에 노출할 모듈
'./CatalogPage': './src/pages/CatalogPage',
'./ProductCard': './src/components/ProductCard',
'./useCatalog': './src/hooks/useCatalog',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
'@company/design-system': {
singleton: true,
requiredVersion: '^2.0.0',
},
},
}),
],
};
2.3 TypeScript 타입 공유 (MF 2.0 신기능)
// catalog-app/webpack.config.ts
import { ModuleFederationPlugin } from '@module-federation/webpack';
export default {
plugins: [
new ModuleFederationPlugin({
name: 'catalog',
exposes: {
'./CatalogPage': './src/pages/CatalogPage',
},
// MF 2.0: 타입 자동 생성 및 배포
dts: {
generateTypes: true, // 타입 파일 생성
consumeTypes: true, // 원격 타입 소비
typesFolder: '@mf-types', // 생성 위치
},
}),
],
};
3. Shell App 라우팅 구현
// shell-app/src/App.tsx
import React, { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 동적 import로 원격 컴포넌트 로드
const CatalogPage = lazy(() => import('catalog/CatalogPage'));
const CheckoutPage = lazy(() => import('checkout/CheckoutPage'));
const AuthPage = lazy(() => import('auth/AuthPage'));
// 에러 바운더리로 원격 앱 실패 격리
class RemoteErrorBoundary extends React.Component<
{ fallback: React.ReactNode; children: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
function App() {
return (
<BrowserRouter>
<Shell> {/* 공통 레이아웃 */}
<Routes>
<Route
path="/catalog/*"
element={
<RemoteErrorBoundary fallback={<ErrorPage remote="catalog" />}>
<Suspense fallback={<PageLoader />}>
<CatalogPage />
</Suspense>
</RemoteErrorBoundary>
}
/>
<Route
path="/checkout/*"
element={
<RemoteErrorBoundary fallback={<ErrorPage remote="checkout" />}>
<Suspense fallback={<PageLoader />}>
<CheckoutPage />
</Suspense>
</RemoteErrorBoundary>
}
/>
</Routes>
</Shell>
</BrowserRouter>
);
}
4. 공유 상태 관리
// 공유 상태는 singleton 서비스로 관리 (Context 대신)
// shared-state/src/store.ts (별도 패키지)
class SharedStore {
private static instance: SharedStore;
private subscribers = new Map<string, Set<(value: unknown) => void>>();
private state: Record<string, unknown> = {};
static getInstance(): SharedStore {
if (!SharedStore.instance) {
SharedStore.instance = new SharedStore();
}
return SharedStore.instance;
}
get<T>(key: string): T | undefined {
return this.state[key] as T;
}
set<T>(key: string, value: T): void {
this.state[key] = value;
this.subscribers.get(key)?.forEach(fn => fn(value));
}
subscribe<T>(key: string, fn: (value: T) => void): () => void {
if (!this.subscribers.has(key)) {
this.subscribers.set(key, new Set());
}
this.subscribers.get(key)!.add(fn as (value: unknown) => void);
return () => this.subscribers.get(key)?.delete(fn as (value: unknown) => void);
}
}
// React hook으로 래핑
function useSharedState<T>(key: string, defaultValue: T): [T, (value: T) => void] {
const store = SharedStore.getInstance();
const [value, setValue] = useState<T>(store.get<T>(key) ?? defaultValue);
useEffect(() => {
return store.subscribe<T>(key, setValue);
}, [key]);
const setter = useCallback((newValue: T) => {
store.set(key, newValue);
}, [key]);
return [value, setter];
}
// 사용 (catalog-app)
const [cartCount, setCartCount] = useSharedState('cartCount', 0);
// 사용 (shell-app)
const [cartCount] = useSharedState('cartCount', 0);
// cart 변경 시 shell의 헤더도 자동 업데이트
5. 배포 전략
[마이크로프론트엔드 배포 아키텍처]
CDN (edge 캐싱)
├── shell.example.com/ → Shell App (React Shell)
├── shell.example.com/remotes.json → 원격 URL 동적 설정
│
├── catalog.example.com/
│ └── remoteEntry.js → Catalog Remote
│
├── checkout.example.com/
│ └── remoteEntry.js → Checkout Remote
│
└── auth.example.com/
└── remoteEntry.js → Auth Remote
배포 순서:
1. Remote Apps 먼저 배포 (하위 호환 API 유지)
2. remotes.json 업데이트 (새 URL 가리키기)
3. Shell App 배포 (선택적)
// shell.example.com/remotes.json (런타임 원격 URL 설정)
{
"catalog": "https://catalog.example.com/remoteEntry.js",
"checkout": "https://checkout.example.com/remoteEntry.js",
"auth": "https://auth.example.com/remoteEntry.js"
}
// Shell에서 동적으로 원격 URL 로드
async function loadRemoteConfig() {
const res = await fetch('/remotes.json');
return res.json();
}
// Module Federation 동적 원격 설정
import { init } from '@module-federation/runtime';
const remoteConfig = await loadRemoteConfig();
init({
name: 'shell',
remotes: Object.entries(remoteConfig).map(([name, entry]) => ({
name,
entry,
})),
});
6. 성능 고려사항
[마이크로프론트엔드 성능 최적화]
1. 공유 의존성 singleton 관리
- React, React DOM은 반드시 singleton
- 버전 불일치 시 두 번 로드됨 → 번들 크기 2배
2. 초기 로딩 최적화
- Shell App은 최소한으로 유지
- 각 Remote는 lazy load
- 중요 Remote만 prefetch
3. 캐싱 전략
- remoteEntry.js: 짧은 TTL (5분)
- 실제 청크: 긴 TTL + content hash
- 배포 시 remoteEntry.js만 무효화
4. 에러 격리
- 각 Remote가 ErrorBoundary로 격리
- Remote 실패 시 Shell은 계속 동작
마무리
Module Federation은 강력하지만 복잡성을 수반한다. 도입 전 반드시 팀 구성, 배포 독립성 필요도, DevOps 역량을 함께 고려해야 한다.
Module Federation 2.0의 TypeScript 타입 공유와 동적 원격 설정은 1.0의 주요 페인 포인트를 해결했다. 신규 마이크로프론트엔드 프로젝트라면 2.0부터 시작하는 것을 강력히 권장한다.