【実務・中級編】Webpackで実現する『Module Federation』のバージョン競合戦略:共有パッケージの依存関係を制御しランタイムエラーを回避する – ビルド・パッケージ管理ツール生産性向上バイブル

Webpack Module Federationの深層:複数マイクロフロントエンドが共存する環境で「依存関係のバージョン競合」をねじ伏せる技術

テックリードの皆さん、こんにちは。
複数のチームが独立してデプロイサイクルを回す「マイクロフロントエンド」アーキテクトにとって、Webpackの `Module Federation` は革命的な技術です。しかし、この技術の導入によって恩恵を受ける裏で、多くの開発現場が必ず1つの「悪夢」に直面します。

そう、「ランタイムにおける依存関係のバージョン競合」です。

チームAは `react@18.2.0` を軸に新機能を開発し、チームBはレガシーなサードパーティ製ライブラリの都合で `react@18.0.0` を要求する。これをそのままModule Federationの `shared` 設定に放り込むと、ブラウザのランタイム上で異なるバージョンのReactインスタンスがロードされ、伝説のエラーである 「Hooks can only be called inside of the Body of a Function Component」 が発生してアプリケーションが盛大にクラッシュします。

今回は、このバージョン不一致問題を根本から断ち切り、大規模チーム運用でも破綻しないための `Module Federation` の依存管理戦略と、実践的な裏設定をコードベースで徹底解説します。

—

1. なぜ共有パッケージの競合がランタイムエラーを招くのか?

WebpackのModule Federationは、ビルド時ではなくブラウザのランタイム(実行時)にリモートコンテナからコードを非同期に読み込み、ホストとリモート間で依存関係を共有(Share)します。

通常、`shared` オプションにパッケージ名を指定すると、Webpackはセマンティックバージョニング(SemVer)のルールに従って「最も適切なバージョン」を一つだけロードしようとします。しかし、以下のような挙動の裏側を知らなければ、意図しないバージョンの混入を防げません。

  • 自動フォールバックの罠: 共有範囲(Scope)で互換性がないと判定された場合、ホスト側とリモート側で別々のバージョンのモジュールが個別にバンドル・評価されます。
  • シングルトンパターンの破壊: ReactやState管理ライブラリ(Redux, Zustand等)のように、インスタンスがグローバルに1つでなければならないライブラリが複数ロードされると、Contextや内部フックの参照先が分裂し、予期せぬ挙動やクラッシュを引き起こします。

この課題を解決するためには、Webpackの隠された(あるいは使いこなされていない)高度なオプションを駆使する必要があります。

—

2. 現場で即効性を持つ『神設定』:Webpack Module Federationのベストプラクティス

以下に、実務で検証を重ねた堅牢な `Module Federation` の設定ファイル(`webpack.config.js`)の模範解答を示します。それぞれの設定が「なぜ必要なのか」をコード内のコメントと解説で深く掘り下げます。

実践的な `webpack.config.js` 構成例

const HtmlWebpackPlugin = require(‘html-webpack-plugin’);
const { ModuleFederationPlugin } = require(‘webpack’).container;
const path = require(‘path’);
const packageJson = require(‘./package.json’);

module.exports = {
entry: ‘./src/index.js’,
mode: ‘development’,
devServer: {
port: 3000,
historyApiFallback: true,
},
output: {
publicPath: ‘auto’,
uniqueName: ‘host_application’, // 複数MF環境でJSONPのグローバル変数名が衝突するのを防ぐ固有ID
},
resolve: {
extensions: [‘.jsx’, ‘.js’, ‘.json’],
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: ‘babel-loader’,
exclude: /node_modules/,
},
],
},
plugins: [
new ModuleFederationPlugin({
name: ‘host’,
filename: ‘remoteEntry.js’,
remotes: {
// リモートアプリケーションの定義
// 開発環境と本番環境で動的にURLを切り替える設計にしておくことが鉄則
featureA: ‘featureA@http://localhost:3001/remoteEntry.js’,
},
shared: {
// ホスト・リモート間で共有するパッケージの定義
…packageJson.dependencies,

// Reactの厳格なシングルトン強制とバージョン管理
react: {
// package.jsonのバージョンをベースにするが、厳密なルールを課す
singleton: true, // ブラウザ上に単一のインスタンスのみ存在することを強制(複数インスタンス化するとエラー)
strictVersion: true, // trueにすることで、指定バージョンと不一致の場合にランタイムエラーを発生させ、サイレントなバグを防ぐ
requiredVersion: packageJson.dependencies.react, // ホスト側が要求する厳密なバージョン範囲
},

// ReactDOMもReactと同様にシングルトンかつ厳格に管理
‘react-dom’: {
singleton: true,
strictVersion: true,
requiredVersion: packageJson.dependencies[‘react-dom’],
},

// 頻繁に更新されるが後方互換性が高いUIライブラリなどの設定例
‘@mui/material’: {
singleton: true,
strictVersion: false, // 厳格なバージョン一致を強制せず、セマンティックバージョンの許容範囲内なら共有を許可
requiredVersion: packageJson.dependencies[‘@mui/material’],
},
},
}),
new HtmlWebpackPlugin({
template: ‘./public/index.html’,
}),
],
};

—

3. チーム開発で絶対に破綻させないための依存管理戦略

コードレベルの設定だけでなく、複数チームが協調して開発する組織においては、ルールと運用の仕組み化が不可欠です。私たちが現場で導入し、劇的な効果を上げた「3つのルール」を伝授します。

ルール1: `strictVersion: true` による「フェイル・ファスト」の徹底

`strictVersion` を `false`(デフォルト)にしているプロジェクトを多く見かけますが、これは技術的負債の温床になります。「バージョンが微妙に違っても動くだろう」という楽観視は、本番環境でのみ再現する不可解なバグ(状態の共有漏れ、Hooksのエラーなど)を引き起こします。

あえて `strictVersion: true` を指定し、「バージョンが一致しない場合はビルド時、もしくは即座のランタイムエラーとして検知する(フェイル・ファスト)」体制を作ってください。これにより、チーム間の依存関係の不整合が早期に浮き彫りになり、開発初期の段階で強制的にバージョンアップや調整を行う文化が定着します。

ルール2: 依存関係のバージョン管理を「モノレポ(Monorepo)」で一元化する

マイクロフロントエンドにおいて、各リポジトリがバラバラの `package.json` を持ち、野放図に依存パッケージを更新し始めると、Module Federationの共有レイヤーは瞬発的に崩壊します。

これを防ぐ究極の解決策が、TurborepoやNxを用いたモノレポ構成への移行、あるいはマルチリポジトリであれば `npm workspaces` や `pnpm workspaces` による依存関係のホイスティングとバージョン統制 です。

特に `pnpm` は、ハードリンクとシンボリックリンクを活用して厳格な依存関係ツリーを構築するため、Module Federation環境において「意図しないバージョンの幽霊依存(Phantom Dependencies)」を完全に排除できるため強く推奨します。

ルール3: 共有ライブラリの「Singleton強制フラグ」の適切な選定基準

すべてのライブラリに `singleton: true` を設定すれば安全、というわけではありません。これを行うと、リモート側が独自にアップデートした最新のライブラリの恩恵を受けられなくなり、ホスト側のバージョンに縛られることになります。

以下の基準で明確にポリシーを策定してください。

1. Singletonを必ず `true` にすべきもの:

  • React, VueなどのUIフレームワーク本体
  • React Routerなどのルーター(履歴管理やコンテキスト共有のため)
  • Redux Toolkit, Zustand, Recoilなどのグローバル状態管理ストア
  • Emotion, Styled-ComponentsなどのCSS-in-JS(スタイルシートの重複生成を防ぐため)

2. Singletonを `false`(または未指定)にしてもよいもの:

  • Lodash, Day.jsなどの純粋なユーティリティ関数ライブラリ(ステートを持たず、純粋関数であるため複数存在しても問題ない)
  • 独立したUIコンポーネントのプリミティブ(デザインシステムの独立したボタンコンポーネント等で、内部状態が完結している場合)

—

4. トラブルシューティング:バージョン不一致エラーに直面したときのCLI/デバッグ手法

もし万が一、ブラウザのコンソールに以下のようなエラーが出現した場合の、プロエンジニアの迅速なデバッグアプローチを共有します。

Uncaught Error: Shared module not available for eager consumption: [module-name]

あるいは、

The following shared modules are not available: …

1. Webpack Container Debuggerの活用

コンソール上で、現在ロードされているModule Federationのインスタンスと、共有されているモジュールのバージョンマトリクスを確認するためには、ブラウザのコンソールで以下のスニペットを実行します。

// 現在ロードされているWebpackの共有スコープを確認する
console.log(window.__webpack_share_scopes__);

このオブジェクトを展開すると、ホストとリモートがそれぞれどのバージョンのパッケージを提供し、どれが採用されたのか(あるいは競合して弾かれたのか)がツリー構造で一目瞭然になります。

2. `eager: true` の適切な適用とリスク

もし初期ロード時に「Shared module not available for eager consumption」エラーが出た場合、該当する共有パッケージ(特にReactなど)に対して `eager: true` を設定することで、非同期チャンクとしてではなく、初期エントリポイントに直接バンドルさせることができます。

react: {
singleton: true,
strictVersion: true,
eager: true, // ホストアプリケーションのエントリポイントで直ちに評価させる
}

※ただし、すべての共有モジュールに `eager: true` を付与すると、Module Federationの最大のメリットである「初期ロードの軽量化」が失われるため、ホスト側のエントリポイントにおける必須依存関係にのみ限定して適用するのがプロの技です。

—

5. おわりに

WebpackのModule Federationは、単なる「コードの動的読み込みツール」ではありません。組織のスケールとチーム間の疎結合なデプロイを支える、高度なガバナンス機構です。

今回解説した `singleton`、`strictVersion`、そしてモノレポ等によるバージョン統制の思想をチームの共通認識として持ち、設定ファイルに厳格に落とし込むことで、マイクロフロントエンド特有のバージョン競合地獄から完全に脱却することができます。

あなたのプロジェクトのビルドパイプラインとランタイムの安定性を、今すぐ最高峰のアーキテクチャへと引き上げましょう。

タイトルとURLをコピーしました