1. 서론: Micro Frontends 환경에서 마주한 구조적 결합도 문제
1.1 MFE 도입의 역설: 분산된 모놀리스(Distributed Monolith)
현대적인 웹 애플리케이션 규모가 커짐에 따라 Micro Frontends(MFE)는 팀 단위의 독립적 배포와 기술 스택의 자율성을 보장하는 핵심 전략이 되었습니다. 하지만 실제 프로젝트 운영 단계에서 직면한 가장 큰 과제는 아키텍처의 파편화와 그로 인한 의존성 관리의 모호함이었습니다.
MFE가 제공하는 '배포의 자유'가 '코드의 무질서'로 변질되는 것은 한순간입니다. Shell App과 Remote App이 물리적으로 분리되어 있음에도 불구하고, 명확한 인터페이스 규칙이 부재할 경우 모듈 간 'Circular Dependency(순환 참조)'나 'Tight Coupling(강결합)'이 발생하기 쉽습니다.
1.2 엔지니어링 관점에서의 페인 포인트(Pain Point)
제가 현재 프로젝트를 처음 마주했을 때 발견한 가장 심각한 문제는, 특정 기능 하나를 수정하거나 위치를 옮기려고 할 때 연관된 로직들이 거대한 타래처럼 줄줄이 딸려오는 현상이었습니다. 명목상 앱을 분리해 두었으나, 내부 코드는 오히려 기존의 모놀리스보다 더 결합도가 높아진 '기형적 구조'가 되어 있었습니다. 이러한 구조에서는 코드의 통일성을 유지하기가 불가능에 가깝고, 신규 팀원이 합류했을 때 시스템 전체를 이해하는 데 과도한 비용이 발생합니다.
2. 거시적 구조 설계: FSD(Feature-Sliced Design) 원칙의 이해
애플리케이션 전반의 통제권을 확보하기 위해 제가 채택한 해답은 Feature-Sliced Design(FSD)입니다. FSD는 단순히 폴더를 나누는 가이드라인을 넘어, 각 모듈의 관심사를 수직적 레이어로 격리하여 단방향 의존성을 강제하는 강력한 설계 규격입니다.
2.1 계층 간의 엄격한 단방향 의존성 (일방통행 도로)
FSD의 가장 중요한 원칙은 레이어가 오직 자신보다 '아래'에 있는 레이어만 사용할 수 있다는 점입니다. 모든 레이어는 피라미드처럼 쌓여 있으며, 의존성은 오직 위에서 아래로만 흐릅니다.
- app (최상층): 애플리케이션의 뼈대와 전역 설정
- pages (페이지 단위): 실제 라우팅되는 화면
- widgets (조립된 UI): 여러 기능을 조합한 독립적 화면 조각
- features (사용자 기능): 구체적인 비즈니스 로직 단위
- entities (비즈니스 로직): 도메인의 핵심 실체
- shared (최하층): 도메인과 무관한 공통 자산
예를 들어, features 레이어는 자신보다 아래에 있는 entities와 shared를 자유롭게 사용할 수 있지만, 절대 위쪽에 있는 widgets나 pages의 존재를 알아서는 안 됩니다.
2.2 수평적 격리: 슬라이스 간 교차 참조 금지 원칙
FSD의 또 다른 핵심은 동일한 레이어 내에 위치한 '슬라이스(Slice) 간의 교차 참조를 엄격히 금지(Cross-import ban)'한다는 것입니다. 예를 들어, features 레이어 내부에 존재하는 add-to-cart 기능은 같은 레이어에 있는 update-user-profile 기능의 내부 로직이나 상태를 직접 참조해서는 안 됩니다.
각 슬라이스는 높은 응집도(Cohesion)와 낮은 결합도(Coupling)를 유지하며 완벽히 격리된 모듈로 동작해야 합니다. 이러한 수평적 격리 원칙 덕분에, 특정 비즈니스 기능을 수정하거나 완전히 시스템에서 들어내더라도(Removal) 다른 기능에 사이드 이펙트가 전파되는 것을 원천적으로 차단할 수 있습니다.
기능 간의 통신이나 조합이 필요한 경우에는 하위 레이어에서 직접 참조하는 대신, 상위 레이어인 widgets나 pages에서 이들을 불러와 조합(Composition)하는 방식으로만 해결해야 합니다.
3. 실무 아키텍처의 고도화: 현재 프로젝트에 맞춘 FSD 커스터마이징
원본 FSD의 철학을 기반으로 하되, 실제 운영 중인 대규모 마이크로 프론트엔드 환경의 특성에 맞게 아키텍처를 재구성(Customizing)했습니다. 현재 시스템은 내부 관리자용 앱(Admin/Staff)과 외부 고객용 앱(Client)으로 물리적 분리가 되어 있으며, 이를 효과적으로 통합하기 위해 아래와 같은 레이어 매핑을 적용했습니다.
3.1 원본 FSD vs 실무 적용 구조 매핑
| FSD 레이어 | 실무 적용 구조 (경로) | 역할 및 특징 |
|---|---|---|
| app | apps/app | 앱의 메인 진입점. 전역 상태, Provider 연동 |
| pages | apps/pages | 도메인별 라우팅 및 페이지 컴포넌트 |
| widgets | apps/widgets | Layout, Route Guard 등 조합된 공통 UI |
| features | apps/features | 특정 기능을 수행하는 Container 조합 계층 |
| entities | view & stub | 순수 UI/Model 및 백엔드 엔드포인트 격리 |
| shared | libraries/shared | 모든 Apps/모듈에서 쓰이는 공통 자원 총집합 |
3.2 세그먼츠(Segments)와 서브 피처(Sub-feature)의 제어
각 슬라이스 내부에서는 역할을 세분화하기 위해 ui, model, lib, hooks 등의 세그먼츠(Segments)를 둡니다. 특히 model 세그먼트는 타입과 상태를 선언적으로 관리하며, 뷰 컴포넌트(ui)와 로직(hooks)을 명확히 분리합니다.
또한, 도메인이 깊어질 경우 Sub-feature를 허용하되, 파일 시스템의 혼란을 막기 위해 동일 레이어 내의 공통 세그먼트에는 _를 붙여(_ui, _model 등) 상위 디렉토리임을 명시적으로 지정했습니다. (예: pages/_ui/Router.tsx와 pages/admin/)
4. MFE 통합을 위한 거시적 조립: App, Pages, Widgets의 방파제 역할
MFE 환경의 가장 큰 딜레마는, 독립적으로 개발된 하위 모듈(Remote)들을 결국 하나의 최상위 껍데기(Shell) 안에서 묶어내야 한다는 점입니다. 이 조립 과정에서 인증, 레이아웃, 라우팅 처리가 하위 비즈니스 로직과 뒤섞이면 구조는 다시 모놀리스로 회귀하게 됩니다.
이를 방지하기 위해 FSD의 상위 레이어인 App, Pages, Widgets를 결합도를 낮추는 조립의 공간이자 방파제로 활용했습니다.
4.1 의존성 주입의 최상단: apps/app
MFE 시스템에서 App 레이어는 각 앱의 진입점이자, 하위 레이어들이 필요로 하는 환경(Context)을 주입하는 역할만을 수행합니다. 인증 체계, 다국어 처리, 테넌트 정보 동기화 같은 전역 상태들은 오직 이 계층의 Provider에서만 관리됩니다.
덕분에 하위 레이어인 features나 entities는 자신이 어떤 앱(내부망/외부망)에 마운트되어 있는지 알 필요 없이 오직 본연의 기능에만 집중할 수 있습니다.
4.2 로직 없는 라우팅과 조립: apps/pages & apps/widgets
MFE의 라우팅 레이어(Pages)는 철저하게 비즈니스 로직을 배제했습니다. Pages는 자신이 직접 데이터를 가공하지 않고, 하위의 Features나 이를 조합한 Widgets를 URL에 매핑하는 역할만 합니다.
예를 들어 특정 관리자 화면을 렌더링할 때, 권한 체크 로직을 페이지 내부나 컴포넌트에 하드코딩하지 않습니다. 대신 apps/widgets에 있는 RoleCheckLayout과 AdminLayout 같은 구조적 위젯으로 감싸서(Wrapping) 조립합니다.
이러한 상위 계층의 철저한 역할 분리 덕분에, 하위 모듈들은 MFE 쉘의 복잡한 라우팅 규칙이나 권한 상태에 종속되지 않고 언제든 떼어 다른 곳에 붙일 수 있는 진정한 마이크로 모듈로 거듭날 수 있었습니다.
5. 미시적 설계의 진화: View와 Container의 철저한 관심사 분리
거시적인 레이어링(FSD)이 하향식 의존성을 잡아주었다면, 실제 코드를 구현하는 단계에서는 컴포넌트 레벨에서의 결합도를 끊어내기 위해 View와 Container의 역할 분리를 고안했습니다.
5.1 기존 구조의 한계와 비효율성 분석
과거 구조에서는 View 모듈 내부에서 API를 직접 호출하고 데이터 가공 로직까지 함께 구현했습니다. 이는 다음과 같은 치명적인 문제를 야기했습니다.
- 재사용성 원천 차단: 화면 레이아웃이 완벽히 동일한 리스트 컴포넌트여도, 데이터를 불러오는 API 엔드포인트가 다르면 결국 새로운 컴포넌트를 복사-붙여넣기 해서 만들어야 했습니다.
- 레이어 경계 침범: 특정 View 모듈이 특정 API 스텁(Stub)에 직접 강결합되면서, 서로 다른 앱 간의 물리적 경계가 모호해지고 의존성 그래프가 엉키는 현상이 발생했습니다.
5.2 현재 아키텍처에서의 해결책: Dramas와 Features의 역할 분담
이 문제를 해결하기 위해 컴포넌트를 '껍데기(Pure View)'와 '알맹이(Logic Container)'로 완전히 분리했으며, 이는 현재 프로젝트의 dramas와 features 폴더 구조에 정확히 반영되어 있습니다.
- Pure View (dramas/view): 오직 "어떻게 보이는가"에만 집중하는 프레젠테이션 계층입니다.
- 이곳의 ui 세그먼트는 서버나 API의 존재를 전혀 모르는 Stateless 컴포넌트입니다. 모든 데이터와 이벤트 핸들러는 Props로만 주입받습니다.
- _api 디렉토리에서는 순수하게 useQuery, useMutation 등을 정의하여 데이터를 가져오는 행위 자체만 명시합니다.
- Logic Container (apps/features): 이 기능의 심장부이자 지휘자(Orchestrator)입니다.
- dramas/view에서 만들어진 순수 컴포넌트들을 불러오고, dramas/stub에서 백엔드 API 로직을 끌어옵니다.
- 여기서 데이터를 가공하여 View 계층의 Props로 내려주는 단방향 제어 구조(Container → View)를 완성합니다.
이 완벽한 블랙박스 구조 덕분에 강력한 유연성이 탄생했습니다. 이제 목적에 따라 dramas/view에서 특정 UI 껍데기만 가져와서, apps/features에서 상황에 맞는 전혀 다른 비즈니스 로직(Container)을 결합해 재사용하는 것이 가능해졌습니다.
6. 결론: 기술 부채를 넘어서는 지속 가능한 프론트엔드 아키텍처
MFE의 겉모양만 흉내 내는 것이 아니라, 내부 복잡성을 실질적으로 통제하기 위해 시작된 이 치열한 고민은 결국 세 가지 핵심 축으로 완성되었습니다.
- MFE를 통한 물리적 배포 독립성
- FSD를 통한 거시적인 단방향 의존성 강제
- View-Container 패턴을 통한 미시적 관심사 분리
단순히 폴더를 예쁘게 나누는 것을 넘어선 이 구조적 규칙은, 프로젝트가 점차 거대해지고 수많은 도메인이 추가되는 상황에서도 코드가 엉키지 않도록 지탱해 주는 튼튼한 뼈대가 되었습니다.
모든 로직과 컴포넌트가 '약속된 위치'에 존재하기 때문에, 새로운 팀원이 합류하더라도 가이드라인 안에서 안전하게 모듈을 조립하고(Composition) 일관된 품질의 코드를 생산할 수 있습니다. 앞으로도 끊임없이 변화하는 비즈니스 요구사항에 대응하며, 개발자 모두가 레고 블록을 맞추듯 즐겁고 안전하게 코딩할 수 있는 프론트엔드 생태계를 지속해서 고도화해 나갈 것입니다.
참고 자료:
- Feature-Sliced Design 공식 문서: https://feature-sliced.design/kr/