T-CREATOR

マイクロフロントエンドアーキテクチャを TypeScript で実現する方法

マイクロフロントエンドアーキテクチャを TypeScript で実現する方法

現代の Web アプリケーション開発において、プロジェクトの規模拡大と複数チームでの協働は避けられない課題となっています。特に大規模なフロントエンドアプリケーションでは、モノリシックな構造による技術的制約が開発効率を著しく阻害することがあります。このような状況下で注目されているのが、マイクロフロントエンドアーキテクチャです。

マイクロフロントエンドは、フロントエンドアプリケーションを独立したマイクロサービスのような小さな単位に分割し、それぞれを独立して開発・デプロイできるアーキテクチャパターンです。TypeScript の強力な型システムと組み合わせることで、堅牢で保守性の高いシステムを構築できます。

この記事では、TypeScript 環境でマイクロフロントエンドアーキテクチャを実現するための具体的な手法を解説します。最新の Module Federation 2.0 や Rspack、Turbopack といった先進的なツールを活用した実装方法から、型安全性の確保、開発ワークフローの最適化まで、実務で即座に活用できる知識をお届けします。

背景

大規模フロントエンド開発の課題

現代の Web アプリケーション開発では、ユーザーの要求が高度化し、アプリケーションの機能も急速に複雑化しています。企業のデジタルトランスフォーメーションが進む中で、単一のフロントエンドアプリケーションに多くの機能が集約される傾向にあります。

特に以下のような問題が顕著に現れています。

ビルド時間の増大

コードベースが大きくなるにつれて、開発時のビルド時間が指数関数的に増加してしまいます。例えば、数十万行の TypeScript コードを含むアプリケーションでは、開発サーバーの起動に数分、ホットリロードに数十秒かかることも珍しくありません。これは開発者の生産性を著しく低下させる要因となります。

typescript// 大規模アプリケーションでよく見られる問題の例
// 数千のコンポーネントが単一のエントリーポイントから読み込まれる
import { ComponentA } from './components/ComponentA';
import { ComponentB } from './components/ComponentB';
// ... 数千のimport文が続く

// 結果:初回ビルドが非常に遅くなる

コード結合度の高さ

モノリシックな構造では、本来独立すべき機能間での依存関係が複雑化し、一部の変更が予期しない箇所に影響を与えるリスクが高まります。これにより、機能追加や修正時の品質担保が困難になります。

チームスケーリングの問題

開発チーム間の競合

複数のチームが同一のコードベースで作業する場合、以下のような問題が発生します:

  • マージ競合の頻発: 複数チームが同時に同じファイルを編集することで、Git のマージ競合が日常茶飯事となります
  • デプロイの相互依存: 一つの機能がデプロイされるために、他の全ての機能も同時にデプロイ可能な状態である必要があります
  • 技術選択の制約: 新しいライブラリやフレームワークを導入する際、アプリケーション全体への影響を考慮する必要があります

ドメイン知識の分散

大規模組織では、各チームが異なるビジネスドメインの専門知識を持っています。しかし、モノリシックな構造では、この専門知識をコードアーキテクチャに反映することが困難です。

typescript// 問題のある例:異なるドメインが混在
class OrderService {
  // 注文処理のロジック
  processOrder(order: Order) {
    /* ... */
  }

  // ユーザー管理のロジック(本来は別ドメイン)
  updateUserProfile(user: User) {
    /* ... */
  }

  // 在庫管理のロジック(本来は別ドメイン)
  updateInventory(product: Product) {
    /* ... */
  }
}

技術的負債の蓄積とメンテナンス性の低下

レガシーコードの増大

時間の経過とともに、以下のような技術的負債が蓄積されていきます:

  • 古いライブラリやフレームワーク: アップデートが困難になり、セキュリティリスクが増大します
  • 一貫性のないコーディングスタイル: チームの変更や時間の経過により、コードの品質にばらつきが生じます
  • テストの不備: 複雑な依存関係により、適切なテストの作成が困難になります

リファクタリングの困難さ

大規模なコードベースでは、部分的なリファクタリングでも広範囲への影響調査が必要となり、改善作業のコストが著しく高くなります。

マイクロサービスアーキテクチャとの関係

バックエンドマイクロサービスとの整合性

多くの企業では、バックエンドシステムでマイクロサービスアーキテクチャを採用しています。しかし、フロントエンドがモノリシックなままでは、以下のような不整合が生じます:

  • 開発速度の乖離: バックエンドチームは独立してデプロイできるのに、フロントエンドチームは全体の調整が必要
  • 技術的制約の相違: バックエンドは各サービスで最適な技術選択ができるのに、フロントエンドは統一技術スタックに制約される
  • ビジネスドメインの不一致: バックエンドのサービス境界とフロントエンドの機能境界が合致しない

Conway's Law の適用

Conway's Law によれば、「組織の設計は、その組織のコミュニケーション構造を反映したシステムを作り出す」とされています。チーム構造がマイクロサービス型であるにもかかわらず、フロントエンドがモノリシックである場合、組織の効率性が阻害される可能性があります。

課題

マイクロフロントエンドアーキテクチャの導入において、開発チームが直面する主要な技術的課題を詳しく見ていきましょう。

モジュール間の独立性確保

依存関係の分離

