ReactとModule Federationによる大規模フロントエンドアプリケーションのスケール手法

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

モノリスのボトルネック

フロントエンドアプリケーションをスケールさせることは、時に狭い運河を巨大な貨物船で操縦しようとする感覚に似ています。私がエンタープライズ向けのReactプロジェクトに携わり始めた頃、すべての機能は肥大化した単一のリポジトリ(モノリス)に収められていました。

当初はそれでうまくいっていました。しかし、チームが5人から50人に拡大すると、開発の停滞が物理的な摩擦として現れ始めました。ビルド時間は2分から20分以上に伸び、マージコンフリクトは日常的な「睨み合い」の場となり、フッターにある小さなCSSのバグが予期せずチェックアウトフロー全体をクラッシュさせることもありました。

モノリス構成は最終的に限界に突き当たります。内部的なNPMパッケージに分割することで解決を図ることも多いですが、そのアプローチには大きな欠点があります。それは、パッケージを更新するたびにホストアプリケーションの再ビルドと再デプロイが必要になることです。私たちは、システム全体の安定性を危険にさらすことなく、UI機能を個別にデプロイする方法を必要としていました。マイクロフロントエンドは、そのための脱出口となりました。

Module Federationへの移行

マイクロフロントエンドは、巨大なWebアプリを小さく自己完結した断片に分割し、エンドユーザーには単一の製品として見せる手法です。開発者は長年 iframeやサーバーサイドでの合成を試みてきましたが、Webpack 5が「Module Federation」というよりエレガントな解決策を導入しました。

この技術により、JavaScriptアプリケーションは実行時に別のビルドからコードを動的にロードできるようになります。すべての依存関係がビルド時に存在しなければならない従来のバンドル手法から脱却し、コンポーネント、フック、あるいはページ全体を異なるサーバー間で共有できるようになります。本番環境において、このアーキテクチャによってチームが他部署と調整することなく1日に10回のデプロイを実現している例を私は見てきました。

これをマスターするには、3つの主要な役割を理解する必要があります。

  • Host: エントリポイントとして機能し、リモートの断片を「消費」するシェルアプリケーション.
  • Remote: 特定のコンポーネントやロジックを「公開(expose)」する独立したアプリケーション.
  • Shared: ReactやMaterial UIなどの共通の依存関係。帯域幅を節約するために一度だけロードされ、エコシステム全体で共有されます。

初めてのマイクロフロントエンド・アーキテクチャの構築

理論はこれくらいにして、実装を見ていきましょう。今回は2つのアプリを構築します。Remote(製品カード)と、Host(メインのダッシュボード)です。

ステップ1:Remoteアプリのセットアップ

まず、リモートアプリケーション用のディレクトリを作成し、初期化します。複雑な処理はWebpack 5に任せます。

mkdir remote-app && cd remote-app
npm init -y
npm install react react-dom webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/preset-react --save-dev

次に、src/Button.jsにシンプルなコンポーネントを作成します。

import React from 'react';

const RemoteButton = () => (
  <button style={{ padding: '12px 24px', background: '#0070f3', color: 'white', border: 'none', borderRadius: '5px' }}>
    ライブ・リモートボタン
  </button>
);

export default RemoteButton;

設定はwebpack.config.jsで行います。このファイルで、どのコンポーネントを公開するかをWebpackに指示します。

const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  mode: 'development',
  devServer: { port: 3001 },
  module: {
    rules: [
      { test: /\.js$/, loader: 'babel-loader', options: { presets: ['@babel/preset-react'] } }
    ]
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/Button',
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
    new HtmlWebpackPlugin({ template: './public/index.html' }),
  ],
};

ステップ2:Hostアプリの接続

Hostアプリケーションは、RemoteからButtonを取得します。ポート3000を使用するhost-appという別のフォルダをセットアップします。Hostの設定では、RemoteのURLを指定する必要があります。

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  // ... 標準的な設定
  plugins: [
    new ModuleFederationPlugin({
      name: 'hostApp',
      remotes: {
        remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
  ],
};

ステップ3:ランタイム統合

Hostアプリでは、ReactのlazySuspenseを使用してリモートコンポーネントをインポートします。コードはネットワーク経由で取得されるため、レイテンシを処理するためのローディング状態が必要です。

import React, { Suspense } from 'react';

const RemoteButton = React.lazy(() => import('remoteApp/Button'));

const App = () => (
  <div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
    <h1>エンタープライズ・ダッシュボード (Host)</h1>
    <Suspense fallback="リモートコンポーネントを取得中...">
      <RemoteButton />
    </Suspense>
  </div>
);

export default App;

共有ステートと依存関係の管理

バージョン不一致は、フェデレーション環境で最も一般的な落とし穴です。HostがReact 18を実行しているのに、RemoteがReact 17を強制的にロードしようとすると、アプリケーションはクラッシュする可能性が高くなります。ここでsingleton: trueフラグを使用することは必須です。これにより、複数のアプリが要求しても、Webpackはライブラリのインスタンスを1つだけロードするように保証します。

ステートに関しては、シンプルに保つことをお勧めします。グローバルなステートはHostレベルで管理し、props経由でデータを渡すのが理想的です。マイクロフロントエンド同士が密結合にならずに通信する必要がある場合は、軽量なカスタムイベントバスやmittのようなライブラリを使用してください。単一の巨大なReduxストアをアプリケーション間で共有してはいけません。それを行うと、隠れた依存関係が生じ、「独立性」という目的が損なわれてしまいます。

終わりに

Module Federationの採用は、単なる技術的な選択ではなく戦略的な決断です。CI/CDパイプラインに複雑さをもたらし、チームにはWebpackの内部構造に対する深い理解が求められます。しかし、大規模な組織にとって、そのトレードオフには十分な価値があります。ビルドの高速化と、チームが独自のリリースサイクルを持てるようになるというメリットが得られるからです。

まずは小さく始めましょう。リスクの低いユーティリティや単一のナビゲーションコンポーネントから移行してみてください。新機能をリリースするためにコードベースの5%だけをビルドすれば済むというデプロイを一度経験すれば、二度とモノリスには戻りたくなくなるはずです。

Share: