안녕하세요! 방탈출과 보드게임을 좋아하는 올리브영의 프론트엔드 개발자 애플🍎입니다.
디플롯 서비스에 마이크로프론트엔드 아키텍처를 적용할지 검토하면서 MFE(Micro Frontend, 마이크로프론트엔드)에 대해 본격적으로 깊이 파고들게 되었습니다.
이 시리즈는 그 과정에서 정리한 내용을 바탕으로 마이크로프론트엔드의 개념부터 구조, 장단점, PoC까지 한 번에 담아보려고 합니다.
이어지는 Part 2에서는 Module Federation PoC를, Part 3에서는 Nx를 활용한 마이크로프론트엔드 과정을 생생하게 소개해드릴 예정이니 많은 기대 부탁드립니다.
이번 Part 1에서는 마이크로프론트엔드 개념과 구조, 장단점에 대해 살펴보겠습니다!
목차
- 1.1 마이크로프론트엔드의 등장 배경
- 1.2 구현 예시
- 2.1 코드 분리의 한계
- 2.2 진정한 마이크로프론트엔드의 조건
1. 마이크로프론트엔드란?
마이크로프론트엔드는 웹 애플리케이션을 독립적으로 개발하고 배포하며 운영 가능한 작은 단위로 분할하는 아키텍처 패턴입니다. 이는 백엔드의 마이크로서비스 아키텍처(MSA) 개념을 프론트엔드 영역에 적용한 것으로 하나의 거대한 단일 페이지 애플리케이션(SPA) 대신 여러 개의 작은 애플리케이션들이 유기적으로 조합되어 전체 서비스를 구성하는 방식입니다.
1.1 마이크로프론트엔드의 등장 배경
기존의 모놀리식 프론트엔드 아키텍처는 애플리케이션의 규모가 커지면서 다음과 같은 문제점들을 나타내기 시작했습니다.
- 배포 의존성: 작은 변경사항도 전체 애플리케이션을 다시 빌드하고 배포해야 함
- 기술 스택 제약: 모든 팀이 동일한 프레임워크와 버전을 사용해야 함
- 코드 충돌: 여러 팀이 동시에 작업할 때 머지 충돌이 빈번하게 발생
- 느린 빌드 시간: 코드 베이스가 커질수록 빌드와 테스트 시간이 점점 증가
이러한 문제들을 해결하고 백엔드에서 이미 검증된 마이크로서비스의 장점을 프론트엔드에도 적용하고자 마이크로프론트엔드가 등장하게 되었습니다.
1.2 구현 예시 - 컨테이너 애플리케이션 기반 마이크로프론트엔드 통합
온라인 쇼핑몰을 예로 들면, 각 마이크로프론트엔드는 서로 다른 기술 스택을 사용할 수 있으며 독립적인 팀이 각각의 앱을 담당할 수 있습니다
- 헤더: React + TypeScript (내비게이션팀)
- 상품 영역: Vue.js (상품팀)
- 장바구니: Angular (주문팀)
- 결제: Svelte (결제팀)
이때 컨테이너 애플리케이션이 모든 마이크로프론트엔드를 통합하고 라우팅을 관리하게 됩니다. 각 팀은 자신들의 영역을 독립적으로 개발하고 배포할 수 있으면서도, 사용자 입장에서는 하나로 자연스럽게 연결된 애플리케이션처럼 인식되도록 구성할 수 있습니다.
이것이 마이크로프론트엔드가 지향하는 핵심 가치입니다.
2. 코드 분리로도 해결할 수 있지 않나요?
맞습니다. 하나의 코드베이스 내에서도 영역별로 코드를 잘 분리하고 팀 간 협업 구조를 마련하면 어느정도 유사한 구조를 만들 수는 있습니다.
하지만 이런 방식은 여전히 빌드, 배포, 기술 스택 통일 등의 제약이 존재하고 팀 간 완전한 독립성과 유연성은 확보하기 어렵습니다.
2.1 코드 분리의 한계
단순히 폴더 구조를 나누고 모듈을 분리하는 것만으로는 진정한 마이크로프론트엔드라고 할 수 없습니다. 이러한 수준의 분리는 잘 구조화된 모놀리식 애플리케이션에서도 충분히 구현할 수 있기 때문입니다.
잘 정리된 모놀리식 애플리케이션도 다음과 같은 특징을 가질 수 있어요.
- 명확한 폴더 구조와 모듈 분리
- 컴포넌트 기반의 재사용 가능한 아키텍처
- 팀별로 담당 영역이 구분된 코드베이스
하지만 이러한 모놀리식 구조는 여전히 다음과 같은 한계를 가져요.
- 단일 빌드 프로세스: 전체 애플리케이션이 함께 빌드되어야함
- 단일 배포 파이프라인: 일부분만 변경되어도 전체를 배포함
- 기술적 의존성: 모든 팀이 동일한 기술 스택을 사용해야함
- 팀 간 의존성: 한 팀의 작업이 다른 팀에 영향을 미칠 수 있음
2.2 진정한 마이크로프론트엔드의 조건
진정한 마이크로프론트엔드를 구현하려면 각 단위가 독립적인 생명주기(개발, 빌드, 배포)를 가질 수 있도록 하는 "통합 방식"이 필요합니다.
즉, 코드 분리뿐만 아니라 이 분리된 코드들을 언제, 어떻게 하나의 애플리케이션으로 통합할 것인지에 대한 전략이 있어야 비로소 마이크로프론트엔드의 진정한 가치를 실현할 수 있어요.
3. 마이크로프론트엔드 통합 방식
앞서 단순한 코드 분리만으로는 진정한 마이크로프론트엔드를 구현할 수 없다는 점을 살펴보았습니다. 그렇다면 여기서 말하는 "통합 방식"이란 구체적으로 무엇일까요?
통합 방식은 간단히 말해 작은 마이크로프론트엔드 코드 조각들을 언제, 어디서 하나의 애플리케이션으로 묶을지를 결정하는 전략입니다.
이 전략은 크게 두 가지 관점을 고려해야 합니다.
- 언제 통합하는가? 개발 시점에 미리 조립할 것인가, 아니면 사용자가 접속할 때 실시간으로 조립할 것인가?
- 어디서 통합하는가? 서버에서 조립할 것인가, 브라우저에서 조립할 것인가, 아니면 그 중간 지점에서 조립할 것인가?
3.1 통합 시점: 언제 조립할 것인가?
3.1.1 빌드 타임 통합
빌드 타임 통합은 개발할 땐 따로따로 작업하지만 배포할 땐 하나로 합쳐서 내보내는 방식입니다.
개발자들이 마이크로 앱을 독립적으로 만들어도 빌드 단계에서 하나의 번들로 코드가 통합됩니다. 이 덕분에 사용자는 앱이 제각각 만들어졌다는 사실을 알 수 없고, 완성된 하나의 앱 형태로 자연스럽게 사용할 수 있습니다.
빌드 타임 통합 방법
1) npm 패키지 방식: 의존성으로 관리하기
가장 간단한 빌드타임 통합 방법은 각 마이크로 앱을 npm 패키지로 만드는 것입니다. 메인 애플리케이션의 package.json에 다른 마이크로 앱들을 의존성으로 추가하면 빌드 시 번들러가 모든 코드를 가져와 하나의 결과물을 만들어냅니다.
// host-app/package.json
{
"dependencies": {
"@app/header-app": "^1.2.0",
"@app/product-app": "^2.1.0",
"@app/checkout-app": "^1.5.0"
}
}
이 방식은 기존 JavaScript 생태계의 패키지 관리 시스템을 그대로 활용할 수 있다는 점에서 장점이 있습니다. npm의 버전 관리나 의존성 해결 같은 기능도 그대로 사용할 수 있어 새로운 도구를 학습해야 하는 부담이 적습니다.
2) 모노레포 기반 통합
좀 더 정교한 방법은 Nx나 Turborepo 같은 모노레포 도구를 활용하는 방법입니다. 여러 마이크로 앱을 하나의 저장소에서 관리하면서도 각각을 독립적인 라이브러리로 취급할 수 있습니다.
모노레포 환경에서는 마이크로프론트엔드끼리 통합하는 과정이 직관적입니다.
예를 들어, 각 마이크로 앱이 libs
폴더에 독립 패키지로 존재하고 main-app
에서는 이들을 일반 npm 패키지처럼 가져다 쓸 수 있습니다.
workspace/
├─ apps/
│ └─ main-app // 메인 호스트 애플리케이션
├─ libs/
│ ├─ header-app // 헤더 마이크로 앱
│ ├─ product-app // 상품 마이크로 앱
│ └─ checkout-app // 결제 마이크로 앱
└─ nx.json
└─ package.json
// main-app/package.json
// main-app의 package.json에서 각 마이크로 앱들을 의존성으로 등록해줌
{
"name": "main-app",
"dependencies": {
"@workspace/header-app": "*",
"@workspace/product-app": "*",
"@workspace/checkout-app": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
// main-app/product/page.tsx
import { ProductComponent } from '@workspace/product';
export function ProductPage() {
return (
// 상품 페이지에 product-app 프로젝트 컴포넌트 렌더링
<div>
<h1>상품 페이지</h1>
<ProductComponent />
</div>
);
}
이렇게 설정하면 각 마이크로 앱은 독립적으로 개발되지만 최종 빌드 시에는 모든 코드가 하나의 번들로 통합됩니다. 이때 각 앱에서 공통으로 사용하는 라이브러리는 자동으로 중복 제거되어 최적화됩니다.
예를 들어, header-app
, product-app
, checkout-app
이 모두 React를 사용하더라도 최종 번들에는 React가 한 번만 포함됩니다.
빌드 타임 통합의 장점은 다음과 같습니다.
- 코드 공유와 일관성 확보
- 초기 로딩 성능 우수
- 타입 안정성 확보 (전체 코드가 같은 빌드 시점에 컴파일되기 때문)
단점은 다음과 같습니다.
- 전체 소스 코드를 함께 빌드해야 하므로 빌드 시간이 증가
물론, 모노레포 도구들의 원격 캐싱 기능을 사용하여 변경된 부분만 재빌드하는 시스템을 사용하여 빌드시간을 단축시킬 수 있긴 합니다.
- 모든 앱이 동일한 라이브러리 버전을 사용해야 하는 제약 (Single Version Policy)
Nx 공식 문서에서도 "Single Version Policy" 전략을 통해 모든 의존성 정의를 루트 package.json에 중앙화해서 코드베이스 전반에 걸쳐 일관된 버전을 보장해야 한다고 명시하고 있어요.
실제로 Nx GitHub 이슈에서도 개발자들이 "서로 다른 팀이 서로 다른 프로젝트를 소유하고 있을 때 그 중 하나가 breaking change가 있는 버전으로 업그레이드하면 모든 프로젝트에 강제 업그레이드가 발생한다. 이는 훨씬 큰 조직에서는 확장되지 않는다"라고 지적했어요.
3.1.2 런타임 통합: 실시간 조립 전략
런타임 통합은 각 마이크로 앱을 독립적으로 배포하고 사용자가 접속할 때 실시간으로 조합하는 방식입니다.
이 방식에서는 각 팀이 자신의 마이크로 앱을 독립적으로 개발하고 배포할 수 있습니다. 새로운 기능을 추가하거나 수정할 때 다른 팀의 영향을 받지 않는다는 장점이 있습니다.
런타임 통합 방법
1) Module Federation
Webpack 5의 Module Federation 기능은 런타임 통합을 대표하는 기술입니다. 이 방식에서는 호스트 애플리케이션이 리모트 애플리케이션을 런타임에 불러와 하나의 앱처럼 통합합니다.
Module Federation을 이해하려면 먼저 두 가지 핵심 개념을 알아야 합니다. 바로 호스트(Host) 애플리케이션과 리모트(Remote) 애플리케이션인데요.
호스트 애플리케이션은 말 그대로 다른 마이크로프론트엔드들을 불러와서 통합하는 메인 애플리케이션입니다.
// host-app/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
productApp: 'productApp@http://localhost:3001/remoteEntry.js',
cartApp: 'cartApp@http://localhost:3002/remoteEntry.js',
},
}),
],
};
반면 리모트 애플리케이션은 자신의 기능을 외부에 노출해서 다른 애플리케이션에서 사용할 수 있도록 하는 독립적인 마이크로프론트엔드 입니다.
// product-app/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'productApp',
filename: 'remoteEntry.js',// productApp이 자신의 컴포넌트들을 다른 앱들이 찾아서 사용할 수 있도록 하는 진입점 역할
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
},
}),
],
};
Module Federation의 장점은 다음과 같습니다.
- 각 팀의 기술적 독립성 보장
- 코드 충돌 방지
- 장애 격리
한 모듈에서 문제가 발생해도 다른 모듈들은 정상적으로 동작할 수 있어서 장애 격리 효과가 있어요.
- 점진적 현대화 가능
비즈니스 관점에서는 여러 팀이 병렬로 개발할 수 있어서 새로운 기능을 훨씬 빠르게 출시할 수 있고 레거시 시스템을 점진적으로 현대화할 수 있어요.
단점은 다음과 같습니다.
- 라이브러리 버전 충돌 위험
예를 들어, 호스트 애플리케이션에서는 React 17을 사용하고 있는데 리모트 애플리케이션에서 React 18을 사용한다면 런타임에서 두 버전의 React가 충돌하면서 예상치 못한 오류들이 발생해요. Context API가 제대로 동작하지 않거나, Hook이 이상하게 작동하거나 심지어 메모리 누수까지 발생할 수 있어요.
이러한 문제를 해결하기 위한 방법 중 하나가 바로 "싱글톤(singleton) 설정"입니다. 싱글톤 설정은 여러 애플리케이션 간에 하나의 라이브러리 인스턴스만 공유하도록 강제하는 설정입니다. 이를 통해 각 애플리케이션이 서로 다른 버전의 라이브러리를 사용하더라도 런타임에서는 하나의 버전만 로드되어 충돌을 방지합니다. - 성능 저하 가능성 (네트워크 지연, 런타임 오버헤드)
- 복잡한 배포 및 의존성 관리
- 타입 안정성 확보 어려움
2) Next.js Multi-zone: 경로 기반 통합
Next.js를 사용하는 조직이라면 Multi-Zone 아키텍처를 고려해볼 수 있습니다. 이는 여러 개의 독립적인 Next.js 애플리케이션을 경로 기반으로 통합하는 방식입니다.
// 호스트 앱의 next.config.js
module.exports = {
async rewrites() {
return [
{
source: "/product/:path*", // /product/ 경로 하위는 모두 상품 앱으로
destination: "http://localhost:3001/:path*", // 상품 앱 포트로 프록시
},
];
},
};
// 상품 앱의 next.config.js
module.exports = {
basePath: "/product", // 정적 리소스 및 라우팅의 base 경로 설정
};
사용자가 /product
경로에 접속하면 내부적으로 상품 앱으로 라우팅됩니다.
Next.js의 기본 기능만으로 구현할 수 있어 설정이 간단하지만 앱 간 이동 시 전체 페이지가 새로 로드되는 하드 탐색이 발생한다는 단점이 있습니다. (next/link를 사용해도 소프트 탐색이 되지않음)
어떤 방식을 선택해야 할까요?
그렇다면 빌드타임 통합과 런타임 통합 중 어떤것을 선택해야할까요?
어느 방식이 더 적합한지는 정해진 정답이 없다고 생각합니다. 프로젝트의 규모, 팀의 구조, 기술적 요건 등에 따라 다르게 결정되어야 합니다.
빌드타임 통합이 적합한 경우
- 소규모 팀이나 단일 팀에서 모든 마이크로 앱을 관리하는 경우
- 초기 로딩 성능이 중요한 경우
- 간단한 배포 및 설정을 선호하는 경우
- 마이크로 앱 간의 강한 결합이 가능한 경우
런타임 통합이 적합한 경우
- 여러 팀이 독립적으로 개발하고 배포해야 하는 경우
- 각 마이크로 앱의 배포 주기가 다른 경우
- 기술적 독립성이 필요한 경우
- 대규모 조직에서 팀 간 의존성을 최소화하려는 경우
결국 마이크로프론트엔드의 통합 방식 선택은 기술적 고려사항과 조직적 요구사항의 균형을 맞추는 것입니다. 현재의 팀 구조와 프로젝트 요구사항을 분석한 후 가장 적합한 방식을 선택해야합니다.
3.2 통합 위치: 어디서 조립할 것인가?
마이크로프론트엔드 아키텍처를 도입할 때 또 하나의 중요한 결정은 “어디서 통합을 수행할 것인가” 입니다. 이 선택에 따라 애플리케이션의 성능과 사용자 경험, 그리고 인프라 비용이 크게 달라집니다.
통합 위치에 따라 크게 세 가지 방식으로 나눌 수 있습니다.
3.2.1 서버 측 구성(SSR)
서버 측 구성은 사용자가 페이지를 요청하면 서버에서 각 마이크로프론트엔드 조각들을 가져와 하나로 조립한 다음 완성된 HTML을 브라우저로 보내는 방식입니다.
Podium이나 Tailor 같은 도구들이 지원합니다.
이 방식의 장점은 빠른 초기 로딩과 SEO 최적화입니다. 사용자는 이미 완성된 페이지를 받기 때문에 첫 화면을 빠르게 볼 수 있습니다.
하지만 모든 조립 작업이 서버에서 일어나다 보니 서버 부하가 커지고 캐싱 전략도 복잡해집니다. 실시간 인터랙션이 많은 앱에서는 한계가 있을 수 있습니다.
3.2.2 클라이언트 측 구성(CSR)
클라이언트 측 구성은 브라우저에서 필요한 마이크로 앱을 동적으로 로드하고 조합합니다.
Module Federation 같은 도구들이 지원합니다. Module Federation은 런타임에 모듈을 동적으로 공유할 수 있게 해줍니다.
이 방식의 장점은 동적이고 인터랙티브한 UX를 제공할 수 있습니다. 필요한 부분만 선택적으로 로드할 수 있어 효율적이고 서버 부하도 줄일 수 있어요.
하지만 초기 로딩이 느릴 수 있고 SEO 최적화도 별도 작업이 필요합니다.
3.2.3 에지 측 구성(Edge Computing)
에지 측 구성은 여러 개의 독립적인 마이크로 앱을 에지(Edge) 네트워크에서 조합하여 사용자에게 최종 페이지를 전달하는 방법입니다.
Cloudflare Workers나 Lambda@Edge 같은 플랫폼이 지원합니다. 이들은 전 세계에 분산된 서버 네트워크를 활용해 사용자와 가까운 지점에서 코드를 실행할 수 있게 해줍니다.
이 방식의 장점은 전 세계 어디서든 빠른 응답 속도를 제공할 수 있다는 점입니다. 서버 부하도 분산되고 클라이언트 성능에 덜 의존적입니다.
하지만 에지 컴퓨팅 인프라가 필요하고 분산된 환경에서의 디버깅과 모니터링이 복잡해집니다. 또한 상대적으로 높은 인프라 비용이 발생할 수 있습니다.
마치며
지금까지 마이크로프론트엔드 개념과 구조, 장단점에 대해 함께 살펴보았습니다!
마이크로프론트엔드는 분명 대규모 프론트엔드 애플리케이션의 복잡성을 효과적으로 관리하고 팀의 자율성을 높이는 데 유용한 강력한 아키텍처입니다. 독립적인 배포, 기술 스택 선택의 자유, 팀 간 의존성 최소화 등 다양한 장점을 갖추고 있습니다.
하지만 동시에 새로운 복잡성과 고려해야 할 요소들도 수반됩니다. 버전 관리의 어려움, 런타임 오류 위험, 증가된 인프라 복잡도 그리고 무엇보다 조직 전체의 기술적 성숙도가 뒷받침되어야 한다는 점은 현실적인 제약으로 작용합니다. 결국 모든 문제를 해결해주는 만병통치약은 아니라는 생각이 들었습니다.
특히 도입을 검토할 때는 현재 겪고 있는 구체적인 문제부터 명확히 정의하는 것이 중요합니다. 단순히 최신 기술이라서 다른 회사에서 성공했다는 이유만으로 도입한다면 예상보다 훨씬 큰 비용을 치를 수 있습니다. 팀 규모, 도메인 복잡성, 조직 문화, 인프라 역량 등을 종합적으로 고려한 후 점진적으로 접근하는 것이 현명한 전략이라고 생각합니다.
마이크로프론트엔드를 처음 접하시는 분들이나 도입을 고민하고 계신 분들에게 조금이라도 도움이 되었기를 바랍니다.
시리즈
- 대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 1. 마이크로프론트엔드 너 뭐야?
- 대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 2. 모듈 페더레이션 PoC (발행 예정)
- 대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 3. Nx를 활용한 마이크로프론트엔드 (발행 예정)
이어서 공개할 Part 2에서는 Module Federation PoC를, Part 3에서는 Nx를 활용한 마이크로프론트엔드 내용을 자세히 다룰 예정입니다. PoC처럼 시리즈물도 팀원들이 모여 단단히 준비했으니 많은 관심과 기대 부탁드립니다.
긴 글 읽어주셔서 감사합니다!☺️