マイクロフロントエンドの最大の利点は、各モジュールが独立して開発・デプロイできることです。しかし、実際の実装では以下のような課題があります:

typescript// 問題のある例:モジュール間の密結合
// UserProfile モジュール
export class UserProfileComponent {
  constructor(
    private orderService: OrderService, // 他のモジュールへの直接依存
    private paymentService: PaymentService // 他のモジュールへの直接依存
  ) {}
}

// このような実装では、UserProfile モジュールを独立してテストできない

適切な分離を実現するためには、以下のような設計パターンが必要です:

typescript// 改善例:インターフェースによる分離
interface OrderServiceInterface {
  getOrderHistory(userId: string): Promise<Order[]>;
}

interface PaymentServiceInterface {
  getPaymentMethods(
    userId: string
  ): Promise<PaymentMethod[]>;
}

export class UserProfileComponent {
  constructor(
    private orderService: OrderServiceInterface,
    private paymentService: PaymentServiceInterface
  ) {}
}

状態管理の境界設定

グローバルな状態管理ライブラリを使用している場合、モジュール間での状態の共有方法が課題となります。各モジュールが独自の状態を持ちつつ、必要な情報のみを適切に共有する仕組みが必要です。

型安全性の維持(TypeScript 環境での課題)

動的インポートでの型情報の損失

マイクロフロントエンドでは、実行時に他のモジュールを動的に読み込むことが一般的です。しかし、この過程で TypeScript の型情報が失われてしまう問題があります:

typescript// 問題のある例:型情報の損失
const RemoteComponent = React.lazy(
  () => import('remote-app/Component') // 型情報が利用できない
);

// 実行時エラーのリスク
function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <RemoteComponent
        invalidProp='value' // TypeScriptがエラーを検出できない
      />
    </Suspense>
  );
}

型定義の共有戦略

マイクロフロントエンド間で型定義を安全に共有する方法も重要な課題です。以下のような選択肢があり、それぞれに利点と欠点があります:

  1. 共有型ライブラリ: 型定義を専用の npm パッケージとして公開
  2. 型定義の自動生成: ビルド時に型定義ファイルを生成・配信
  3. Contract-First 開発: API スキーマから型定義を自動生成
typescript// 型共有の例:共有型ライブラリパターン
// @company/shared-types パッケージ
export interface UserProfile {
  id: string;
  name: string;
  email: string;
  preferences: UserPreferences;
}

export interface Order {
  id: string;
  userId: string;
  items: OrderItem[];
  status: OrderStatus;
}

ビルド・デプロイプロセスの複雑化

複数モジュールの協調ビルド

従来の単一アプリケーションと比較して、マイクロフロントエンドでは複数のモジュールを協調してビルドする必要があります:

yaml# CI/CDパイプラインの複雑化例
stages:
  - build-shared-components
  - build-micro-frontends:
      parallel:
        - build-user-module
        - build-order-module
        - build-payment-module
  - integration-test
  - deploy-to-staging
  - e2e-test
  - deploy-to-production

依存関係の管理

各マイクロフロントエンドが異なるバージョンのライブラリを使用している場合、実行時に競合が発生する可能性があります。

パフォーマンスとバンドルサイズの最適化

重複コードの問題

複数のマイクロフロントエンドが同じライブラリを個別にバンドルすると、以下のような問題が発生します:

typescript// 問題:各モジュールで React を個別にバンドル
// Module A
import React from 'react'; // React 18.2.0 (500KB)
import { Button } from './components';

// Module B
import React from 'react'; // React 18.2.0 (500KB) - 重複!
import { Modal } from './components';

// 結果:1MB の React が重複してダウンロードされる

ネットワークリクエストの増加

マイクロフロントエンドでは、各モジュールが独立したチャンクとしてダウンロードされるため、ネットワークリクエスト数が増加し、初期表示速度に影響を与える可能性があります。

状態管理とデータ共有の問題

マイクロフロントエンド間でのデータ共有

独立性を保ちながら、必要なデータを適切に共有する方法は重要な設計課題です:

typescript// 課題:ユーザー情報を複数のモジュールで共有
// User Module で取得したユーザー情報を
// Order Module や Payment Module でも使用したい

// しかし、直接的な依存関係は作りたくない

イベント駆動通信の複雑さ

モジュール間の疎結合を実現するためにイベント駆動通信を採用する場合、イベントの管理やデバッグが複雑になります:

typescript// イベント駆動通信の例
interface GlobalEvents {
  'user:login': { userId: string; timestamp: Date };
  'order:created': {
    paymentId: string;
    orderId: string;
  };
}

// デバッグ時にイベントフローを追跡するのが困難

これらの課題に対して、次章では最新のツールチェーンを活用した具体的な解決策を詳しく解説していきます。

解決策

前章で挙げた課題に対して、現代のツールチェーンと設計パターンを活用した具体的な解決策を詳しく解説します。特に、2024 年にリリースされた Module Federation 2.0 や Rspack、Turbopack といった最新技術を中心に、実践的なアプローチをご紹介します。

Module Federation 2.0 によるランタイム統合

Module Federation 2.0 の革新的な改善点

Module Federation 2.0 は、従来の Module Federation を大幅に改善し、より柔軟で強力なマイクロフロントエンドの実現を可能にします。ByteDance の Web Infra チームとコミュニティの協力により開発されたこの新バージョンには、以下の画期的な機能が含まれています:

