마이크로 프론트엔드: 대규모 팀을 위한 프론트엔드 아키텍처 전략

마이크로프론트엔드아키텍처팀협업Module FederationSingle-SPA확장성

마이크로 프론트엔드: 대규모 팀을 위한 프론트엔드 아키텍처 전략

2026년 현재, 많은 기업들이 프론트엔드 애플리케이션의 복잡성과 팀 확장성 문제에 직면하고 있습니다. 수백 명의 개발자가 하나의 거대한 프론트엔드 애플리케이션을 개발할 때 발생하는 문제들 - 배포 병목, 기술 스택 제약, 팀 간의 의존성 - 을 해결하기 위해 마이크로 프론트엔드 아키텍처가 주목받고 있습니다.

Netflix, Amazon, Spotify 같은 대기업들이 성공적으로 도입한 마이크로 프론트엔드는 백엔드의 마이크로서비스 개념을 프론트엔드에 적용한 것입니다. 이제 각 팀이 독립적으로 개발, 테스트, 배포할 수 있으면서도 사용자에게는 하나의 통합된 애플리케이션으로 보이는 아키텍처를 구축할 수 있습니다.

마이크로 프론트엔드의 핵심 개념과 장점

기존 모놀리식 프론트엔드의 문제점

// 전형적인 모놀리식 React 애플리케이션
// src/App.tsx - 모든 기능이 하나의 애플리케이션에
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// 모든 팀의 컴포넌트가 하나의 저장소에
import UserDashboard from './user/Dashboard';
import ProductCatalog from './products/Catalog';
import ShoppingCart from './cart/Cart';
import PaymentFlow from './payment/Payment';
import AdminPanel from './admin/AdminPanel';
import AnalyticsDashboard from './analytics/Dashboard';

function App() {
    return (
        <BrowserRouter>
            <Routes>
                {/* 사용자 관리 팀 */}
                <Route path="/dashboard/*" element={<UserDashboard />} />

                {/* 상품 팀 */}
                <Route path="/products/*" element={<ProductCatalog />} />

                {/* 결제 팀 */}
                <Route path="/cart/*" element={<ShoppingCart />} />
                <Route path="/payment/*" element={<PaymentFlow />} />

                {/* 어드민 팀 */}
                <Route path="/admin/*" element={<AdminPanel />} />

                {/* 데이터 팀 */}
                <Route path="/analytics/*" element={<AnalyticsDashboard />} />
            </Routes>
        </BrowserRouter>
    );
}

모놀리식 아키텍처의 문제점:

  • 배포 의존성: 한 팀의 변경사항이 전체 애플리케이션 배포에 영향
  • 기술 스택 제약: 모든 팀이 동일한 프레임워크와 버전 사용
  • 코드 충돌: 여러 팀이 동일한 저장소에서 작업시 병합 충돌
  • 확장성 한계: 팀과 기능이 늘어날수록 복잡도 기하급수적 증가

마이크로 프론트엔드의 해결책

// Shell Application (Container)
// apps/shell/src/App.tsx
import React, { Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// 각 마이크로 프론트엔드를 동적으로 로드
const UserApp = React.lazy(() => import('userApp/UserDashboard'));
const ProductsApp = React.lazy(() => import('productsApp/ProductCatalog'));
const CartApp = React.lazy(() => import('cartApp/ShoppingCart'));
const PaymentApp = React.lazy(() => import('paymentApp/PaymentFlow'));

function ShellApp() {
    return (
        <BrowserRouter>
            <GlobalHeader />
            <Suspense fallback={<LoadingSpinner />}>
                <Routes>
                    <Route
                        path="/dashboard/*"
                        element={<UserApp />}
                    />
                    <Route
                        path="/products/*"
                        element={<ProductsApp />}
                    />
                    <Route
                        path="/cart/*"
                        element={<CartApp />}
                    />
                    <Route
                        path="/payment/*"
                        element={<PaymentApp />}
                    />
                </Routes>
            </Suspense>
            <GlobalFooter />
        </BrowserRouter>
    );
}

마이크로 프론트엔드의 장점:

  1. 팀 독립성: 각 팀이 독립적으로 개발, 테스트, 배포
  2. 기술적 자율성: 팀별로 최적의 기술 스택 선택 가능
  3. 점진적 업그레이드: 부분적으로 기술 스택 변경 가능
  4. 확장성: 새로운 팀과 기능을 쉽게 추가

주요 구현 방식과 도구들

1. Webpack Module Federation

Webpack 5에서 도입된 Module Federation은 가장 인기 있는 마이크로 프론트엔드 구현 방식입니다.

// 제품 팀의 webpack.config.js
const ModuleFederationPlugin = require('@module-federation/webpack');

module.exports = {
    entry: './src/index.tsx',
    mode: 'development',
    plugins: [
        new ModuleFederationPlugin({
            name: 'productsApp',
            filename: 'remoteEntry.js',
            exposes: {
                './ProductCatalog': './src/ProductCatalog',
                './ProductDetail': './src/ProductDetail',
                './ProductSearch': './src/ProductSearch',
            },
            shared: {
                react: { singleton: true },
                'react-dom': { singleton: true },
            },
        }),
    ],
};

// Shell 애플리케이션의 webpack.config.js
module.exports = {
    entry: './src/index.tsx',
    plugins: [
        new ModuleFederationPlugin({
            name: 'shell',
            remotes: {
                productsApp: 'productsApp@http://localhost:3001/remoteEntry.js',
                userApp: 'userApp@http://localhost:3002/remoteEntry.js',
                cartApp: 'cartApp@http://localhost:3003/remoteEntry.js',
                paymentApp: 'paymentApp@http://localhost:3004/remoteEntry.js',
            },
            shared: {
                react: { singleton: true },
                'react-dom': { singleton: true },
            },
        }),
    ],
};

런타임 로딩:

// 동적 마이크로 프론트엔드 로딩
import { loadRemoteModule } from '@module-federation/runtime';

async function loadProductApp() {
    try {
        const ProductModule = await loadRemoteModule({
            url: 'http://localhost:3001',
            scope: 'productsApp',
            module: './ProductCatalog'
        });

        return ProductModule.default;
    } catch (error) {
        console.error('Failed to load products app:', error);
        return ErrorFallback;
    }
}

// 동적 컴포넌트 렌더링
function DynamicProductCatalog() {
    const [ProductComponent, setProductComponent] = useState(null);

    useEffect(() => {
        loadProductApp().then(setProductComponent);
    }, []);

    if (!ProductComponent) {
        return <LoadingSkeleton />;
    }

    return <ProductComponent />;
}

2. Single-SPA Framework

Single-SPA는 여러 JavaScript 프레임워크를 하나의 애플리케이션에서 사용할 수 있게 해주는 프레임워크입니다.

// single-spa root config
import { registerApplication, start } from 'single-spa';

// React 애플리케이션 등록
registerApplication({
    name: 'user-dashboard',
    app: () => import('./user-dashboard/main.js'),
    activeWhen: '/dashboard',
    customProps: {
        authToken: getAuthToken(),
        apiUrl: process.env.API_URL,
    }
});

// Vue 애플리케이션 등록
registerApplication({
    name: 'product-catalog',
    app: () => import('./product-catalog/main.js'),
    activeWhen: '/products',
});

// Angular 애플리케이션 등록
registerApplication({
    name: 'admin-panel',
    app: () => import('./admin-panel/main.js'),
    activeWhen: '/admin',
});

start();

// React 마이크로 앱 (user-dashboard/main.js)
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import UserDashboard from './UserDashboard';

const lifecycles = singleSpaReact({
    React,
    ReactDOM,
    rootComponent: UserDashboard,
    errorBoundary(err, info, props) {
        return <ErrorBoundary error={err} />;
    },
});

export const { bootstrap, mount, unmount } = lifecycles;

3. Micro Frontend as iframe

가장 간단하지만 제한적인 방식입니다.

// 간단한 iframe 기반 마이크로 프론트엔드
import React, { useEffect, useRef } from 'react';

interface MicroFrontendProps {
    name: string;
    host: string;
    path: string;
    onNavigate?: (path: string) => void;
}

function MicroFrontend({ name, host, path, onNavigate }: MicroFrontendProps) {
    const iframeRef = useRef<HTMLIFrameElement>(null);

    useEffect(() => {
        const iframe = iframeRef.current;
        if (!iframe) return;

        // iframe과 parent 간의 통신
        const handleMessage = (event: MessageEvent) => {
            if (event.origin !== host) return;

            const { type, data } = event.data;

            switch (type) {
                case 'NAVIGATION':
                    onNavigate?.(data.path);
                    break;
                case 'RESIZE':
                    iframe.style.height = `${data.height}px`;
                    break;
            }
        };

        window.addEventListener('message', handleMessage);
        return () => window.removeEventListener('message', handleMessage);
    }, [host, onNavigate]);

    return (
        <iframe
            ref={iframeRef}
            src={`${host}${path}`}
            style={{
                width: '100%',
                border: 'none',
                minHeight: '600px',
            }}
            title={name}
        />
    );
}

// 사용 예시
function App() {
    return (
        <div>
            <MicroFrontend
                name="products"
                host="http://localhost:3001"
                path="/products"
                onNavigate={(path) => console.log('Navigate to:', path)}
            />
        </div>
    );
}

상태 관리와 통신 전략

글로벌 상태 공유

// 공유 이벤트 버스
class MicroFrontendEventBus {
    private listeners: Map<string, Function[]> = new Map();

    subscribe(event: string, callback: Function) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, []);
        }
        this.listeners.get(event)!.push(callback);

        // 구독 해제 함수 반환
        return () => {
            const callbacks = this.listeners.get(event);
            if (callbacks) {
                const index = callbacks.indexOf(callback);
                if (index > -1) {
                    callbacks.splice(index, 1);
                }
            }
        };
    }

    publish(event: string, data: any) {
        const callbacks = this.listeners.get(event);
        if (callbacks) {
            callbacks.forEach(callback => callback(data));
        }
    }
}

// 글로벌 인스턴스
window.microFrontendBus = new MicroFrontendEventBus();

// 사용 예시 - 장바구니 애플리케이션
function CartApp() {
    const [cartItems, setCartItems] = useState([]);

    useEffect(() => {
        // 상품이 추가될 때 이벤트 수신
        const unsubscribe = window.microFrontendBus.subscribe(
            'CART_ADD_ITEM',
            (product) => {
                setCartItems(items => [...items, product]);
            }
        );

        return unsubscribe;
    }, []);

    const removeItem = (itemId: string) => {
        setCartItems(items => items.filter(item => item.id !== itemId));

        // 다른 앱에 알림
        window.microFrontendBus.publish('CART_ITEM_REMOVED', { itemId });
    };

    return (
        <div>
            {cartItems.map(item => (
                <CartItem key={item.id} item={item} onRemove={removeItem} />
            ))}
        </div>
    );
}

// 상품 애플리케이션에서 장바구니에 추가
function ProductCard({ product }) {
    const addToCart = () => {
        window.microFrontendBus.publish('CART_ADD_ITEM', product);
    };

    return (
        <div>
            <h3>{product.name}</h3>
            <button onClick={addToCart}>Add to Cart</button>
        </div>
    );
}

중앙 집중식 상태 관리

// Redux 스토어를 마이크로 프론트엔드 간에 공유
// shared/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { authSlice } from './authSlice';
import { cartSlice } from './cartSlice';
import { userSlice } from './userSlice';

export const createSharedStore = () => {
    return configureStore({
        reducer: {
            auth: authSlice.reducer,
            cart: cartSlice.reducer,
            user: userSlice.reducer,
        },
        middleware: (getDefaultMiddleware) =>
            getDefaultMiddleware({
                serializableCheck: {
                    ignoredActions: ['persist/PERSIST'],
                },
            }),
    });
};

// Shell 애플리케이션에서 스토어 제공
import { Provider } from 'react-redux';

function ShellApp() {
    const store = createSharedStore();

    // 글로벌 스토어를 window 객체에 노출
    useEffect(() => {
        window.__SHARED_STORE__ = store;
    }, [store]);

    return (
        <Provider store={store}>
            <Router>
                {/* 마이크로 프론트엔드들 */}
            </Router>
        </Provider>
    );
}

// 각 마이크로 프론트엔드에서 공유 스토어 사용
function ProductApp() {
    const dispatch = window.__SHARED_STORE__.dispatch;
    const getState = window.__SHARED_STORE__.getState;

    const addToCart = (product) => {
        dispatch(cartSlice.actions.addItem(product));
    };

    return (
        <div>
            {/* 제품 컴포넌트들 */}
        </div>
    );
}

라우팅과 네비게이션

중앙 집중식 라우팅

// Shell 애플리케이션의 라우팅
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([
    {
        path: '/',
        element: <Layout />,
        children: [
            {
                path: 'dashboard/*',
                lazy: () => import('./microfrontends/UserDashboard'),
            },
            {
                path: 'products/*',
                lazy: () => import('./microfrontends/ProductCatalog'),
            },
            {
                path: 'cart/*',
                lazy: () => import('./microfrontends/ShoppingCart'),
            },
        ],
    },
]);

function App() {
    return <RouterProvider router={router} />;
}

// 마이크로 프론트엔드 내부 라우팅
// products/src/ProductApp.tsx
import { Routes, Route, useLocation } from 'react-router-dom';

function ProductApp() {
    const location = useLocation();

    // /products 경로를 제거하고 내부 라우팅 처리
    const basePath = '/products';
    const relativePath = location.pathname.replace(basePath, '');

    return (
        <Routes>
            <Route index element={<ProductList />} />
            <Route path="category/:categoryId" element={<CategoryProducts />} />
            <Route path="product/:productId" element={<ProductDetail />} />
            <Route path="search" element={<ProductSearch />} />
        </Routes>
    );
}

분산 라우팅

// 각 마이크로 프론트엔드가 자체 라우팅 관리
// shared/router/index.ts
class MicroFrontendRouter {
    private routes: Map<string, () => Promise<any>> = new Map();

    registerRoute(path: string, loader: () => Promise<any>) {
        this.routes.set(path, loader);
    }

    async navigate(path: string) {
        const loader = this.routes.get(path);
        if (loader) {
            const component = await loader();
            this.renderComponent(component);
        } else {
            // 404 처리
            this.render404();
        }
    }

    private renderComponent(component: any) {
        // 컴포넌트 렌더링 로직
    }

    private render404() {
        // 404 페이지 렌더링
    }
}

// 글로벌 라우터
window.__MF_ROUTER__ = new MicroFrontendRouter();

// 각 마이크로 프론트엔드에서 라우트 등록
// products/src/index.ts
window.__MF_ROUTER__.registerRoute('/products', () =>
    import('./ProductApp')
);

window.__MF_ROUTER__.registerRoute('/products/category/:id', () =>
    import('./CategoryPage')
);

// 네비게이션 헬퍼
function navigateTo(path: string) {
    window.__MF_ROUTER__.navigate(path);

    // 브라우저 히스토리 업데이트
    window.history.pushState({}, '', path);
}

스타일링과 디자인 시스템

공유 디자인 시스템

// design-system/src/index.ts
export { Button } from './components/Button';
export { Input } from './components/Input';
export { Modal } from './components/Modal';
export { theme } from './theme';
export { GlobalStyles } from './GlobalStyles';

// design-system/src/components/Button.tsx
import styled from 'styled-components';
import { theme } from '../theme';

interface ButtonProps {
    variant?: 'primary' | 'secondary' | 'danger';
    size?: 'small' | 'medium' | 'large';
    children: React.ReactNode;
    onClick?: () => void;
}

