Skip to Content
Software Architecture엔터프라이즈 MFE에서 호스트와 앱의 경계
🏗️ Software Architecture2026년 1월 9일

엔터프라이즈 MFE에서 호스트와 앱의 경계

#frontend#micro-frontend#module-federation#architecture#react

Micro Frontend를 처음 들으면 “프론트엔드를 여러 개로 나누는 것” 정도로 이해하기 쉽다. 틀린 말은 아니지만, 실제로 중요한 것은 파일을 몇 개로 나누느냐가 아니라 어떤 기능을 어떤 경계로 나눠서 독립적으로 개발하고 배포할 수 있게 만들 것인가다.

이 글은 MFE로 구성된 애플리케이션에 기능을 추가하다가 구조를 다시 공부하게 된 경험에서 출발했다. 처음에는 단순히 특정 화면 하나를 수정하면 될 줄 알았는데, 실제로는 호스트 셸, 공용 인증, navigation, remote 앱 경계를 이해하지 않으면 어디를 건드려야 하는지조차 판단하기 어려웠다.

그래서 이 글에서는 특정 조직의 내부 구현을 일반화해서, 엔터프라이즈 환경에서 MFE를 설계할 때 호스트 셸과 도메인 앱이 어떤 책임을 나눠 가져야 하는지 정리한다. 먼저 MFE가 무엇인지와 런타임에서 어떻게 동작하는지부터 살펴보고, 그 다음에 호스트 셸, 도메인 앱, shared dependency, 독립 배포 전략으로 이어 간다.

MFE는 무엇인가

MFE는 하나의 거대한 프론트엔드 애플리케이션을 기능 경계에 따라 여러 개의 독립적인 앱으로 나누고, 이를 하나의 사용자 경험처럼 조합하는 방식이다. 백엔드에서 마이크로서비스가 기능 경계를 나누듯, 프론트엔드에서도 배포와 ownership 경계를 나누려는 시도라고 볼 수 있다.

단, MFE는 “화면을 여러 개의 iframe으로 쪼개는 것”과는 다르다. 보통은 사용자가 하나의 앱처럼 느끼되, 내부적으로는 여러 팀이 각자 맡은 영역을 따로 개발하고 배포할 수 있게 만드는 쪽에 가깝다.

관점단일 SPAMFE
배포 단위하나의 큰 번들기능별 앱 단위
ownership중앙 프론트엔드 팀에 몰리기 쉽다도메인 팀이 각 앱을 소유하기 쉽다
공통 의존성 변경전체 앱에 동시에 영향공유 전략에 따라 영향 범위를 줄일 수 있다
운영 복잡도비교적 단순하다경계와 공유 정책을 잘못 잡으면 복잡도가 크게 올라간다

즉, MFE는 “무조건 최신 프론트엔드 아키텍처”가 아니라, 여러 팀과 여러 배포 리듬이 실제로 존재할 때 선택할 만한 구조다.

왜 엔터프라이즈 프론트엔드는 MFE를 고민하게 되는가

하나의 관리 플랫폼을 여러 팀이 함께 운영하기 시작하면, 단일 SPA는 빠르게 병목이 된다. 배포 주기가 서로 다른 기능이 같은 번들에 묶이고, 공통 의존성 변경이 전체 릴리스에 영향을 주며, 한 팀의 변경이 다른 팀의 테스트 범위를 넓혀 버린다.

이때 MFE가 매력적으로 보이는 이유는 명확하다.

  • 팀별로 기능을 독립 배포할 수 있다
  • 기능 단위 ownership이 분명해진다
  • 전체 앱을 한 번에 다시 배포하지 않아도 된다

하지만 MFE는 자동으로 분리와 독립을 만들어 주지 않는다. 어떤 책임을 호스트에 남기고, 어떤 책임을 각 앱으로 내려보낼지를 먼저 정하지 않으면 운영 복잡도만 늘어난다.

MFE의 실패 패턴은 대개 기술이 아니라 경계에서 시작된다. 호스트 셸이 너무 많은 기능을 가져가거나, 반대로 각 앱이 공통 책임까지 제각각 구현하면 독립성보다 결합도가 더 커진다.

런타임에서는 어떻게 동작하는가