1. ビルドツールからの独立

従来の Module Federation は Webpack に強く依存していましたが、2.0 ではランタイムがビルドツールから完全に分離されました。これにより、Webpack、Rspack、Vite など異なるビルドツール間でもシームレスに連携できます。

typescript// Module Federation 2.0 での新しいランタイムAPI
import {
  init,
  loadRemote,
} from '@module-federation/enhanced/runtime';

// ビルドツールに依存しない動的な初期化
init({
  name: '@demo/app-main',
  remotes: [
    {
      name: '@demo/user-module',
      entry: 'http://localhost:3001/mf-manifest.json', // 新しいmanifest形式
      alias: 'userModule',
    },
    {
      name: '@demo/order-module',
      entry: 'http://localhost:3002/remoteEntry.js', // 従来形式もサポート
      alias: 'orderModule',
    },
  ],
  shared: {
    react: {
      version: '18.2.0',
      scope: 'default',
      lib: () => React,
      shareConfig: {
        singleton: true,
        requiredVersion: '^18.0.0',
      },
    },
  },
});

// 動的なモジュール読み込み
const UserProfile = await loadRemote<
  React.ComponentType<UserProfileProps>
>('userModule/UserProfile');
2. TypeScript 型の自動同期

Module Federation 2.0 の最も画期的な機能の一つが、動的型ヒント機能です。この機能により、リモートモジュールの型情報がリアルタイムで同期され、開発時の型安全性が確保されます。

typescript// 従来の課題:型情報の欠如
const RemoteComponent = React.lazy(
  () => import('remote-app/Component') // 型情報なし
);

// Module Federation 2.0 の解決策:自動型生成
// ビルド時に mf-manifest.json と共に型定義が生成される
const RemoteComponent = React.lazy(
  () => import('remote-app/Component') // 完全な型サポート!
);

// TypeScript が適切に型チェックを行う
<RemoteComponent
  userId='123' // ✓ 正しい型
  invalidProp='value' // ✗ TypeScriptエラー
/>;
3. Runtime Plugins システム

新しい Runtime Plugins システムにより、Module Federation の動作を柔軟にカスタマイズできます:

typescript// カスタムランタイムプラグインの例
import { FederationRuntimePlugin } from '@module-federation/runtime/types';

const errorHandlingPlugin =
  (): FederationRuntimePlugin => ({
    name: 'error-handling-plugin',

    // リモートモジュールの読み込みエラー時
    errorLoadRemote({ id, error, from, origin }) {
      console.error(
        `Failed to load remote module: ${id}`,
        error
      );

      // フォールバック用のコンポーネントを返す
      return {
        default: () => (
          <div className='error-fallback'>
            <h3>モジュールの読み込みに失敗しました</h3>
            <p>
              しばらく時間をおいてから再度お試しください。
            </p>
          </div>
        ),
      };
    },

    // モジュール読み込み前の処理
    beforeRequest(args) {
      console.log(`Loading module: ${args.id}`);
      return args;
    },

    // 共有ライブラリの解決処理
    resolveShare(args) {
      // 特定のライブラリをホストから強制的に取得
      if (
        args.pkgName === 'react' ||
        args.pkgName === 'react-dom'
      ) {
        const host =
          args.GlobalFederation['__INSTANCES__'][0];
        if (host) {
          args.resolver = () =>
            host.options.shared[args.pkgName];
        }
      }
      return args;
    },
  });

// プラグインの使用
init({
  name: 'host-app',
  plugins: [errorHandlingPlugin()],
  // その他の設定...
});

Rspack での Module Federation 2.0 活用

Rspack は Module Federation 2.0 をネイティブサポートしており、高速なビルドパフォーマンスを実現します:

typescript// rspack.config.ts
import { defineConfig } from '@rspack/cli';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';

export default defineConfig({
  mode: 'development',
  entry: './src/index.ts',
  plugins: [
    new ModuleFederationPlugin({
      name: 'user_module',
      filename: 'remoteEntry.js',
      exposes: {
        './UserProfile': './src/components/UserProfile',
        './UserSettings': './src/components/UserSettings',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
        '@company/shared-types': { singleton: true },
      },
      // TypeScript型の自動生成を有効化
      typescript: {
        generateTypes: true,
        typesFolder: '@mf-types',
      },
      // マニフェストファイルの生成
      manifest: {
        fileName: 'mf-manifest.json',
        getPublicPath: () => 'http://localhost:3001/',
      },
    }),
  ],
  devServer: {
    port: 3001,
  },
});

Single-SPA フレームワークによるマイクロアプリ管理

Single-SPA は、複数のマイクロフロントエンドアプリケーションを統合するための成熟したフレームワークです。Module Federation と組み合わせることで、より堅牢なマイクロフロントエンドシステムを構築できます。

Single-SPA の基本アーキテクチャ

typescript// root-config.ts - ルートアプリケーション
import { registerApplication, start } from 'single-spa';

// マイクロアプリケーションの登録
registerApplication({
  name: 'user-module',
  app: () => import('@company/user-module'),
  activeWhen: ['/user', '/profile'],
  customProps: {
    apiBase: process.env.API_BASE_URL,
    theme: 'light',
  },
});

registerApplication({
  name: 'order-module',
  app: () => import('@company/order-module'),
  activeWhen: ['/orders', '/checkout'],
});

