architecture

マイクロフロントエンド設計 - 独立したチームによるスケーラブルなUI開発

2025.12.02

マイクロフロントエンドとは

マイクロフロントエンドは、マイクロサービスの考え方をフロントエンドに適用したアーキテクチャパターンです。大規模なアプリケーションを、独立してデプロイ可能な小さなアプリケーションに分割します。

flowchart TB
    subgraph Monolithic["モノリシックフロントエンド"]
        subgraph Single["Single Application"]
            M1["Product<br/>Module"]
            M2["Cart<br/>Module"]
            M3["Account<br/>Module"]
        end
        Note1["単一のデプロイ、単一のチーム"]
    end

    subgraph MicroFE["マイクロフロントエンド"]
        subgraph Shell["Container App (Shell)"]
            Header["Shared Header"]
            subgraph Apps["独立アプリケーション"]
                A1["Product App<br/>(Team A)"]
                A2["Cart App<br/>(Team B)"]
                A3["Account App<br/>(Team C)"]
            end
        end
        Note2["独立デプロイ、独立チーム、技術選択の自由"]
    end

導入を検討すべきケース

ケース理由
大規模チーム(10+人)チーム間の独立性を確保
レガシーシステムの段階的移行新旧技術の共存が可能
異なるリリースサイクル機能ごとに独立してデプロイ
ドメイン駆動組織ビジネスドメインでチーム分割
技術スタックの多様性React, Vue, Angular の共存

統合パターン

1. Module Federation(Webpack 5)

flowchart TB
    subgraph Host["Host App (Shell)"]
        Config["webpack.config.js<br/>remotes: {<br/>  products, cart, account<br/>}"]
    end

    Host --> Products
    Host --> Cart
    Host --> Account

    subgraph Products["Products Remote"]
        P1["exposes:<br/>- List<br/>- Detail"]
    end

    subgraph Cart["Cart Remote"]
        C1["exposes:<br/>- Cart<br/>- Summary"]
    end

    subgraph Account["Account Remote"]
        A1["exposes:<br/>- Profile<br/>- Settings"]
    end
// host-app/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        products: 'products@http://localhost:3001/remoteEntry.js',
        cart: 'cart@http://localhost:3002/remoteEntry.js',
        account: 'account@http://localhost:3003/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

// products-app/webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'products',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/components/ProductDetail',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};
// host-app/src/App.tsx
import React, { Suspense, lazy } from 'react';

// 動的インポートでリモートコンポーネントを読み込み
const ProductList = lazy(() => import('products/ProductList'));
const Cart = lazy(() => import('cart/Cart'));
const AccountSettings = lazy(() => import('account/Settings'));

function App() {
  return (
    <div>
      <Header />
      <Routes>
        <Route
          path="/products/*"
          element={
            <Suspense fallback={<Skeleton />}>
              <ProductList />
            </Suspense>
          }
        />
        <Route
          path="/cart"
          element={
            <Suspense fallback={<Skeleton />}>
              <Cart />
            </Suspense>
          }
        />
        <Route
          path="/account/*"
          element={
            <Suspense fallback={<Skeleton />}>
              <AccountSettings />
            </Suspense>
          }
        />
      </Routes>
    </div>
  );
}

2. Single-SPA

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

// マイクロフロントエンドの登録
registerApplication({
  name: '@myorg/products',
  app: () => System.import('@myorg/products'),
  activeWhen: ['/products'],
});

registerApplication({
  name: '@myorg/cart',
  app: () => System.import('@myorg/cart'),
  activeWhen: ['/cart'],
});

registerApplication({
  name: '@myorg/account',
  app: () => System.import('@myorg/account'),
  activeWhen: ['/account'],
});

// 共有コンポーネント(常にアクティブ)
registerApplication({
  name: '@myorg/navbar',
  app: () => System.import('@myorg/navbar'),
  activeWhen: () => true,
});

start();
// products-app/src/myorg-products.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './App';

const lifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: App,
  errorBoundary(err, info, props) {
    return <div>Error in products app</div>;
  },
});

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

3. Native Federation(Vite/Rspack対応)

