Skip to content
Published on

マイクロフロントエンドアーキテクチャ実践ガイド:Module Federation、Single-SPA、Web Components比較

Authors

マイクロフロントエンドアーキテクチャ

1. マイクロフロントエンドが必要な理由

モノリシックフロントエンドアプリケーションが成長すると予測可能な問題が発生する。ビルド時間が10分を超え、一つのチームがデプロイすると他チームのコードまで一緒にデプロイされ、小さな修正一つに全体QAサイクルを回す必要がある。Martin Fowlerの「Micro Frontends」記事(2019年)で初めて体系的に整理されたマイクロフロントエンドアーキテクチャは、マイクロサービスの哲学をフロントエンドに適用したものである。

マイクロフロントエンドの核心原則は以下の通り。

  • 独立デプロイ(Independent Deployment):各チームが自身のリリースサイクルを持つ
  • 技術独立性(Technology Agnostic):チームごとにReact、Vue、Angularなど異なるフレームワークを選択できる
  • チーム自律性(Team Autonomy):ドメイン単位でチームを構成し、フロントエンドからバックエンドまで垂直スライスで所有する
  • 障害分離(Fault Isolation):一つのマイクロフロントエンドの障害がアプリ全体をダウンさせない

しかしマイクロフロントエンドは万能薬ではない。Cam Jacksonのmartinfowler.com寄稿(2019年)でも警告するように、不必要な複雑性を導入する最も確実な方法でもある。導入前に「本当に必要か?」をまず問うべきである。

2. 三つの実装方式の詳細比較

2.1 Webpack Module Federation

Webpack 5で導入されたModule Federationは現在最も広く使われているマイクロフロントエンド実装方式である。Webpack Module Federation公式ドキュメントによると、別々にビルドされた複数のアプリケーションがランタイムに互いのモジュールを動的に共有できる。

ホストアプリケーション(Shell)設定

// host-app/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')

module.exports = {
  mode: 'production',
  output: {
    publicPath: 'https://host.example.com/',
    uniqueName: 'host_app',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        // リモートマイクロフロントエンド宣言
        productApp: 'product@https://product.example.com/remoteEntry.js',
        cartApp: 'cart@https://cart.example.com/remoteEntry.js',
        userApp: 'user@https://user.example.com/remoteEntry.js',
      },
      shared: {
        react: {
          singleton: true, // 単一インスタンスのみ許可
          requiredVersion: '^18.0.0',
          eager: true, // 即時ロード(ホストのみ)
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0',
          eager: true,
        },
        'react-router-dom': {
          singleton: true,
          requiredVersion: '^6.0.0',
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
}

リモートアプリケーション(Product MFE)設定

// product-app/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')

module.exports = {
  mode: 'production',
  output: {
    publicPath: 'https://product.example.com/',
    uniqueName: 'product_app',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'product',
      filename: 'remoteEntry.js',
      exposes: {
        // 外部に公開するモジュール
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/components/ProductDetail',
        './ProductSearch': './src/components/ProductSearch',
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
        'react-router-dom': {
          singleton: true,
          requiredVersion: '^6.0.0',
        },
      },
    }),
  ],
}

ホストでリモートコンポーネントを使用

// host-app/src/App.jsx
import React, { Suspense, lazy } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import ErrorBoundary from './components/ErrorBoundary'
import Navigation from './components/Navigation'
import LoadingSpinner from './components/LoadingSpinner'

// リモートコンポーネントをlazy loading
const ProductList = lazy(() => import('productApp/ProductList'))
const ProductDetail = lazy(() => import('productApp/ProductDetail'))
const Cart = lazy(() => import('cartApp/Cart'))
const UserProfile = lazy(() => import('userApp/UserProfile'))

// 障害分離のためのラッパーコンポーネント
function MicroFrontendWrapper({ children, fallback }) {
  return (
    <ErrorBoundary fallback={fallback || <div>このセクションを読み込めませんでした。</div>}>
      <Suspense fallback={<LoadingSpinner />}>{children}</Suspense>
    </ErrorBoundary>
  )
}

export default function App() {
  return (
    <BrowserRouter>
      <Navigation />
      <main>
        <Routes>
          <Route
            path="/products"
            element={
              <MicroFrontendWrapper>
                <ProductList />
              </MicroFrontendWrapper>
            }
          />
          <Route
            path="/products/:id"
            element={
              <MicroFrontendWrapper>
                <ProductDetail />
              </MicroFrontendWrapper>
            }
          />
          <Route
            path="/cart"
            element={
              <MicroFrontendWrapper>
                <Cart />
              </MicroFrontendWrapper>
            }
          />
          <Route
            path="/profile"
            element={
              <MicroFrontendWrapper>
                <UserProfile />
              </MicroFrontendWrapper>
            }
          />
        </Routes>
      </main>
    </BrowserRouter>
  )
}