registerApplication({
  name: 'navigation-module',
  app: () => import('@company/navigation-module'),
  activeWhen: () => true, // 常にアクティブ
});

start({
  urlRerouteOnly: true,
});

マイクロアプリケーションの実装

typescript// user-module/src/main.ts
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) => (
    <div className='error-boundary'>
      <h2>ユーザーモジュールでエラーが発生しました</h2>
      <details>
        <summary>エラー詳細</summary>
        <pre>{err.stack}</pre>
      </details>
    </div>
  ),
});

// Single-SPA ライフサイクルのエクスポート
export const { bootstrap, mount, unmount } = lifecycles;

// スタンドアロン実行のサポート
if (!window.singleSpaNavigate) {
  ReactDOM.render(<App />, document.getElementById('root'));
}

TypeScript 型定義の共有戦略

TypeScript 環境でのマイクロフロントエンド開発において、型安全性を維持するための戦略は以下の通りです:

1. 共有型ライブラリパターン
typescript// @company/shared-types/src/index.ts
export interface User {
  id: string;
  name: string;
  email: string;
  role: UserRole;
  createdAt: Date;
  updatedAt: Date;
}

export type UserRole = 'admin' | 'user' | 'guest';

export interface UserCreateRequest {
  name: string;
  email: string;
  role: UserRole;
}
2. 型定義の自動生成パターン
typescript// build-scripts/generate-types.ts
import { generateTypes } from '@module-federation/typescript';

async function generateSharedTypes() {
  await generateTypes({
    federationConfig: {
      exposes: {
        './UserProfile': './src/components/UserProfile',
        './UserAPI': './src/services/UserAPI',
      },
    },
    outputDir: './dist/@mf-types',
    moduleName: 'user-module',
    remoteTypesFolder: '@mf-types',
    enableDTS: true,
  });
}

generateSharedTypes();
3. Contract-First 開発パターン
typescript// api-contracts/user.contract.ts
import { z } from 'zod';

// APIスキーマの定義
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  roles: z.array(z.enum(['admin', 'user', 'viewer'])),
  preferences: z.object({
    theme: z.enum(['light', 'dark']),
    language: z.string().length(2),
    notifications: z.boolean(),
  }),
});

// TypeScript型の自動生成
export type User = z.infer<typeof UserSchema>;

// APIクライアントでの使用
export class UserAPI {
  async getUser(id: string): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    const data = await response.json();

    // 実行時型検証
    return UserSchema.parse(data);
  }
}

Container/Content パターンの実装

Container/Content パターンは、マイクロフロントエンドにおける責任分離を明確にする重要な設計パターンです。

Container アプリケーション(ホスト)

typescript// container-app/src/App.tsx
import React, { Suspense } from 'react';
import {
  BrowserRouter,
  Routes,
  Route,
} from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { SharedThemeProvider } from '@company/shared-ui';

// リモートコンポーネントの動的インポート
const UserModule = React.lazy(
  () => import('userModule/App')
);
const OrderModule = React.lazy(
  () => import('orderModule/App')
);
const NavigationModule = React.lazy(
  () => import('navigationModule/Navigation')
);

function App() {
  return (
    <SharedThemeProvider>
      <BrowserRouter>
        <div className='app-layout'>
          <header>
            <ErrorBoundary
              fallback={<div>ナビゲーションエラー</div>}
            >
              <Suspense
                fallback={<div>Loading navigation...</div>}
              >
                <NavigationModule />
              </Suspense>
            </ErrorBoundary>
          </header>

          <main>
            <Routes>
              <Route
                path='/user/*'
                element={
                  <ErrorBoundary
                    fallback={
                      <div>ユーザーモジュールエラー</div>
                    }
                  >
                    <Suspense
                      fallback={
                        <div>Loading user module...</div>
                      }
                    >
                      <UserModule />
                    </Suspense>
                  </ErrorBoundary>
                }
              />
              <Route
                path='/orders/*'
                element={
                  <ErrorBoundary
                    fallback={
                      <div>注文モジュールエラー</div>
                    }
                  >
                    <Suspense
                      fallback={
                        <div>Loading order module...</div>
                      }
                    >
                      <OrderModule />
                    </Suspense>
                  </ErrorBoundary>
                }
              />
            </Routes>
          </main>
        </div>
      </BrowserRouter>
    </SharedThemeProvider>
  );
}

export default App;

Content アプリケーション(リモート)

typescript// user-module/src/App.tsx
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { UserProvider } from './contexts/UserContext';
import UserProfile from './components/UserProfile';
import UserSettings from './components/UserSettings';

function UserApp() {
  return (
    <UserProvider>
      <Routes>
        <Route path='profile' element={<UserProfile />} />
        <Route path='settings' element={<UserSettings />} />
      </Routes>
    </UserProvider>
  );
}

export default UserApp;

イベントドリブンな通信アーキテクチャ

マイクロフロントエンド間の疎結合な通信を実現するため、イベントドリブンアーキテクチャを採用します。

グローバルイベントバスの実装

typescript// shared-lib/src/EventBus.ts
type EventCallback<T = any> = (data: T) => void;

class EventBus {
  private events: Map<string, EventCallback[]> = new Map();