const StyledButton = styled.button<ButtonProps>`
    padding: ${props => theme.spacing[props.size || 'medium']};
    background-color: ${props => theme.colors[props.variant || 'primary']};
    border: none;
    border-radius: ${theme.borderRadius.medium};
    color: white;
    font-family: ${theme.fonts.primary};
    cursor: pointer;

    &:hover {
        opacity: 0.8;
    }

    &:disabled {
        opacity: 0.5;
        cursor: not-allowed;
    }
`;

export function Button(props: ButtonProps) {
    return <StyledButton {...props} />;
}

// 각 마이크로 프론트엔드에서 사용
// products/src/ProductCard.tsx
import { Button, theme } from '@company/design-system';

function ProductCard({ product }) {
    return (
        <div style={{ padding: theme.spacing.medium }}>
            <h3>{product.name}</h3>
            <Button variant="primary" onClick={() => addToCart(product)}>
                Add to Cart
            </Button>
        </div>
    );
}

CSS-in-JS 네임스페이스

// 각 마이크로 프론트엔드마다 고유한 네임스페이스
// products/src/styles.ts
import styled, { createGlobalStyle } from 'styled-components';

// 프리픽스를 사용한 스타일 격리
export const ProductContainer = styled.div.attrs({
    className: 'mf-products-container'
})`
    /* 제품 앱 전용 스타일 */
    .mf-products {
        &-card {
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            padding: 16px;
        }

        &-title {
            font-size: 18px;
            font-weight: bold;
            color: #333;
        }
    }
`;

// 글로벌 스타일 격리
export const ProductGlobalStyles = createGlobalStyle`
    .mf-products-container {
        /* 이 마이크로 프론트엔드 전용 글로벌 스타일 */
        font-family: 'Roboto', sans-serif;

        * {
            box-sizing: border-box;
        }
    }
`;

// 사용
function ProductApp() {
    return (
        <ProductContainer>
            <ProductGlobalStyles />
            <div className="mf-products-card">
                <h2 className="mf-products-title">Products</h2>
                {/* 제품 목록 */}
            </div>
        </ProductContainer>
    );
}

테스트 전략

단위 테스트와 통합 테스트

// 마이크로 프론트엔드 단위 테스트
// products/src/__tests__/ProductCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { ProductCard } from '../ProductCard';

// Mock 글로벌 이벤트 버스
const mockEventBus = {
    publish: jest.fn(),
    subscribe: jest.fn(),
};

beforeEach(() => {
    window.microFrontendBus = mockEventBus;
});

describe('ProductCard', () => {
    const mockProduct = {
        id: '1',
        name: 'Test Product',
        price: 99.99,
    };

    it('renders product information', () => {
        render(<ProductCard product={mockProduct} />);

        expect(screen.getByText('Test Product')).toBeInTheDocument();
        expect(screen.getByText('$99.99')).toBeInTheDocument();
    });

    it('publishes add to cart event when button clicked', () => {
        render(<ProductCard product={mockProduct} />);

        const addButton = screen.getByText('Add to Cart');
        fireEvent.click(addButton);

        expect(mockEventBus.publish).toHaveBeenCalledWith(
            'CART_ADD_ITEM',
            mockProduct
        );
    });
});

// E2E 테스트
// e2e/tests/shopping-flow.spec.ts
import { test, expect } from '@playwright/test';

test('complete shopping flow across microfrontends', async ({ page }) => {
    await page.goto('/products');

    // 제품 마이크로 프론트엔드에서 제품 선택
    await page.waitForSelector('[data-testid="product-card"]');
    await page.click('[data-testid="add-to-cart-btn"]');

    // 장바구니 마이크로 프론트엔드로 이동
    await page.goto('/cart');
    await page.waitForSelector('[data-testid="cart-item"]');

    // 결제 마이크로 프론트엔드로 이동
    await page.click('[data-testid="checkout-btn"]');
    await page.waitForSelector('[data-testid="payment-form"]');

    // 결제 완료
    await page.fill('[data-testid="card-number"]', '4111111111111111');
    await page.click('[data-testid="pay-btn"]');

    await expect(page.locator('[data-testid="success-message"]'))
        .toContainText('Payment successful');
});