3. 三つのアプローチの総合比較

項目Module FederationSingle-SPAWeb Components
実装複雑度中間高い低い〜中間
フレームワーク独立性Webpack依存フレームワーク無関係完全独立
共有依存関係内蔵サポート(shared)Import Mapで管理手動管理
CSS分離別途戦略必要別途戦略必要Shadow DOM内蔵
ルーティング統合ホストルーター使用自前ルーティングエンジン手動実装
バンドルサイズ最適化可能(tree-shaking)オーケストレーターオーバーヘッド最小(ネイティブ)
学習曲線Webpack知識必要Single-SPA API学習Web標準知識
ブラウザサポートWebpackランタイムSystemJSポリフィルモダンブラウザネイティブ
独立デプロイサポートサポートサポート
デバッグ難易度中間高い(ライフサイクル追跡)低い
エコシステム成熟度高い高い中間
SSRサポート部分サポート限定的限定的
適切な規模中〜大規模大規模/異種小〜中規模/ウィジェット

選択ガイド

Module Federationを選ぶべきとき:

  • すべてのチームがすでにWebpackを使っている
  • React単一フレームワークで統一された環境
  • コンポーネントレベルの細かい共有が必要

Single-SPAを選ぶべきとき:

  • React、Vue、Angularなど異種フレームワークが共存する必要がある
  • レガシーアプリを段階的にマイグレーション中
  • ページ単位のマイクロフロントエンドが必要

Web Componentsを選ぶべきとき:

  • フレームワーク依存性を最小化したい
  • ウィジェット形式の独立コンポーネントをデプロイ
  • 標準技術のみで長期メンテナンスを希望

11. マイクロフロントエンドを使うべきでないとき

Martin Fowlerの記事とLuca Mezzaliraの著書が共に強調するように、マイクロフロントエンドは組織の問題を解決するアーキテクチャパターンであり、技術の問題を解決するパターンではない。

使うべきでない状況:

  1. チームが5人以下の場合:コミュニケーションオーバーヘッドよりマイクロフロントエンドの運用複雑性のほうが大きい
  2. 製品が初期段階の場合:ドメイン境界が確定していない状態で分離すると間違った境界で固定化される
  3. パフォーマンスが最優先の場合:マイクロフロントエンドは必然的にバンドルサイズとネットワークリクエストを増加させる
  4. 単一フレームワークで十分な場合:「多様なフレームワークを使える」ことは「使うべき」という意味ではない
  5. デプロイサイクルが同一の場合:全チームが2週間ごとに一緒にデプロイするなら独立デプロイの利点がない

代替アプローチ:

  • モノレポ + パッケージ分離:TurborepoやNxでコードは分離しつつビルドは統合
  • ルートベースのコード分割:React.lazy + Suspenseでルート別独立バンドル
  • コンポーネントライブラリ:共有UIキットで一貫性を確保
  • モジュラーモノリス:ドメイン別のモジュール境界を明確にしつつ一つのアプリとしてデプロイ

12. おわりに

マイクロフロントエンドは強力だが複雑なアーキテクチャパターンである。成功する導入のために覚えるべき核心事項を整理する。

  1. 組織が先:技術的決定の前にチーム構造と所有権モデルをまず定義する
  2. 段階的に導入する:ビッグバン転換ではなく一つのMFEから始めて段階的に拡張する
  3. 共有契約を明確にする:共有依存関係バージョン、API契約、イベントスキーマをドキュメント化し自動検証する
  4. CSS分離は必須:スタイル衝突は最も一般的でデバッグが困難な問題
  5. 障害分離を設計する:ErrorBoundaryとフォールバックUIで一つのMFE障害が全体をダウンさせないようにする
  6. パフォーマンスを継続監視する:バンドルサイズ、ロード時間、Core Web VitalsをMFE別に追跡する

Module FederationはWebpackエコシステムで最も成熟した選択肢であり、Single-SPAは異種フレームワーク環境で柔軟であり、Web Componentsは標準技術で長期的な互換性を保証する。プロジェクトの規模、チーム構造、技術スタックに合った正しいツールを選択することが成功の鍵である。

参考資料