  subscribe<T>(
    eventName: string,
    callback: EventCallback<T>
  ): () => void {
    if (!this.events.has(eventName)) {
      this.events.set(eventName, []);
    }

    this.events.get(eventName)!.push(callback);

    // 購読解除関数を返す
    return () => {
      const callbacks = this.events.get(eventName);
      if (callbacks) {
        const index = callbacks.indexOf(callback);
        if (index > -1) {
          callbacks.splice(index, 1);
        }
      }
    };
  }

  publish<T>(eventName: string, data: T): void {
    const callbacks = this.events.get(eventName);
    if (callbacks) {
      callbacks.forEach((callback) => {
        try {
          callback(data);
        } catch (error) {
          console.error(
            `Error in event callback for ${eventName}:`,
            error
          );
        }
      });
    }
  }

  // デバッグ用:現在のイベント購読状況を表示
  debug(): void {
    console.log('EventBus状況:', {
      eventCount: this.events.size,
      events: Array.from(this.events.entries()).map(
        ([name, callbacks]) => ({
          name,
          subscriberCount: callbacks.length,
        })
      ),
    });
  }
}

// グローバルインスタンス
export const globalEventBus = new EventBus();

// TypeScript型安全なイベントエミッター
export class TypedEventBus<
  TEvents extends Record<string, any>
> {
  private eventBus = new EventBus();

  subscribe<K extends keyof TEvents>(
    eventName: K,
    callback: EventCallback<TEvents[K]>
  ): () => void {
    return this.eventBus.subscribe(
      String(eventName),
      callback
    );
  }

  publish<K extends keyof TEvents>(
    eventName: K,
    data: TEvents[K]
  ): void {
    this.eventBus.publish(String(eventName), data);
  }
}

型安全なイベント通信の実装

typescript// global-events.ts
import { TypedEventBus } from '@company/shared-lib';

interface GlobalEvents {
  'user:login': { user: User; timestamp: Date };
  'user:logout': { userId: string; timestamp: Date };
  'user:profile-updated': {
    user: User;
    changedFields: string[];
  };
  'order:created': { order: Order; user: User };
  'order:status-changed': {
    orderId: string;
    newStatus: OrderStatus;
    timestamp: Date;
  };
  'navigation:route-changed': {
    from: string;
    to: string;
    timestamp: Date;
  };
  'theme:changed': {
    theme: 'light' | 'dark';
    userId: string;
  };
}

export const appEventBus =
  new TypedEventBus<GlobalEvents>();

// 使用例:ユーザーモジュール
export class UserModule {
  constructor() {
    // ログイン成功時のイベント発行
    this.loginService.onLoginSuccess((user) => {
      appEventBus.publish('user:login', {
        user,
        timestamp: new Date(),
      });
    });
  }
}

// 使用例:注文モジュール
export class OrderModule {
  constructor() {
    // ユーザーログイン時の処理
    appEventBus.subscribe('user:login', ({ user }) => {
      this.orderService.loadUserOrders(user.id);
    });

    // ユーザーログアウト時の処理
    appEventBus.subscribe('user:logout', ({ userId }) => {
      this.orderService.clearCache();
    });
  }
}

リアクティブな状態管理との統合

typescript// reactive-state/GlobalState.ts
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
import { appEventBus } from './global-events';

interface GlobalState {
  currentUser: User | null;
  theme: 'light' | 'dark';
  notifications: Notification[];

  // アクション
  setCurrentUser: (user: User | null) => void;
  setTheme: (theme: 'light' | 'dark') => void;
  addNotification: (notification: Notification) => void;
}

export const useGlobalState = create<GlobalState>()(
  subscribeWithSelector((set, get) => ({
    currentUser: null,
    theme: 'light',
    notifications: [],

    setCurrentUser: (user) => {
      set({ currentUser: user });

      // グローバルイベントの発行
      if (user) {
        appEventBus.publish('user:login', {
          user,
          timestamp: new Date(),
        });
      } else {
        const previousUser = get().currentUser;
        if (previousUser) {
          appEventBus.publish('user:logout', {
            userId: previousUser.id,
            timestamp: new Date(),
          });
        }
      }
    },

    setTheme: (theme) => {
      set({ theme });
      const user = get().currentUser;
      if (user) {
        appEventBus.publish('theme:changed', {
          theme,
          userId: user.id,
        });
      }
    },

    addNotification: (notification) => {
      set((state) => ({
        notifications: [
          ...state.notifications,
          notification,
        ],
      }));
    },
  }))
);

// 外部イベントの購読
appEventBus.subscribe(
  'user:profile-updated',
  ({ user }) => {
    useGlobalState.getState().setCurrentUser(user);
  }
);

このように、Module Federation 2.0 を中心とした最新のツールチェーンと適切な設計パターンを組み合わせることで、堅牢で保守性の高いマイクロフロントエンドアーキテクチャを実現できます。次章では、これらの技術を使った具体的な実装例を詳しく見ていきましょう。

具体例

これまで解説した理論を実際のプロジェクトで活用するための具体的な実装例をご紹介します。

React + TypeScript + Module Federation 2.0 の実装

プロジェクト構成

以下のような構成でマイクロフロントエンドシステムを構築します:

rubyproject-root/
├── packages/
│   ├── host-app/          # ホストアプリケーション
│   ├── user-module/       # ユーザー管理モジュール
│   ├── order-module/      # 注文管理モジュール
│   └── shared-types/      # 共有型定義
├── tools/
│   ├── build-scripts/     # ビルドスクリプト
│   └── deployment/        # デプロイメント設定
└── package.json

Yarn Workspace の設定

json{
  "name": "microfrontend-workspace",
  "private": true,
  "workspaces": ["packages/*"],
  "devDependencies": {
    "@module-federation/enhanced": "^0.6.0",
    "@rspack/cli": "^0.5.0",
    "@rspack/core": "^0.5.0",
    "typescript": "^5.3.0"
  },
  "scripts": {
    "dev": "yarn workspaces run dev",
    "build": "yarn workspaces run build",
    "type-check": "yarn workspaces run type-check"
  }
}

ホストアプリケーションの実装

typescript// host-app/rspack.config.ts
import { defineConfig } from '@rspack/cli';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';

export default defineConfig({
  mode: 'development',
  entry: './src/index.ts',
  plugins: [
    new ModuleFederationPlugin({
      name: 'host_app',
      remotes: {
        userModule:
          'user_module@http://localhost:3001/mf-manifest.json',
        orderModule:
          'order_module@http://localhost:3002/mf-manifest.json',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
        '@company/shared-types': { singleton: true },
      },
    }),
  ],
});
typescript// host-app/src/App.tsx
import React, { Suspense } from 'react';
import {
  BrowserRouter,
  Routes,
  Route,
} from 'react-router-dom';
import ErrorBoundary from './components/ErrorBoundary';

const UserModule = React.lazy(
  () => import('userModule/App')
);
const OrderModule = React.lazy(
  () => import('orderModule/App')
);

function App() {
  return (
    <BrowserRouter>
      <div className='app'>
        <nav className='main-nav'>
          <a href='/users'>ユーザー管理</a>
          <a href='/orders'>注文管理</a>
        </nav>

        <main>
          <Routes>
            <Route
              path='/users/*'
              element={
                <ErrorBoundary>
                  <Suspense
                    fallback={
                      <div>
                        ユーザーモジュール読み込み中...
                      </div>
                    }
                  >
                    <UserModule />
                  </Suspense>
                </ErrorBoundary>
              }
            />
            <Route
              path='/orders/*'
              element={
                <ErrorBoundary>
                  <Suspense
                    fallback={
                      <div>注文モジュール読み込み中...</div>
                    }
                  >
                    <OrderModule />
                  </Suspense>
                </ErrorBoundary>
              }
            />
          </Routes>
        </main>
      </div>
    </BrowserRouter>
  );
}

共有ライブラリの設計と型安全性

共有型定義パッケージ

typescript// shared-types/src/user.ts
export interface User {
  id: string;
  name: string;
  email: string;
  role: UserRole;
  createdAt: Date;
  updatedAt: Date;
}

export type UserRole = 'admin' | 'user' | 'guest';

export interface UserCreateRequest {
  name: string;
  email: string;
  role: UserRole;
}
typescript// shared-types/src/api.ts
export interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
  errors?: string[];
}

export interface PaginatedResponse<T>
  extends ApiResponse<T[]> {
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

ユーザーモジュールの実装

typescript// user-module/rspack.config.ts
import { defineConfig } from '@rspack/cli';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';

export default defineConfig({
  entry: './src/index.tsx',
  mode: 'development',
  devServer: { port: 3001 },
  plugins: [
    new ModuleFederationPlugin({
      name: 'user_module',
      filename: 'remoteEntry.js',
      exposes: {
        './App': './src/App',
        './UserList': './src/components/UserList',
        './UserService': './src/services/UserService',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
        '@company/shared-types': { singleton: true },
      },
      typescript: {
        generateTypes: true,
        typesFolder: '@mf-types',
      },
      manifest: {
        fileName: 'mf-manifest.json',
      },
    }),
  ],
});
typescript// user-module/src/services/UserService.ts
import {
  User,
  UserCreateRequest,
  ApiResponse,
} from '@company/shared-types';

export class UserService {
  private baseUrl =
    process.env.API_BASE_URL || 'http://localhost:8000';

  async getUsers(): Promise<User[]> {
    const response = await fetch(`${this.baseUrl}/users`);
    const data: ApiResponse<User[]> = await response.json();

    if (!data.success) {
      throw new Error(
        data.message || 'ユーザー取得に失敗しました'
      );
    }

    return data.data;
  }

  async createUser(
    userData: UserCreateRequest
  ): Promise<User> {
    const response = await fetch(`${this.baseUrl}/users`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(userData),
    });

    const data: ApiResponse<User> = await response.json();

    if (!data.success) {
      throw new Error(
        data.message || 'ユーザー作成に失敗しました'
      );
    }

    return data.data;
  }
}

複数チームでの開発ワークフロー

チーム構成とマイクロフロントエンドの対応

チーム担当モジュール責任範囲
Platform TeamHost App, Shared Libraries全体アーキテクチャ、共通基盤
User TeamUser Moduleユーザー管理機能
Order TeamOrder Module注文・決済機能
DevOps TeamCI/CD, Infrastructureデプロイメント、監視

開発ワークフロー