개념만 보면 MFE는 추상적으로 느껴질 수 있다. 하지만 런타임 흐름으로 보면 생각보다 단순하다. 사용자는 하나의 URL로 진입하고, 호스트 셸이 현재 위치와 권한 맥락에 맞는 도메인 앱을 로드해서 화면 안에 조합한다.

다이어그램 로딩 중...

이 그림에서 핵심은 호스트 셸이 모든 비즈니스 기능을 직접 처리하지 않는다는 점이다. 셸은 “어떤 앱을 어떤 맥락에서 보여 줄지”를 관리하고, 실제 도메인 기능은 각 앱이 담당한다.

좀 더 순서대로 풀어 보면 보통 이런 흐름이다.

사용자가 호스트 셸로 진입한다

브라우저는 하나의 진입 URL로 들어오고, 먼저 호스트 셸이 로드된다.

호스트 셸이 공통 컨텍스트를 확인한다

현재 로그인 상태, 권한, 라우트 정보, 공통 레이아웃 구성을 확인한다.

셸이 필요한 도메인 앱을 로드한다

현재 URL과 화면 맥락에 맞는 도메인 앱을 런타임에 가져온다.

도메인 앱이 자신의 기능을 렌더링한다

각 앱은 자신이 담당하는 화면, 상태, API 호출, 오류 처리를 수행한다.

이 구조를 이해하면 MFE가 단순히 “여러 저장소”가 아니라, 하나의 공통 진입점 위에 여러 기능 앱이 붙는 방식이라는 감각이 생긴다.

MFE 연결을 가능하게 하는 Module Federation

여기서 자주 함께 등장하는 기술이 Module Federation이다. 이 기능은 Webpack 5에서 도입됐고, 서로 따로 빌드된 프론트엔드 번들끼리 모듈을 런타임에 공유할 수 있게 해 준다. 쉽게 말해 host 앱이 다른 앱의 컴포넌트를 “배포 이후”에 가져와 붙일 수 있게 만드는 장치다.

MFE와 Module Federation은 같은 개념은 아니다.

  • MFE는 프론트엔드 경계와 배포 방식을 나누는 아키텍처 접근이다
  • Module Federation은 그 경계를 런타임에 연결해 주는 구현 기술 중 하나다

실무에서는 보통 아래 세 가지 설정이 핵심이 된다.

설정역할
remoteshost가 어떤 remote 앱을 어디서 가져올지 정의한다
exposesremote가 외부에 어떤 컴포넌트나 모듈을 공개할지 정의한다
sharedReact 같은 공통 라이브러리를 어떤 버전 정책으로 함께 쓸지 정의한다

이렇게 보면 Module Federation은 “MFE를 만든다”기보다, host와 remote 사이의 런타임 계약을 구현한다고 이해하는 편이 정확하다. 바로 아래 코드 예시에서 import("orders/OrdersPage") 같은 문법이 가능한 이유도, 결국 이 계약이 뒤에서 성립하고 있기 때문이다.

코드로 보면 host와 remote는 이렇게 연결된다

MFE를 처음 접할 때 가장 궁금한 지점은 보통 이것이다. “그래서 호스트 셸이 도메인 앱을 실제로 어떻게 불러오는데?” 개념적으로는 런타임 로딩이라고 설명할 수 있지만, 코드 한 번 보는 편이 훨씬 빠르다.

먼저 호스트 셸 쪽에서는 remote 앱을 import해서 화면 안에 렌더링한다.

tsx
sample/host-shell-routes.tsx
import { Suspense, lazy } from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; const OrdersPage = lazy(() => import("orders/OrdersPage")); const PaymentsPage = lazy(() => import("payments/PaymentsPage")); export default function AppShell() { return ( <BrowserRouter> <Routes> <Route path="/orders/*" element={ <Suspense fallback={<div>Loading orders app...</div>}> <OrdersPage /> </Suspense> } /> <Route path="/payments/*" element={ <Suspense fallback={<div>Loading payments app...</div>}> <PaymentsPage /> </Suspense> } /> </Routes> </BrowserRouter> ); }

여기서 orders/OrdersPagepayments/PaymentsPage 같은 경로는 로컬 파일 import가 아니라, Module Federation이 런타임에 연결해 주는 remote 모듈이라고 이해하면 된다.

