마이크로 프론트엔드 구현: 모듈 페더레이션, iFrame, 웹 컴포넌트 심층 분석
Daniel Hayes
Full-Stack Engineer · Leapcell

서론
현대 웹 개발 환경은 확장 가능하고 유지 관리 가능하며 독립적으로 배포 가능한 애플리케이션 구축에 대한 강조가 커지면서 끊임없이 진화하고 있습니다. 프론트엔드 애플리케이션의 복잡성과 팀 규모가 커짐에 따라 모놀리식 아키텍처는 민첩성과 개발자 생산성을 저해하는 병목 현상이 되는 경우가 많습니다. 이러한 과제는 마이크로서비스의 원칙을 프론트엔드로 확장하는 패러다임인 마이크로 프론트엔드 아키텍처의 채택을 이끌었습니다. 대규모 프론트엔드 애플리케이션을 작고 자율적인 단위로 분할함으로써 마이크로 프론트엔드는 더 큰 유연성, 더 빠른 개발 주기, 개선된 팀 자율성을 약속합니다. 그러나 마이크로 프론트엔드를 위한 올바른 구현 전략을 선택하는 것이 중요합니다. 이 글에서는 모듈 페더레이션, iFrame, 웹 컴포넌트라는 세 가지 주요 기술을 철저히 탐구하고, 접근 방식, 사용 사례를 비교하여 이 중요한 결정에 도움을 드릴 것입니다.
마이크로 프론트엔드의 핵심 개념
각 기술의 세부 사항을 자세히 살펴보기 전에, 논의 전반에 걸쳐 참조될 마이크로 프론트엔드 아키텍처와 관련된 몇 가지 핵심 개념에 대한 공통된 이해를 정립해 봅시다.
마이크로 프론트엔드: 웹 애플리케이션이 독립적으로 개발, 배포 및 관리할 수 있는 많은 독립적인 프론트엔드 애플리케이션으로 구성되는 아키텍처 스타일입니다.
호스트 애플리케이션: 다양한 마이크로 프론트엔드를 조정하고 통합하는 메인 애플리케이션입니다. 전체 사용자 경험을 위한 셸 또는 레이아웃을 제공합니다.
원격 애플리케이션 (또는 자식 마이크로 프론트엔드): 호스트 애플리케이션 내에서 로드되고 표시되는 독립적인 프론트엔드 애플리케이션입니다.
격리: 마이크로 프론트엔드가 다른 마이크로 프론트엔드나 호스트에 의도치 않게 영향을 미치거나 영향을 받지 않고 독립적으로 작동하는 정도입니다. 여기에는 JavaScript, CSS 및 전역 상태의 격리가 포함됩니다.
런타임 통합: 애플리케이션이 브라우저에서 실행되는 동안 호스트 애플리케이션 내에서 마이크로 프론트엔드를 로드하고 표시하는 프로세스입니다.
빌드 타임 통합: 마이크로 프론트엔드가 배포 전에 빌드 단계에서 함께 결합되고 번들링되는 프로세스입니다.
공유 종속성: 여러 마이크로 프론트엔드에서 필요할 수 있는 공통 라이브러리 또는 프레임워크(예: React, Vue, Lodash)이며, 성능 저하를 피하기 위해 이상적으로는 한 번만 로드되어야 합니다.
모듈 페더레이션
Webpack 5와 함께 도입된 모듈 페더레이션은 독립적으로 빌드된 애플리케이션 간에 코드와 종속성을 공유하는 문제를 해결하기 위해 설계된 강력하고 비교적 새로운 기능입니다. 다양한 Webpack 빌드가 런타임에 서로의 모듈을 노출하고 소비할 수 있도록 합니다.
작동 방식
모듈 페더레이션의 핵심은 "호스트" 애플리케이션이 "원격" 애플리케이션에서 코드를 동적으로 로드할 수 있도록 하는 것입니다. 호스트 및 원격 애플리케이션은 본질적으로 이러한 역할을 수행하도록 구성된 Webpack 빌드입니다. 원격 애플리케이션은 특정 모듈을 "페더레이션된 모듈"로 노출하며, 호스트 애플리케이션은 이를 로컬 모듈처럼 소비할 수 있습니다. 이를 위해 중요한 것은 "공유 모듈"의 개념으로, React나 디자인 시스템과 같은 공통 종속성을 분리하여 여러 페더레이션된 모듈이 이를 필요로 하더라도 한 번만 로드할 수 있습니다.
구현 예시
호스트 애플리케이션에서 원격 ProductApp
에서 ProductDetail
컴포넌트 를 로드해야 하는 시나리오를 생각해 보겠습니다.
원격 (ProductApp
의 webpack.config.js
):
const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; module.exports = { mode: 'development', entry: './src/index.js', output: { publicPath: 'http://localhost:8081/', // This app is served from the Public URL }, devServer: { port: 8081, }, plugins: [ new ModuleFederationPlugin({ name: 'productApp', filename: 'remoteEntry.js', exposes: { './ProductDetail': './src/components/ProductDetail', }, shared: { react: { singleton: true, requiredVersion: '^17.0.2' }, 'react-dom': { singleton: true, requiredVersion: '^17.0.2' } }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], // ... other Webpack configurations };
호스트 (HostApp
의 webpack.config.js
):
const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; module.exports = { mode: 'development', entry: './src/index.js', output: { publicPath: 'http://localhost:8080/', // This app is served from the Public URL }, devServer: { port: 8080, }, plugins: [ new ModuleFederationPlugin({ name: 'hostApp', remotes: { productApp: 'productApp@http://localhost:8081/remoteEntry.js', }, shared: { react: { singleton: true, requiredVersion: '^17.0.2' }, 'react-dom': { singleton: true, requiredVersion: '^17.0.2' } }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], // ... other Webpack configurations };
호스트 (HostApp
의 src/App.js
):
import React, { Suspense } from 'react'; const ProductDetail = React.lazy(() => import('productApp/ProductDetail')); const App = () => { return ( <div> <h1>Host Application</h1> <Suspense fallback={<div>Loading Product Detail...</div>}> <ProductDetail productId="123" /> </Suspense> </div> ); }; export default App;
이 예시에서 HostApp
은 런타임에 ProductApp
에서 ProductDetail
컴포넌트를 동적으로 로드합니다. React와 같은 공유 종속성은 한 번만 로드되도록 최적화됩니다.
애플리케이션 시나리오
모듈 페더레이션은 다음과 같은 시나리오에서 탁월합니다.
- 대규모의 복잡한 컴포넌트 또는 전체 하위 애플리케이션을 공유해야 할 때.
- 공유 라이브러리의 최적화된 로드가 성능에 중요할 때.
- 팀이 프론트엔드 개발 및 배포에 대해 높은 수준의 자율성을 필요로 할 때.
- 프레임워크가 서로 다른 마이크로 프론트엔드 간에 동일하거나 호환될 때(모듈 페더레이션은 일반적으로 래퍼를 사용하여 프레임워크 차이를 잘 처리합니다).
- 여러 팀의 기능을 결합하는 단일 페이지 애플리케이션(SPA)을 구축할 때.
iFrames
iFrame (인라인 프레임)은 기존 HTML 요소로, 현재 HTML 문서 내에 다른 HTML 문서를 포함할 수 있습니다. 각 iFrame은 자체 브라우저 컨텍스트 내에서 작동하기 때문에 높은 수준의 격리를 제공합니다.
작동 방식
iFrame은 본질적으로 별도의 브라우징 컨텍스트를 생성합니다. 이는 iFrame 내의 JavaScript, CSS 및 로컬 저장소가 부모 문서 및 다른 iFrame에서 완전히 격리됨을 의미합니다. 부모와 자식 (또는 자식 간) 간의 통신은 일반적으로 서로 다른 출처 간에 메시지를 안전하게 전송하는 postMessage
API에 의존합니다.
구현 예시
호스트 (index.html
):
<!DOCTYPE html> <html> <head> <title>Host App with iFrame</title> </head> <body> <h1>Host Application</h1> <iframe id="productIframe" src="http://localhost:8082/product-app.html" style="width: 100%; height: 400px; border: 1px solid blue;" ></iframe> <script> const iframe = document.getElementById('productIframe'); // Listen for messages from the iframe window.addEventListener('message', (event) => { if (event.origin !== 'http://localhost:8082') { // Verify origin for security return; } console.log('Message from iframe:', event.data); // Example: Update host UI based on iframe message }); // Send message to the iframe after it loads iframe.onload = () => { iframe.contentWindow.postMessage({ type: 'INIT_DATA', payload: { userId: 'abc' } }, 'http://localhost:8082'); }; </script> </body> </html>
원격 (product-app.html
http://localhost:8082
에서 실행):
<!DOCTYPE html> <html> <head> <title>Product App (in iFrame)</title> </head> <body> <h2>Product Details</h2> <div id="product-info">Loading...</div> <script> const productInfoDiv = document.getElementById('product-info'); // Listen for messages from the parent window window.addEventListener('message', (event) => { if (event.origin !== 'http://localhost:8080') { // Verify origin return; } console.log('Message from host:', event.data); if (event.data.type === 'INIT_DATA') { productInfoDiv.innerHTML = ``; } }); // Example: Send a message to the host setTimeout(() => { window.parent.postMessage({ type: 'PRODUCT_LOADED', payload: { productId: '456' } }, 'http://localhost:8080'); }, 2000); </script> </body> </html>
애플리케이션 시나리오
iFrame은 다음과 같은 경우에 적합합니다.
- 서드 파티 콘텐츠 또는 잠재적으로 안전하지 않은 애플리케이션을 포함하는 경우와 같이 최대 격리가 필요한 경우.
- 서로 다른 기술 스택으로 구축되고 간섭 없이 독립적으로 작동해야 하는 서로 다른 컴포넌트.
- 기존 애플리케이션을 최신 셸에 포함해야 할 때.
- 엄격한 환경 분리가 필요한 보안상의 우려가 있을 때 (예: 결제 게이트웨이).
- 포함된 콘텐츠에 대해 SEO가 주요 관심사가 아닐 때 (검색 엔진이 과거에 iFrame 콘텐츠로 어려움을 겪었지만 이것은 개선되었습니다).
- 여러 브라우저 컨텍스트의 성능 오버헤드가 허용될 때.
웹 컴포넌트
웹 컴포넌트는 개발자가 사용자 정의 가능하고, 재사용 가능하며, 캡슐화된 HTML 태그를 만들 수 있도록 하는 W3C 표준 세트입니다. 다양한 프레임워크 및 기본 JavaScript에서 사용할 수 있는 모듈식 컴포넌트를 구축할 수 있는 네이티브 방법을 제공합니다.
작동 방식
웹 컴포넌트는 네 가지 주요 기술로 구성됩니다.
- 사용자 정의 요소: 새 HTML 태그를 정의할 수 있습니다.
- Shadow DOM: 구성 요소를 메인 문서의 스타일 및 스크립트에서 격리하여 캡슐화된 DOM 및 스타일을 제공합니다.
- HTML 템플릿: 인스턴스화될 때까지 렌더링되지 않는 마크업 조각을 선언할 수 있습니다.
- ES 모듈: 모듈을 가져오고 내보내기 위해.
마크업과 스타일 모두에 대한 강력한 캡슐화를 제공하므로 컴포넌트의 내부 구조와 스타일이 외부 CSS에 영향을 주거나 영향을 받지 않으며 그 반대도 마찬가지입니다. 통신은 일반적으로 표준 DOM 이벤트 및 속성/특성을 통해 이루어집니다.
구현 예시
사용자 정의 요소 정의 (product-card.js
):
class ProductCard extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); // Create a shadow DOM const template = document.createElement('template'); template.innerHTML = ` <style> .card { border: 1px solid #ccc; padding: 16px; margin: 16px; border-radius: 8px; font-family: sans-serif; } h3 { color: #333; } .price { font-weight: bold; color: green; } </style> <div class="card"> <h3></h3> <p class="description"></p> <p class="price"></p> <button>Add to Cart</button> </div> `; this.shadowRoot.appendChild(template.content.cloneNode(true)); this.titleElement = this.shadowRoot.querySelector('h3'); this.descriptionElement = this.shadowRoot.querySelector('.description'); this.priceElement = this.shadowRoot.querySelector('.price'); this.button = this.shadowRoot.querySelector('button'); this.button.addEventListener('click', () => { this.dispatchEvent(new CustomEvent('add-to-cart', { detail: { productId: this.getAttribute('product-id') }, bubbles: true, composed: true // Allows event to pass through shadow DOM boundary })); }); } // Define observed attributes static get observedAttributes() { return ['product-id', 'title', 'description', 'price']; } // Respond to attribute changes attributeChangedCallback(name, oldValue, newValue) { if (name === 'title') { this.titleElement.textContent = newValue; } else if (name === 'description') { this.descriptionElement.textContent = newValue; } else if (name === 'price') { this.priceElement.textContent = `$${parseFloat(newValue).toFixed(2)}`; } } } // Define the custom element customElements.define('product-card', ProductCard);
호스트 (index.html
):
<!DOCTYPE html> <html> <head> <title>Host App with Web Components</title> <script type="module" src="./product-card.js"></script> </head> <body> <h1>Host Application</h1> <div id="product-list"> <product-card product-id="P001" title="Super Widget" description="The best widget you'll ever own." price="29.99" ></product-card> <product-card product-id="P002" title="Mega Gadget" description="A revolutionary device for modern living." price="99.00" ></product-card> </div> <script> document.getElementById('product-list').addEventListener('add-to-cart', (event) => { console.log('Product added to cart:', event.detail.productId); // Handle cart logic in the host application }); </script> </body> </html>
애플리케이션 시나리오
웹 컴포넌트는 다음과 같은 경우에 가장 적합합니다.
- 다른 프로젝트, 팀 및 프레임워크에 걸쳐 공유할 수 있는 재사용 가능한 UI 컴포넌트를 만들기 위해.
- 프레임워크에 구애받지 않는 컴포넌트 개발을 달성하기 위해.
- iFrame의 오버헤드 없이 강력한 UI 및 스타일 캡슐화가 필요할 때.
- 개별 컴포넌트를 매우 이식 가능하고 유지 관리 가능하게 만들어야 하는 디자인 시스템을 구축할 때.
- 부분적으로 다른 프레임워크로 구축된 하이브리드 애플리케이션 (예: React 호스트와 Vue 웹 컴포넌트).
마이크로 프론트엔드 구현 비교
각 접근 방식의 주요 특징, 장단점을 요약해 보겠습니다.
Feature/Criterion | Module Federation | iFrames | Web Components |
---|---|---|---|
Isolation Level | High (runtime), shared context for JS & CSS often | Very High (browser context) | High (Shadow DOM for CSS, native JS isolation) |
Runtime Performance | Good (shared dependencies, lazy loading) | Moderate to Poor (new browsing context per iFrame) | Good (native browser features) |
Development Complexity | Moderate (Webpack configuration can be complex) | Low to Moderate (standard HTML, postMessage ) | Moderate (native APIs, can be verbose) |
Sharing Dependencies | Excellent (native Webpack feature) | Poor (each iFrame loads its own deps) | Moderate (global scope, external mechanisms) |
Framework Agnosticism | Good (can wrap different frameworks) | Excellent (entirely separate apps) | Excellent (native standard) |
Routing | Highly flexible (integrates into host router) | Complex (each iFrame has its own history/URL) | Flexible (integrates into host router) |
Communication | Direct JS calls, shared state management | postMessage API | Custom Events, properties/attributes |
SEO Impact | Generally good (dynamic loading, unified DOM) | Potentially poor (content in separate contexts) | Generally good (unified DOM) |
Use Cases | SPAs with loosely coupled features, shared libs, team autonomy | Embedding third-party content, legacy apps, high security isolation | Reusable UI components, design systems, framework-agnostic libs |
Styling | Shared stylesheets, CSS-in-JS, CSS Modules | Completely isolated (separate stylesheets) | Encapsulated (Shadow DOM CSS) |
Bundle Size | Optimized by sharing common libraries | Can be large (each app bundles its own deps) | Relatively small (native APIs) |
결론
마이크로 프론트엔드를 구현하기 위해 모듈 페더레이션, iFrame, 웹 컴포넌트 간의 선택은 주로 특정 프로젝트 요구 사항, 기존 인프라, 예산 및 원하는 격리 수준에 따라 달라집니다. 모듈 페더레이션은 특히 JavaScript 생태계 내에서 고도로 통합되고 성능이 뛰어난 마이크로 프론트엔드를 달성하기 위한 정교한 Webpack 기반 솔루션을 제공합니다. iFrame은 잠재적인 성능 및 통합 복잡성을 희생하면서 분산된 애플리케이션 또는 신뢰할 수 없는 콘텐츠를 포함하는 데 이상적인 비교할 수 없는 격리를 제공합니다. 웹 컴포넌트는 네이티브이며 프레임워크에 구애받지 않는 재사용 가능한 UI 컴포넌트를 구축하여 공유 디자인 시스템 및 프레임워크 간 통합을 위한 격차를 해소합니다. 각 접근 방식은 고유한 강점을 제공하며, 견고하고 확장 가능한 마이크로 프론트엔드 아키텍처를 구축하려면 각 접근 방식의 고유한 절충점에 대한 명확한 이해가 필수적입니다.