마이크로 프론트엔드 간 계약 테스트

// Contract Testing with Pact
// products/src/__tests__/cart-integration.test.ts
import { Pact } from '@pact-foundation/pact';

const provider = new Pact({
    consumer: 'ProductApp',
    provider: 'CartApp',
    port: 1234,
    log: path.resolve(process.cwd(), 'logs', 'pact.log'),
    dir: path.resolve(process.cwd(), 'pacts'),
});

describe('Product to Cart Integration', () => {
    beforeAll(() => provider.setup());
    afterAll(() => provider.finalize());

    beforeEach(() => {
        const interaction = {
            state: 'cart is empty',
            uponReceiving: 'a request to add product to cart',
            withRequest: {
                method: 'POST',
                path: '/api/cart/items',
                body: {
                    productId: '123',
                    quantity: 1,
                },
            },
            willRespondWith: {
                status: 201,
                body: {
                    id: '456',
                    productId: '123',
                    quantity: 1,
                },
            },
        };

        return provider.addInteraction(interaction);
    });

    it('adds product to cart successfully', async () => {
        const response = await addToCart('123', 1);
        expect(response.status).toBe(201);
        expect(response.data.productId).toBe('123');
    });
});

배포와 CI/CD

독립적 배포 파이프라인

# .github/workflows/products-app.yml
name: Products App CI/CD

on:
  push:
    paths:
      - 'apps/products/**'
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci
        working-directory: ./apps/products

      - name: Run tests
        run: npm run test:ci
        working-directory: ./apps/products

      - name: Run E2E tests
        run: npm run test:e2e
        working-directory: ./apps/products

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Build application
        run: |
          npm ci
          npm run build
        working-directory: ./apps/products

      - name: Build and push Docker image
        env:
          REGISTRY: your-registry.com
          IMAGE_NAME: products-app
        run: |
          docker build -t $REGISTRY/$IMAGE_NAME:${{ github.sha }} ./apps/products
          docker push $REGISTRY/$IMAGE_NAME:${{ github.sha }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Deploy to production
        run: |
          kubectl set image deployment/products-app \
            products-app=your-registry.com/products-app:${{ github.sha }}

카나리 배포

// 배포 설정
// k8s/products-app/deployment.yml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: products-app
spec:
  replicas: 10
  strategy:
    canary:
      steps:
      - setWeight: 20
      - pause: {}
      - setWeight: 40
      - pause: {duration: 10}
      - setWeight: 60
      - pause: {duration: 10}
      - setWeight: 80
      - pause: {duration: 10}
  selector:
    matchLabels:
      app: products-app
  template:
    metadata:
      labels:
        app: products-app
    spec:
      containers:
      - name: products-app
        image: your-registry.com/products-app:latest
        ports:
        - containerPort: 3001

피처 플래그를 활용한 안전한 배포

// 피처 플래그 관리
// shared/feature-flags/index.ts
interface FeatureFlags {
    newProductLayout: boolean;
    enhancedSearch: boolean;
    aiRecommendations: boolean;
}

class FeatureFlagService {
    private flags: FeatureFlags;

    constructor() {
        this.flags = this.loadFlags();
    }

    private loadFlags(): FeatureFlags {
        // 환경변수, 원격 설정 등에서 플래그 로드
        return {
            newProductLayout: process.env.REACT_APP_NEW_PRODUCT_LAYOUT === 'true',
            enhancedSearch: process.env.REACT_APP_ENHANCED_SEARCH === 'true',
            aiRecommendations: process.env.REACT_APP_AI_RECOMMENDATIONS === 'true',
        };
    }

    isEnabled(flag: keyof FeatureFlags): boolean {
        return this.flags[flag];
    }
}

export const featureFlags = new FeatureFlagService();

// 컴포넌트에서 사용
function ProductList() {
    const showNewLayout = featureFlags.isEnabled('newProductLayout');

    return (
        <div>
            {showNewLayout ? (
                <NewProductLayout />
            ) : (
                <LegacyProductLayout />
            )}
        </div>
    );
}

성능 최적화와 모니터링

번들 크기 최적화

// 웹팩 설정 최적화
// apps/products/webpack.config.js
const ModuleFederationPlugin = require('@module-federation/webpack');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'async',
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all',
                },
                common: {
                    name: 'common',
                    minChunks: 2,
                    chunks: 'all',
                    enforce: true,
                },
            },
        },
    },

    plugins: [
        new ModuleFederationPlugin({
            // 중복 제거를 위한 shared 설정
            shared: {
                react: {
                    singleton: true,
                    requiredVersion: '^18.0.0',
                },
                'react-dom': {
                    singleton: true,
                    requiredVersion: '^18.0.0',
                },
                // 자주 사용되는 라이브러리들 공유
                lodash: {
                    singleton: true,
                },
                '@emotion/react': {
                    singleton: true,
                },
            },
        }),

        // 프로덕션에서만 번들 분석
        process.env.ANALYZE && new BundleAnalyzerPlugin(),
    ].filter(Boolean),
};