yaml# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      host-app: ${{ steps.changes.outputs.host-app }}
      user-module: ${{ steps.changes.outputs.user-module }}
      order-module: ${{ steps.changes.outputs.order-module }}
      shared-types: ${{ steps.changes.outputs.shared-types }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v2
        id: changes
        with:
          filters: |
            host-app:
              - 'packages/host-app/**'
            user-module:
              - 'packages/user-module/**'
            order-module:
              - 'packages/order-module/**'
            shared-types:
              - 'packages/shared-types/**'

  build-shared-types:
    if: needs.detect-changes.outputs.shared-types == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      - name: Build shared types
        run: yarn workspace @company/shared-types build

  build-modules:
    needs: [detect-changes, build-shared-types]
    strategy:
      matrix:
        module: [host-app, user-module, order-module]
    runs-on: ubuntu-latest
    if: needs.detect-changes.outputs[matrix.module] == 'true'
    steps:
      - uses: actions/checkout@v4
      - name: Build ${{ matrix.module }}
        run: |
          yarn install --frozen-lockfile
          yarn workspace ${{ matrix.module }} build
      - name: Run tests
        run: yarn workspace ${{ matrix.module }} test

CI/CD パイプラインの設計

Rspack 利用による高速ビルド

typescript// tools/build-scripts/build-all.ts
import { spawn } from 'child_process';
import { performance } from 'perf_hooks';

interface BuildResult {
  module: string;
  duration: number;
  success: boolean;
}

async function buildModule(
  moduleName: string
): Promise<BuildResult> {
  const startTime = performance.now();

  return new Promise((resolve) => {
    const child = spawn(
      'yarn',
      ['workspace', moduleName, 'build'],
      {
        stdio: 'inherit',
      }
    );

    child.on('close', (code) => {
      const duration = performance.now() - startTime;
      resolve({
        module: moduleName,
        duration,
        success: code === 0,
      });
    });
  });
}

async function buildAllModules() {
  const modules = [
    'shared-types',
    'user-module',
    'order-module',
    'host-app',
  ];
  const results: BuildResult[] = [];

  // 共有型を最初にビルド
  const sharedTypesResult = await buildModule(
    'shared-types'
  );
  results.push(sharedTypesResult);

  if (!sharedTypesResult.success) {
    console.error('共有型のビルドに失敗しました');
    process.exit(1);
  }

  // その他のモジュールを並列ビルド
  const moduleBuilds = await Promise.all(
    modules.slice(1).map(buildModule)
  );
  results.push(...moduleBuilds);

  // 結果を表示
  console.log('\n📊 ビルド結果:');
  results.forEach((result) => {
    const status = result.success ? '✅' : '❌';
    const time = (result.duration / 1000).toFixed(2);
    console.log(`${status} ${result.module}: ${time}秒`);
  });

  const totalTime =
    results.reduce((sum, r) => sum + r.duration, 0) / 1000;
  console.log(
    `\n⏱️  総ビルド時間: ${totalTime.toFixed(2)}秒`
  );
}

buildAllModules().catch(console.error);

パフォーマンス監視とデバッグ手法

Module Federation 専用の監視ツール

typescript// tools/monitoring/federation-monitor.ts
interface ModuleLoadEvent {
  moduleName: string;
  loadTime: number;
  success: boolean;
  error?: string;
}

class FederationMonitor {
  private events: ModuleLoadEvent[] = [];

  init() {
    // グローバルなModule Federationイベントを監視
    if (typeof window !== 'undefined') {
      const originalLoadRemote =
        window.__FEDERATION__?.loadRemote;

      if (originalLoadRemote) {
        window.__FEDERATION__.loadRemote = async (
          id: string
        ) => {
          const startTime = performance.now();

          try {
            const result = await originalLoadRemote(id);
            const loadTime = performance.now() - startTime;

            this.recordEvent({
              moduleName: id,
              loadTime,
              success: true,
            });

            return result;
          } catch (error) {
            const loadTime = performance.now() - startTime;

            this.recordEvent({
              moduleName: id,
              loadTime,
              success: false,
              error: error.message,
            });

            throw error;
          }
        };
      }
    }
  }

  private recordEvent(event: ModuleLoadEvent) {
    this.events.push(event);

    // パフォーマンス閾値をチェック
    if (event.loadTime > 3000) {
      console.warn(
        `⚠️  モジュール ${
          event.moduleName
        } の読み込みが遅いです: ${event.loadTime.toFixed(
          2
        )}ms`
      );
    }

    // 外部監視サービスに送信
    this.sendToMonitoring(event);
  }

  private sendToMonitoring(event: ModuleLoadEvent) {
    // DatadogやNew Relicなどの監視サービスに送信
    fetch('/api/monitoring/federation-events', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(event),
    }).catch(console.error);
  }

  getStats() {
    return {
      totalEvents: this.events.length,
      averageLoadTime:
        this.events.reduce(
          (sum, e) => sum + e.loadTime,
          0
        ) / this.events.length,
      successRate:
        this.events.filter((e) => e.success).length /
        this.events.length,
      slowModules: this.events.filter(
        (e) => e.loadTime > 2000
      ),
    };
  }
}

export const federationMonitor = new FederationMonitor();

// アプリケーション起動時に初期化
if (typeof window !== 'undefined') {
  federationMonitor.init();
}

このような具体的な実装例により、TypeScript を活用したマイクロフロントエンドアーキテクチャを実際のプロジェクトで効果的に導入できます。