그럼 remote 앱 쪽에서는 무엇을 내보내야 할까. 보통은 호스트가 붙일 수 있는 엔트리 컴포넌트를 하나 export해 둔다.

tsx
sample/orders/OrdersPage.tsx
import { Routes, Route, Link } from "react-router-dom"; function OrdersList() { return ( <div> <h2>Orders</h2> <Link to="/orders/42">Open order 42</Link> </div> ); } function OrderDetail() { return <div>Order detail page</div>; } export default function OrdersPage() { return ( <Routes> <Route index element={<OrdersList />} /> <Route path=":orderId" element={<OrderDetail />} /> </Routes> ); }

즉, 호스트는 “어떤 앱을 어디에 붙일지”를 알고 있고, remote 앱은 “붙여졌을 때 자기 내부 라우트와 기능을 어떻게 렌더링할지”를 알고 있는 구조다.

이 연결을 가능하게 하는 설정은 대략 이런 형태가 된다.

js
sample/module-federation.host.config.js
new ModuleFederationPlugin({ name: "shell", remotes: { orders: "orders@https://cdn.example.com/orders/remoteEntry.js", payments: "payments@https://cdn.example.com/payments/remoteEntry.js", }, });
js
sample/module-federation.remote.config.js
new ModuleFederationPlugin({ name: "orders", filename: "remoteEntry.js", exposes: { "./OrdersPage": "./src/OrdersPage", }, });

이 예시만 보면 host와 remote의 역할이 꽤 분명해진다.

  • host는 remote 위치와 진입 컴포넌트를 안다
  • remote는 외부에 노출할 엔트리 포인트를 정한다
  • 실제 도메인 기능은 remote 내부에서 계속 확장된다

그래서 MFE를 설계할 때 중요한 것은 import 문법 자체보다, 어디까지를 host가 알고 어디부터를 remote가 책임질지를 먼저 합의하는 일이다.

호스트 셸은 어디까지 맡아야 하는가

호스트 셸은 보통 “모든 것을 조금씩 하는 앱”이 아니라, 플랫폼 공통 책임만 담당하는 얇은 런타임 계층이어야 한다. 대표적으로는 아래 네 가지가 여기에 들어간다.

영역호스트 셸이 맡는 이유
라우팅 진입점URL과 각 도메인 앱의 연결은 중앙에서 일관되게 관리해야 한다
공통 레이아웃헤더, 사이드바, 브레드크럼 같은 프레임은 앱마다 다시 구현하지 않는 편이 낫다
인증 경계로그인 상태, 세션 확인, 접근 제어는 모든 앱이 공통으로 의존한다
런타임 로딩어떤 앱을 언제 어떤 버전으로 불러올지 결정하는 책임은 셸에 있다

여기서 중요한 점은 호스트 셸이 “공통 기능”과 “도메인 기능”을 혼동하지 않는 것이다. 예를 들어 검색 박스는 모든 화면에 보인다고 해서 반드시 셸 책임은 아니다. 그 기능이 특정 도메인에 종속된다면, 셸이 아니라 해당 앱이 소유해야 한다.

공용 인증과 공용 nav는 왜 MFE에 잘 맞을까

MFE에서 특히 공용화 효과가 큰 영역은 인증과 전역 navigation이다. 이 둘은 거의 모든 도메인 앱이 공통으로 의존하고, 사용자 입장에서도 하나의 일관된 경험으로 느껴져야 하기 때문이다.

예를 들어 공용 인증을 host나 shared 계층에 두면 각 도메인 앱은 이런 고민을 덜 하게 된다.

  • 로그인 여부를 앱마다 따로 확인하지 않아도 된다
  • 토큰 갱신이나 세션 만료 처리를 반복 구현하지 않아도 된다
  • 권한 맥락을 공통 방식으로 받을 수 있다

공용 nav도 비슷하다. 헤더, 사이드바, 브레드크럼을 각 앱이 따로 가지면 도메인 앱은 자기 기능보다 프레임을 맞추는 데 더 많은 비용을 쓰게 된다. 반대로 host가 이를 책임지면 각 앱은 “이 화면 안에서 무엇을 할 수 있는가”에 더 집중할 수 있다.

아래처럼 역할을 나눠 보면 감각이 더 잘 온다.