성능 모니터링

// 성능 메트릭 수집
// shared/monitoring/performance.ts
class MicroFrontendPerformanceMonitor {
    private metrics: Map<string, number> = new Map();

    startTiming(label: string) {
        this.metrics.set(`${label}_start`, performance.now());
    }

    endTiming(label: string) {
        const start = this.metrics.get(`${label}_start`);
        if (start) {
            const duration = performance.now() - start;
            this.metrics.set(`${label}_duration`, duration);

            // 분석 서비스로 전송
            this.sendMetric(label, duration);
        }
    }

    private sendMetric(label: string, duration: number) {
        if ('sendBeacon' in navigator) {
            navigator.sendBeacon('/api/metrics', JSON.stringify({
                metric: label,
                duration,
                microfrontend: window.__MF_NAME__,
                timestamp: Date.now(),
            }));
        }
    }
}

export const performanceMonitor = new MicroFrontendPerformanceMonitor();

// 사용 예시
function ProductApp() {
    useEffect(() => {
        performanceMonitor.startTiming('products_app_mount');

        return () => {
            performanceMonitor.endTiming('products_app_mount');
        };
    }, []);

    const handleProductLoad = useCallback(async () => {
        performanceMonitor.startTiming('product_load');

        try {
            await loadProducts();
        } finally {
            performanceMonitor.endTiming('product_load');
        }
    }, []);

    return <ProductList onLoad={handleProductLoad} />;
}

실제 도입 사례와 교훈

Netflix의 마이크로 프론트엔드

Netflix는 전 세계 수백 명의 개발자가 작업하는 복잡한 웹 애플리케이션을 마이크로 프론트엔드로 구성했습니다.

아키텍처 특징:

  • 팀별 자율성: 각 팀이 독립적인 기술 스택 선택
  • 점진적 배포: 일부 기능만 업데이트 가능
  • A/B 테스트: 마이크로 프론트엔드 단위의 실험

성과:

  • 개발 속도 40% 향상
  • 배포 빈도 300% 증가
  • 시스템 안정성 향상

Spotify의 Squad 모델

// Spotify의 Squad 기반 마이크로 프론트엔드
// squads/playlist-squad/src/PlaylistApp.tsx
function PlaylistApp() {
    return (
        <PlaylistProvider>
            <PlaylistHeader />
            <PlaylistContent />
            <PlaylistPlayer />
        </PlaylistProvider>
    );
}

// squads/search-squad/src/SearchApp.tsx
function SearchApp() {
    return (
        <SearchProvider>
            <SearchInput />
            <SearchResults />
            <SearchFilters />
        </SearchProvider>
    );
}

도입 시 주의사항

기술적 복잡성:

  • 마이크로 프론트엔드 간 통신 오버헤드
  • 중복된 라이브러리로 인한 번들 크기 증가
  • 디버깅과 테스트의 복잡성