まとめ

マイクロフロントエンドの適用場面

マイクロフロントエンドアーキテクチャは以下のような場面で特に効果を発揮します:

大規模チーム開発

  • 複数チームの並行開発: 10 名以上の開発者が関わるプロジェクトで、チーム間の依存関係を最小化
  • 異なる技術スタック: チームごとに最適な技術を選択できる柔軟性
  • 独立したリリースサイクル: 機能ごとに異なる頻度でリリースが可能

レガシーシステムの段階的移行

  • 部分的な現代化: 既存システムの一部を徐々に新技術で置き換え
  • リスクの分散: 全面的な書き換えではなく、段階的な移行でリスクを最小化
  • 投資対効果の最適化: 最も価値の高い部分から優先的に改善

エンタープライズアプリケーション

  • マルチブランド対応: 複数のブランドや事業部門で共通基盤を活用
  • 地域特化機能: 地域ごとの要件に応じたカスタマイゼーション
  • コンプライアンス対応: 機能ごとに異なるセキュリティ要件への対応

導入時の注意点とベストプラクティス

技術的な注意点

パフォーマンスの考慮
typescript// 悪い例:過度な分割
const TinyComponent = React.lazy(
  () => import('remote/TinyButton')
);
// 小さなコンポーネントをリモート化するとオーバーヘッドが大きい

// 良い例:適切な粒度での分割
const UserManagementModule = React.lazy(
  () => import('remote/UserModule')
);
// 機能単位での分割が効果的
状態管理の複雑化
typescript// 状態の境界を明確に定義
interface ModuleBoundary {
  // ローカル状態(モジュール内のみ)
  localState: LocalUserState;

  // 共有状態(グローバル)
  sharedState: Pick<GlobalState, 'currentUser' | 'theme'>;

  // イベント通信
  events: {
    onUserUpdate: (user: User) => void;
    onLogout: () => void;
  };
}

組織的な考慮事項

チーム間のガバナンス
typescript// 共有規約の策定例
interface FederationContract {
  // 技術要件
  framework: 'React' | 'Vue' | 'Angular';
  typeScript: boolean;
  testCoverage: number; // 最低80%

  // インターフェース規約
  exposedComponents: Record<string, ComponentInterface>;
  sharedDependencies: string[];

  // 運用要件
  deploymentProcess: DeploymentStrategy;
  monitoringRequirements: MonitoringConfig;
}
コミュニケーション体制
役割責任頻度
アーキテクト全体設計、技術判断週次
テックリードモジュール間連携日次
DevOps エンジニアインフラ、CI/CD随時
QA エンジニア統合テストリリース前

チーム体制とガバナンスの重要性

成功要因

明確な責任分界
mermaidgraph TD
    A[Platform Team] --> B[共通基盤]
    A --> C[アーキテクチャ設計]

    D[Feature Team A] --> E[ユーザー機能]
    D --> F[ユーザーAPI]

    G[Feature Team B] --> H[注文機能]
    G --> I[決済API]

    J[DevOps Team] --> K[CI/CD]
    J --> L[監視・運用]
継続的な改善
  • 定期的なアーキテクチャレビュー: 四半期ごとの設計見直し
  • パフォーマンス監視: リアルタイムでの性能追跡
  • 開発者体験の向上: ツールチェーンの継続的な改善

失敗パターンの回避

過度な分割
typescript// 避けるべきパターン
const Button = React.lazy(
  () => import('design-system/Button')
);
const Icon = React.lazy(() => import('design-system/Icon'));
// 粒度が細かすぎて管理コストが増大

// 推奨パターン
const DesignSystem = React.lazy(
  () => import('design-system/Components')
);
// 関連コンポーネントをパッケージ化
技術的負債の放置
typescript// 定期的な依存関係の更新
const updateDependencies = {
  schedule: 'monthly',
  scope: ['security-patches', 'minor-updates'],
  process: 'automated-with-review',
};

// 型定義の一元管理
const typeManagement = {
  centralRepository: '@company/shared-types',
  versionSync: 'automated',
  breaking_changes: 'manual-review-required',
};

マイクロフロントエンドアーキテクチャは、適切に設計・運用すれば、大規模な Web アプリケーション開発における多くの課題を解決できる強力な手法です。しかし、その効果を最大化するためには、技術的な実装だけでなく、組織体制やガバナンスの整備が不可欠であることを忘れてはいけません。

TypeScript と最新の Module Federation 2.0、Rspack、Turbopack などのツールを組み合わせることで、型安全で高性能なマイクロフロントエンドシステムを構築できます。この記事で紹介した手法を参考に、皆さんのプロジェクトでも効果的なマイクロフロントエンドアーキテクチャを実現していただければと思います。

関連リンク

公式ドキュメント

  • Module Federation 2.0 - 最新の Module Federation 公式サイト
  • Rspack - 高速な Webpack の代替バンドラー
  • Turbopack - Next.js 開発チームによる超高速バンドラー
  • Single-SPA - マイクロフロントエンドフレームワーク

TypeScript 関連

ビルドツール・パッケージマネージャー

  • Yarn - 高速な JavaScript パッケージマネージャー
  • Yarn Workspaces - モノレポ管理機能
  • Vite - 高速な開発サーバー付きビルドツール

ベストプラクティス・学習リソース

監視・デバッグツール