계층맡는 역할
Host Shell로그인 상태 확인, 토큰 갱신, 전역 nav, 공통 레이아웃, 앱 로딩
Orders MFE주문 목록, 주문 상세, 상태 변경
Payments MFE결제 조회, 환불, 정산 화면
Users MFE사용자 목록, 권한 관리, 조직별 설정

이 구조가 좋은 이유는 공용 기능이 “모든 앱의 기반” 역할을 하고, 도메인 앱은 “자기 기능”에만 집중할 수 있기 때문이다.

간단한 개념 예시로 보면 host는 공용 인증과 nav를 먼저 렌더링하고, 그 아래에 현재 도메인 앱을 붙이는 식이 된다.

tsx
sample/app-shell-layout.tsx
import { Suspense, lazy } from "react"; const OrdersPage = lazy(() => import("orders/OrdersPage")); function AuthGate({ children }: { children: React.ReactNode }) { return <>{children}</>; } function GlobalNavigation() { return ( <nav> <a href="/orders">Orders</a> <a href="/payments">Payments</a> <a href="/users">Users</a> </nav> ); } export default function AppShell() { return ( <AuthGate> <GlobalNavigation /> <Suspense fallback={<div>Loading app...</div>}> <OrdersPage /> </Suspense> </AuthGate> ); }

이 예시에서 핵심은 OrdersPage가 인증 자체를 다시 구현하거나, 전역 nav를 직접 만들지 않는다는 점이다. host가 공통 기반을 제공하고, 도메인 앱은 그 위에서 자기 기능만 렌더링한다.

공용 인증과 공용 nav는 MFE에서 가장 자연스러운 shared 영역이다. 반대로 주문 규칙, 환불 정책, 사용자 권한 편집처럼 도메인 의미가 강한 로직은 각 앱 안에 두는 편이 보통 더 낫다.

그래서 호스트 셸과 도메인 앱의 경계가 중요하다

MFE를 도입한 뒤 가장 많이 생기는 문제는, 호스트 셸이 비대해지거나 도메인 앱이 공통 책임까지 가져가는 것이다. 결국 경계가 흐려지면 독립 배포라는 장점이 빠르게 줄어든다.

그래서 설계 초반에 아래 질문을 먼저 던지는 편이 좋다.

  • 이 책임은 모든 앱이 공통으로 의존하는가
  • 아니면 특정 기능이 있을 때만 필요한가
  • 이 변경은 플랫폼 전체 릴리스와 함께 움직여야 하는가
  • 아니면 특정 앱만 따로 배포돼도 되는가

이 질문에 답하다 보면, 무엇이 셸 책임이고 무엇이 도메인 앱 책임인지 조금씩 선명해진다.

각 도메인 앱은 무엇을 소유해야 하는가

도메인 앱은 자신이 담당하는 사용자 흐름과 상태, API 연동, 화면 조합을 소유해야 한다. 다시 말해 “이 기능을 잘 동작하게 만드는 데 필요한 것”은 도메인 앱 안에 있어야 한다.

대표적으로는 이런 책임이 들어간다.

  • 도메인별 라우트와 화면
  • 기능에 종속된 상태 관리
  • 해당 기능이 호출하는 API와 데이터 가공
  • 기능 전용 에러 처리와 fallback

반대로 도메인 앱이 소유하지 않는 편이 좋은 것도 있다.

  • 전역 인증 상태
  • 앱 간 공통 레이아웃
  • 조직 전체에서 공유하는 디자인 토큰
  • 호스트가 이미 관리하는 런타임 로딩 규칙

이 경계를 잘못 잡으면, 각 앱이 전역 상태를 중복으로 들고 있게 되거나, 호스트 셸이 도메인 로직까지 품게 된다. 둘 다 독립 배포의 장점을 약하게 만든다.

공통 UI와 shared dependency는 어떻게 다뤄야 할까

엔터프라이즈 MFE에서 가장 쉽게 꼬이는 영역 중 하나가 shared dependency다. React, React DOM, 상태 관리 라이브러리, 디자인 시스템 컴포넌트처럼 여러 앱이 함께 쓰는 라이브러리는 공유 전략이 필요하다.

단순하게 보면 선택지는 세 가지다.