조직적 도전:

  • 팀 간 커뮤니케이션 복잡성
  • 공통 표준 수립의 어려움
  • 전체 시스템 관점의 부족

미래 전망과 발전 방향

2026-2028 예상 트렌드

기술적 발전:

  • 네이티브 ESM 지원: 빌드 없이 브라우저에서 직접 모듈 로딩
  • WebAssembly 통합: 성능이 중요한 부분을 WASM으로 구현
  • Edge Computing: CDN 엣지에서 마이크로 프론트엔드 조합
// 미래의 네이티브 ESM 기반 마이크로 프론트엔드
// native-esm/shell/index.html
<script type="module">
    import { createApp } from './shell-app.js';

    // 동적 ESM import로 마이크로 프론트엔드 로드
    const [ProductsApp, CartApp] = await Promise.all([
        import('https://products.example.com/app.js'),
        import('https://cart.example.com/app.js')
    ]);

    const app = createApp({
        products: ProductsApp,
        cart: CartApp,
    });

    app.mount('#root');
</script>

도구 생태계의 발전

새로운 프레임워크들:

  • Bit: 컴포넌트 기반 마이크로 프론트엔드
  • Nx: 모노리포에서 마이크로 프론트엔드 관리
  • Vite Federation: Vite 기반의 Module Federation

실전 도입 가이드

단계별 마이그레이션 전략

1단계: 평가와 계획 (1-2개월)

// 현재 애플리케이션 분석
interface ApplicationAnalysis {
    totalLoc: number;
    teamCount: number;
    deploymentFrequency: string;
    buildTime: number;
    testTime: number;
    criticalBugs: number;
}

const currentState: ApplicationAnalysis = {
    totalLoc: 150000,
    teamCount: 8,
    deploymentFrequency: 'weekly',
    buildTime: 45, // minutes
    testTime: 30, // minutes
    criticalBugs: 12, // per quarter
};

// 마이크로 프론트엔드 후보 식별
const microfrontendCandidates = [
    {
        name: 'user-management',
        team: 'Identity Team',
        complexity: 'medium',
        independence: 'high',
        businessValue: 'high',
    },
    {
        name: 'product-catalog',
        team: 'Commerce Team',
        complexity: 'low',
        independence: 'high',
        businessValue: 'high',
    },
];

2단계: POC 구현 (2-3개월) 3단계: 점진적 마이그레이션 (6-12개월) 4단계: 최적화와 표준화 (3-6개월)

성공 지표 정의

// KPI 추적 시스템
interface MicroFrontendKPIs {
    developmentVelocity: {
        featuresPerMonth: number;
        deploymentFrequency: number;
        leadTime: number; // hours
    };
    qualityMetrics: {
        bugCount: number;
        testCoverage: number;
        performanceScore: number;
    };
    teamProductivity: {
        blockerResolutionTime: number; // hours
        crossTeamDependencies: number;
        developerSatisfaction: number; // 1-10
    };
}

결론: 대규모 프론트엔드 개발의 새로운 표준

마이크로 프론트엔드는 대규모 팀과 복잡한 애플리케이션을 위한 효과적인 해결책으로 자리잡았습니다. 2026년 현재, 많은 기업들이 성공적으로 도입하여 개발 속도와 팀 자율성을 크게 향상시켰습니다.

성공적인 도입을 위한 핵심 요소:

  1. 명확한 팀 경계와 책임: 각 팀의 역할과 소유권 정의
  2. 기술적 표준 수립: 공통 라이브러리와 통신 프로토콜
  3. 점진적 마이그레이션: 위험을 최소화한 단계적 전환
  4. 지속적인 최적화: 성능과 개발자 경험의 균형

마이크로 프론트엔드는 단순히 기술적 패턴을 넘어 조직의 확장성과 생산성을 높이는 전략적 도구입니다. 올바르게 구현된다면, 대규모 팀에서도 빠르고 안정적인 프론트엔드 개발이 가능해집니다.

궁금한 점이 있으신가요?

문의사항이 있으시면 언제든지 연락주세요.