// vite.config.ts
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    federation({
      name: 'host-app',
      remotes: {
        products: 'http://localhost:3001/assets/remoteEntry.js',
        cart: 'http://localhost:3002/assets/remoteEntry.js',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
});

状態管理と通信

flowchart TB
    subgraph Events["1. Custom Events (推奨)"]
        EA["App A"] -->|CustomEvent| EB["App B"]
    end

    subgraph SharedState["2. Shared State (複雑な場合)"]
        SA["App A"] --> Store["Store<br/>(Global)"]
        SC["App C"] --> Store
        Store --> SB["App B"]
        Store --> SD["App D"]
    end

    subgraph URL["3. URL/Query Params"]
        URLNote["ナビゲーション情報の共有"]
    end

Custom Events

// 共有ライブラリ
// @myorg/events
export const CartEvents = {
  ITEM_ADDED: 'cart:item-added',
  ITEM_REMOVED: 'cart:item-removed',
  CART_UPDATED: 'cart:updated',
} as const;

export interface CartItemEvent {
  productId: string;
  quantity: number;
  price: number;
}

export function dispatchCartEvent(
  type: string,
  detail: CartItemEvent
): void {
  window.dispatchEvent(new CustomEvent(type, { detail }));
}

export function subscribeToCartEvent(
  type: string,
  handler: (event: CustomEvent<CartItemEvent>) => void
): () => void {
  window.addEventListener(type, handler as EventListener);
  return () => window.removeEventListener(type, handler as EventListener);
}
// products-app/src/ProductCard.tsx
import { CartEvents, dispatchCartEvent } from '@myorg/events';

function ProductCard({ product }) {
  const handleAddToCart = () => {
    dispatchCartEvent(CartEvents.ITEM_ADDED, {
      productId: product.id,
      quantity: 1,
      price: product.price,
    });
  };

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

// cart-app/src/Cart.tsx
import { useEffect, useState } from 'react';
import { CartEvents, subscribeToCartEvent } from '@myorg/events';

function Cart() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    const unsubscribe = subscribeToCartEvent(CartEvents.ITEM_ADDED, (event) => {
      setItems((prev) => [...prev, event.detail]);
    });

    return unsubscribe;
  }, []);

  return (
    <div>
      <h2>Cart ({items.length} items)</h2>
      {/* ... */}
    </div>
  );
}

グローバル状態の共有

// @myorg/shared-store
import { create } from 'zustand';

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  login: (user: User) => void;
  logout: () => void;
}

// グローバルに共有されるストア
export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  isAuthenticated: false,
  login: (user) => set({ user, isAuthenticated: true }),
  logout: () => set({ user: null, isAuthenticated: false }),
}));

// 各マイクロフロントエンドで使用
// header-app/src/Header.tsx
import { useAuthStore } from '@myorg/shared-store';

function Header() {
  const { user, isAuthenticated, logout } = useAuthStore();

  return (
    <header>
      {isAuthenticated ? (
        <>
          <span>Welcome, {user?.name}</span>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <a href="/login">Login</a>
      )}
    </header>
  );
}

ルーティング戦略

// shell-app/src/routing.ts
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { lazy, Suspense } from 'react';

// 各マイクロフロントエンドのルートを動的読み込み
const ProductsRoutes = lazy(() => import('products/Routes'));
const CartRoutes = lazy(() => import('cart/Routes'));
const AccountRoutes = lazy(() => import('account/Routes'));

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      {
        path: 'products/*',
        element: (
          <Suspense fallback={<Loading />}>
            <ProductsRoutes />
          </Suspense>
        ),
      },
      {
        path: 'cart/*',
        element: (
          <Suspense fallback={<Loading />}>
            <CartRoutes />
          </Suspense>
        ),
      },
      {
        path: 'account/*',
        element: (
          <Suspense fallback={<Loading />}>
            <AccountRoutes />
          </Suspense>
        ),
      },
    ],
  },
]);

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

デプロイ戦略

flowchart TB
    subgraph CDN["CDN構成"]
        Shell["shell/<br/>└── v1.2.3/"]
        Products["products/<br/>└── v2.1.0/"]
        Cart["cart/<br/>└── v1.8.5/"]
        Account["account/<br/>└── v3.0.1/"]
    end

    subgraph Manifest["マニフェスト管理"]
        JSON["products: cdn/products/v2.1.0/<br/>cart: cdn/cart/v1.8.5/<br/>account: cdn/account/v3.0.1/"]
    end

    CDN --> Manifest
    Note["Blue-Green / Canary デプロイ対応可能"]

共通UIコンポーネント

// @myorg/ui-kit (共有デザインシステム)
export { Button } from './Button';
export { Input } from './Input';
export { Card } from './Card';
export { Modal } from './Modal';
export { theme } from './theme';

// 各マイクロフロントエンドで使用
// webpack.config.js
{
  shared: {
    '@myorg/ui-kit': {
      singleton: true,
      requiredVersion: '^1.0.0',
    },
  },
}

テスト戦略

レベル対象ツール
ユニット個別コンポーネントVitest, Jest
統合マイクロFE単体Testing Library
E2E全体統合Playwright, Cypress
コントラクトAPI契約Pact

参考リンク

← 一覧に戻る