방식장점주의할 점
런타임 공유중복 로딩을 줄이고 플랫폼 일관성을 만들기 쉽다버전 충돌이 생기면 셸과 앱이 함께 깨질 수 있다
빌드 시 각 앱 포함앱 독립성이 커진다번들 크기와 중복이 증가한다
공통 UI 패키지 별도 배포설계 의도를 중앙에서 유지하기 쉽다패키지 버전 업데이트가 전체 앱 릴리스 리듬에 영향을 줄 수 있다

실무에서는 보통 핵심 런타임 라이브러리는 제한적으로 공유하고, 공통 UI는 별도 패키지로 두되, 도메인별 상태와 비즈니스 로직은 각 앱이 들고 가는 식으로 균형을 맞춘다.

아래 코드는 Module Federation에서 shared dependency를 어떻게 생각하면 되는지 보여 주는 개념 예시다.

js
sample/module-federation.shared.config.js
module.exports = { shared: { react: { singleton: true, requiredVersion: "^18.0.0" }, "react-dom": { singleton: true, requiredVersion: "^18.0.0" }, "@company/design-system": { singleton: true }, }, };

이 설정은 단순히 “한 번만 로드하자”가 아니다. 실제 의미는 플랫폼 전체가 이 버전 정책에 함께 묶인다는 것이다. 그래서 shared dependency는 기술 설정이 아니라 운영 정책에 가깝다.

독립 배포는 장점이지만 롤백 전략도 함께 필요하다

MFE를 도입하면 각 앱을 따로 배포할 수 있다는 장점이 강조된다. 그런데 운영 관점에서 보면 더 중요한 것은 “문제가 생겼을 때 어디까지 되돌릴 수 있는가”다.

독립 배포를 제대로 활용하려면 최소한 아래 질문에 답할 수 있어야 한다.

  1. 호스트 셸과 앱의 배포 순서가 충돌하면 어떻게 할 것인가
  2. 새 앱 버전이 로드되지 않을 때 이전 버전으로 안전하게 되돌릴 수 있는가
  3. shared dependency 변경이 여러 앱에 동시에 영향을 줄 때 롤백 단위는 무엇인가
  4. 특정 앱 장애가 전체 셸을 깨뜨리지 않도록 fallback UI가 준비되어 있는가

이 단계에서 흔히 놓치는 것은 “독립 배포”와 “독립 장애”는 다르다는 점이다. 앱을 따로 배포한다고 해서 장애도 자동으로 격리되지는 않는다. 런타임 로딩, 공통 인증, shared dependency가 얽혀 있으면 특정 앱 문제도 전체 경험을 무너뜨릴 수 있다.

MFE를 선택하지 않는 편이 더 나은 경우도 있다

MFE는 팀 구조와 제품 구조가 정말로 분리돼 있을 때 가장 힘을 발휘한다. 반대로 이런 경우라면 오히려 단일 SPA가 더 나을 수 있다.

  • 배포 리듬이 사실상 모두 비슷한 경우
  • 팀 간 경계보다 공통 화면이 훨씬 많은 경우
  • shared dependency가 너무 많아 런타임 독립성이 낮은 경우
  • 프론트엔드보다 백엔드 도메인 경계가 더 중요한 경우

즉, MFE는 “모놀리식 프론트엔드를 무조건 쪼개자”는 전략이 아니라, 배포 독립성과 조직 경계를 어디까지 프론트엔드에 반영할 것인가에 대한 선택이다.

결국 중요한 것은 기술보다 책임의 분리다

호스트 셸은 플랫폼 공통 책임에 집중하고, 각 도메인 앱은 자신의 기능과 배포 리듬을 소유해야 한다. 이 경계를 명확히 하지 않으면 MFE는 독립성을 얻기보다 새로운 운영 복잡도를 만든다.

그래서 엔터프라이즈 MFE를 설계할 때 가장 먼저 해야 할 일은 remote 설정을 쓰는 방법을 익히는 것이 아니라, 다음 질문에 답하는 것이다.

  • 무엇이 플랫폼 공통 책임인가
  • 무엇이 도메인 기능인가
  • 무엇을 공유할 것이고 무엇을 공유하지 않을 것인가

이 세 가지가 선명해지면, MFE는 기술 선택이 아니라 구조적 선택으로 보이기 시작한다.

Last updated on