마이크로 프론트엔드 거버넌스 실전 — Module Federation 계약·번들·소유권 자동화
마이크로 프론트엔드(MFE)를 도입하고 처음 몇 달은 정말 좋습니다. 팀마다 독립 배포하고, 기술 스택도 자유롭고, 서로 발목 잡는 일도 없어 보입니다. 저도 처음엔 이게 답이라고 생각했었습니다. 그런데 어느 순간 슬랙에 이런 메시지가 올라옵니다. "결제 팀이 React 18 올렸는데 우리 앱이 왜 터지는 거죠?"
MFE 아키텍처가 규모를 가지면 필연적으로 두 번째 문제가 등장합니다. 바로 거버넌스입니다. 각 팀이 완전한 자율을 가지면 의존성 충돌, UI 파편화, 번들 폭증이 일어나고, 반대로 중앙에서 모든 걸 통제하면 MFE를 도입한 이유 자체가 사라집니다. 이 글에서는 그 균형점을 어떻게 잡을 수 있는지, 실제 도구와 코드를 통해 살펴보겠습니다.
이 글의 핵심은 계약·번들·소유권이라는 세 가지 경계를 자동화로 내재화하면, 팀 자율성을 건드리지 않고도 대규모 분산 프론트엔드를 안정적으로 운용할 수 있다는 것입니다.
핵심 개념
거버넌스란 결국 "자동화된 경계"다
솔직히 말하면, 초기에 많은 팀이 "거버넌스 위원회를 만들자"는 방향으로 가다가 실패합니다. 사람이 매번 검토하는 방식은 팀이 늘어날수록 병목이 되거든요. 결국 모두가 게이트키퍼를 우회하는 방법을 찾게 됩니다. 플랫폼 팀이 게이트키퍼가 아닌 인프라 제공자 역할을 맡고, 거버넌스 규칙은 리뷰 보드 대신 빌드 파이프라인·타입 시스템·린트 규칙 안에 녹아 있어야 합니다.
거버넌스 = 인적 프로세스 × 0 + 자동화 × n 팀이 계약을 위반하면 빌드가 깨지고, 번들이 기준을 넘으면 PR이 차단됩니다. 규칙은 사람이 기억하는 게 아니라 도구가 강제합니다.
이 원칙을 중심으로, 실무에서 실제로 효과 있었던 네 가지 축을 차례로 살펴보겠습니다. 계약 → 공유 의존성 → 번들 예산 → 소유권 가시화 순서로 각각 개념과 코드를 한 묶음으로 다룹니다.
계약(Contract): 팀 간의 유일한 공식 언어
MFE 거버넌스의 첫 번째 축은 **경계 계약(boundary contract)**입니다. Remote 모듈이 무엇을 노출하고, Host가 무엇을 기대하는지를 명시적으로 정의하는 것이죠.
Module Federation 구조에서 Remote는 기능을 노출하는 앱이고, Host는 그것을 불러다 쓰는 앱입니다. 아래는 Remote 모듈이 타입을 자동 생성·배포하고, Host가 그 타입을 자동으로 소비하는 Module Federation 3.0 설정 예시입니다.
// apps/payment/webpack.config.ts (Remote 쪽 — 타입을 생성해서 배포)
import { ModuleFederationPlugin } from '@module-federation/enhanced';
export default {
plugins: [
new ModuleFederationPlugin({
name: 'payment',
filename: 'remoteEntry.js',
exposes: {
'./Checkout': './src/components/Checkout',
'./usePaymentStatus': './src/hooks/usePaymentStatus',
},
dts: {
generateTypes: true,
tsConfigPath: './tsconfig.json',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};// apps/shell/webpack.config.ts (Host 쪽 — Remote 타입을 자동으로 내려받음)
import { ModuleFederationPlugin } from '@module-federation/enhanced';
export default {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
payment: 'payment@https://payment.example.com/remoteEntry.js',
},
dts: {
consumeTypes: true, // Remote 타입 선언을 중복 작성할 필요 없음
},
}),
],
};Module Federation 3.0의 dts 옵션 덕분에 Host 팀이 Remote의 인터페이스를 수동으로 복붙하던 시대는 끝났습니다. Remote가 타입을 바꾸면 Host의 빌드가 컴파일 에러를 냅니다. 런타임 폭탄이 정적 분석 단계로 앞당겨지는 거죠. 실무에서 처음 이 옵션을 켰을 때 "왜 이걸 진작 안 썼나" 싶었습니다.
공유 의존성 정책: 싱글톤이냐 아니냐
저도 처음엔 헷갈렸는데, MFE에서 가장 자주 발생하는 충돌이 바로 공유 의존성 문제입니다. React를 각 Remote가 따로 번들하면 페이지에 React 인스턴스가 여러 개 뜨면서 훅이 깨집니다. 공유하면 버전 고착 문제가 생깁니다.
| 전략 | 적합한 경우 | 주의점 |
|---|---|---|
singleton: true |
React, React-DOM처럼 인스턴스가 하나여야 하는 경우 | 버전 협상 필요, 한 팀의 업그레이드가 전체에 영향 |
singleton: false |
유틸 라이브러리, 독립 실행 가능한 경우 | 중복 번들 → 페이지 크기 증가 |
| 외부화 + CDN | 안정적인 메이저 버전 라이브러리 | CDN 장애 시 단일 실패 지점 |
이 정책은 Module Federation 도입 전에 반드시 명시적으로 결정해야 합니다. 나중에 바꾸면 모든 Remote 설정을 다 건드려야 해서 의외로 고통스럽습니다.
Contract Testing: CI가 계약 위반을 먼저 잡는다
실무에서 자주 맞닥뜨리는 상황인데, API 문서는 바뀌지 않았는데 Remote가 조용히 인터페이스를 변경하는 경우가 있습니다. Pact 기반 계약 테스트를 CI에 연결하면 이런 상황을 사전에 차단할 수 있습니다.
Contract Testing이란? 소비자(Consumer)와 제공자(Provider)가 서로 합의한 인터페이스 사양을 독립적으로 검증하는 테스트 방식입니다. E2E 테스트보다 빠르고, 양쪽이 동시에 배포되지 않아도 계약 유효성을 확인할 수 있습니다.
아래 예시는 shell(Host)이 payment Remote의 Checkout 컴포넌트가 내부적으로 호출하는 HTTP API의 계약을 검증하는 코드입니다. Pact는 HTTP 레이어의 소비자-제공자 계약 도구이므로, 컴포넌트의 props 타입이 아니라 그 컴포넌트가 의존하는 API 응답 형식을 보장합니다.
// payment-remote.pact.spec.ts (Consumer 쪽 계약 정의)
// 이 테스트는 Checkout 컴포넌트가 내부에서 호출하는
// /api/order/:id 엔드포인트의 응답 계약을 검증합니다.
import { Pact } from '@pact-foundation/pact';
const provider = new Pact({
consumer: 'shell',
provider: 'payment',
port: 4000,
});
describe('payment/Checkout — 내부 API 계약', () => {
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
it('orderId로 주문 정보를 조회할 수 있어야 한다', async () => {
await provider.addInteraction({
state: '유효한 주문이 존재함',
uponReceiving: 'orderId로 주문 상세 조회 요청',
withRequest: {
method: 'GET',
path: '/api/order/abc-123',
},
willRespondWith: {
status: 200,
body: { orderId: 'abc-123', amount: 15000, currency: 'KRW' },
},
});
// ... Checkout 컴포넌트 렌더링 후 API 호출 결과 검증
});
});도입 장벽이 있는 건 사실입니다. 처음 셋업할 때 Pact Broker 구성까지 포함하면 하루 이상 걸리기도 하거든요. 그래도 Remote 배포 직후 Host가 조용히 깨지는 경험을 한 번만 해보면, 설득이 따로 필요 없어집니다.
실전 적용
예시 1: 번들 크기 CI 게이트로 성능 거버넌스 자동화
팀별 번들 크기 기준이 없으면 각 MFE가 독립적으로 무거워져 전체 페이지 성능이 합산 악화됩니다. Bundlemon을 CI에 연결하면 PR 단위로 번들 기준 초과 여부를 자동으로 차단할 수 있습니다.
# .github/workflows/bundle-governance.yml
name: Bundle Size Gate
on: [pull_request]
jobs:
check-bundle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- name: 빌드
run: pnpm build
- name: 번들 크기 검증
uses: nickvdyck/bundlemon-action@v1
with:
files: |
[
{ "path": "dist/remoteEntry.js", "maxSize": "50kb" },
{ "path": "dist/assets/*.js", "maxSize": "200kb", "compression": "gzip" }
]
env:
BUNDLEMON_PROJECT_ID: ${{ secrets.BUNDLEMON_PROJECT_ID }}| 설정 항목 | 역할 |
|---|---|
maxSize |
허용 최대 번들 크기. 초과 시 PR 차단 |
compression: "gzip" |
실제 전송 크기 기준으로 측정 |
BUNDLEMON_PROJECT_ID |
팀별 기준을 프로젝트 단위로 별도 관리 |
이 게이트 하나로 "우리 팀 번들이 왜 이렇게 커졌지?" 라는 회고 시간을 CI가 대신하게 됩니다. 처음 기준값이 뭔지 모르겠다면, 현재 크기에 20% 여유를 더한 값으로 시작해도 됩니다.
예시 2: Backstage로 MFE 소유권·의존성 가시화
팀이 늘어날수록 "이 Remote 의존하고 있는 팀이 어디어디야?"라는 질문을 슬랙에서 찾는 시간이 쌓입니다. 각 MFE가 catalog-info.yaml 하나를 저장소에 추가하면, Backstage가 자동으로 소유팀·의존성·상태를 인덱싱해 포털에서 검색할 수 있게 됩니다.
# apps/payment/catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payment-mfe
description: 결제 흐름 마이크로 프론트엔드
tags:
- micro-frontend
- react
annotations:
backstage.io/techdocs-ref: dir:.
mfe/exposed-modules: 'Checkout, usePaymentStatus'
mfe/singleton-deps: 'react@^18.0.0, react-dom@^18.0.0'
mfe/bundle-budget: '50kb'
spec:
type: micro-frontend
lifecycle: production
owner: team-payments
dependsOn:
- component:order-mfe
- component:design-system
providesApis:
- payment-checkout-api# catalog-info.yaml — API 계약 선언
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: payment-checkout-api
spec:
type: openapi
lifecycle: production
owner: team-payments
definition:
$text: ./openapi/checkout.yamlmfe/exposed-modules, mfe/bundle-budget 같은 커스텀 annotation은 MFE 전용 메타데이터입니다. 실무에서 써보니 "어떤 팀이 이 Remote를 의존하고 있나?"를 포털에서 바로 확인하게 되니, 영향 범위를 파악하는 속도가 눈에 띄게 달라집니다.
Backstage 설치가 부담스럽다면 같은 형식의 YAML 파일을 저장소에 두는 것부터 시작해도 됩니다. 인프라 없이 Markdown으로 시작하고, 규모가 커질 때 Backstage를 붙이는 방식도 충분히 현실적입니다.
예시 3: Import Maps로 싱글톤 버전 중앙 제어
Module Federation이 부담스러운 환경이라면 브라우저 네이티브 Import Maps를 거버넌스 레이어로 활용하는 방법도 있습니다. 외부 빌드 도구 없이 브라우저 수준에서 모듈 해석 경로를 중앙 제어할 수 있습니다.
<!-- index.html — 플랫폼 팀이 관리하는 Import Map -->
<script type="importmap">
{
"imports": {
"react": "https://cdn.example.com/react@18.3.1/index.js",
"react-dom": "https://cdn.example.com/react-dom@18.3.1/index.js",
"@company/design-system": "https://ds.example.com/v2.5.0/index.js"
}
}
</script>
<!-- 각 MFE는 이 맵을 기반으로 import -->
<script type="module">
import React from 'react'; // Import Map이 버전을 결정
import { Button } from '@company/design-system';
</script>버전을 바꾸고 싶으면 HTML 한 줄만 수정하면 됩니다. 각 팀의 빌드 설정을 건드릴 필요가 없어서 싱글톤 버전 정책 적용이 매우 명시적입니다. 단, CDN 장애가 단일 실패 지점이 된다는 점은 운영 전에 반드시 고려해야 합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 독립 배포 | 팀이 타 팀 릴리즈 사이클에 의존하지 않고 독자 배포가 가능합니다 |
| 기술 자율성 | 도메인에 최적화된 프레임워크·라이브러리 선택이 허용됩니다 |
| 책임 경계 명확화 | 계약으로 인터페이스가 공식화되어 오너십이 명확해집니다 |
| 점진적 마이그레이션 | 레거시 일부를 교체하면서 나머지를 운영할 수 있습니다 |
| 거버넌스 자동화 | 규칙이 빌드 파이프라인에 녹아 있어 팀이 늘어도 오버헤드가 선형적으로 증가하지 않습니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 공유 의존성 딜레마 | 공유하면 버전 고착, 공유 안 하면 번들 폭증 | 싱글톤 정책을 명시적으로 선결하고 Import Maps 또는 MF shared 설정으로 강제 |
| 계약 드리프트 | Remote 실제 인터페이스와 문서·타입이 서로 벌어짐 | MF 3.0 타입 자동 생성 + Pact 계약 테스트를 CI 게이트로 편입 |
| 디자인 파편화 | 공통 컴포넌트가 "모두의 것"이 되면 아무도 책임지지 않음 | 디자인 시스템 소유팀을 명시하고 Storybook + Chromatic으로 시각적 회귀 테스트 |
| 초기 투자 비용 | 거버넌스 체계 수립에 상당한 선행 투자 필요 | 팀 5개 이상, 독립 배포 니즈가 실제로 충돌할 때 도입 검토 |
| 인프라 복잡도 | 배포 파이프라인, 모니터링, 개발 환경이 모두 복잡해짐 | Backstage 포털로 가시화, OpenTelemetry로 MFE 단위 트레이싱 |
계약 드리프트(Contract Drift)란? 실제 구현과 공식 계약(API 사양, 타입 정의)이 시간이 지나면서 서로 어긋나는 현상입니다. MFE 환경에서는 이 드리프트가 다른 팀의 런타임 오류로 직결되기 때문에, 단순한 문서 관리 문제가 아닙니다.
실무에서 가장 흔한 실수
-
거버넌스를 리뷰 보드로 구현하는 것 — 사람이 매번 검토하는 방식은 팀이 늘수록 병목이 되고, 결국 모두가 게이트키퍼를 우회하는 방법을 찾게 됩니다. 규칙은 CI/CD 안에 자동화로 존재해야 합니다.
-
공유 의존성 정책을 나중으로 미루는 것 — Module Federation 설정을 먼저 올리고 나중에
singleton: true여부를 결정하는 팀이 많습니다. 나중에 바꾸면 모든 Remote 설정을 다 건드려야 하는데, 이게 예상보다 훨씬 번거롭습니다. -
팀 규모가 작을 때 MFE 거버넌스 전체를 적용하는 것 — 팀이 3개 이하이거나 독립 배포 니즈가 실제로 충돌하지 않는다면, 모노레포 + 공유 패키지 전략이 더 실용적입니다. Feature-Sliced Design 커뮤니티도 MFE 도입 적정 시점을 팀 약 5개 이상부터로 보고 있습니다.
마치며
글을 시작하게 된 슬랙 메시지가 기억납니다. 결국 그 장애의 원인은 계약도, 번들 예산도, 소유권도 모두 사람 머릿속에만 있었기 때문이었습니다. 어떤 팀이 무엇을 expose하고, 어떤 버전의 React를 써야 하는지를 코드와 파이프라인이 알지 못하면, 장애는 반복됩니다.
지금 바로 시작해볼 수 있는 3단계:
-
계약 가시화부터 시작해볼 수 있습니다. 현재 팀 간에 어떤 Remote 모듈이 오가는지 목록을 만들고,
catalog-info.yaml형태로 소유팀과 exposed 모듈을 문서화해두시면 좋습니다. Backstage 설치 없이 단순 Markdown 파일로 시작해도 충분합니다. -
번들 예산 게이트를 CI에 하나 추가하는 것을 권장합니다. Bundlemon 또는
webpack-bundle-analyzer+ 커스텀 스크립트로 각 Remote의remoteEntry.js크기를 PR마다 측정하는 스텝을 추가해보시면 됩니다. 기준은 현재 크기 + 20% 여유로 시작해도 넉넉합니다. -
공유 의존성 정책 문서를 팀 내에 한 장으로 남겨두는 것이 나중을 위해 큰 도움이 됩니다. "무엇을 singleton으로 공유할지, 버전 업그레이드 cadence는 어떻게 할지"를 ADR(Architecture Decision Record) 형태로 기록해두면, Remote가 늘어났을 때 같은 논쟁을 반복하지 않아도 됩니다.
참고 자료
- Need micro-frontend benefits at scale? Reimagine the operating model | McKinsey & Company
- Micro-Frontends: Are They Still Worth It in 2025? | Feature-Sliced Design
- Micro-frontends 2026: Module Federation 3.0 & Native ESM Federation | weskill.org
- 대규모 프론트엔드 아키텍처의 새로운 패러다임 | 올리브영 테크블로그
- Micro Frontends | Martin Fowler
- Enabling Typed Micro-Frontend Architectures | arXiv (2025)
- Backstage by Spotify — The Ultimate Guide 2026 | Roadie.io
- Practical micro frontends: How we orchestrated multiple frameworks with single-spa | Nutrient
- Micro-Frontends: a Sociotechnical Journey | InfoQ
- Maximizing the value of CX modernization with micro frontends | McKinsey