Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

マイクロフロントエンド入門:プロジェクトが1人では管理できなくなったとき

背景

私たちの管理システムは3年前に Vue 2 で構築しました。最初は数ページしかなく、快適でした。しかし3年後、それは巨大なモンスターになっていました:

  • 50以上のルーティングページ
  • 200以上のコンポーネント
  • 3チーム、12人が同時に開発
  • Webpack のビルドが最低4分
  • メインバンドルが3MB以上で、初回表示がどんどん遅くなる

さらに悪いことに、毎回のリリースが全量デプロイでした。受注モジュールで文言を1箇所変えただけで、システム全体を再デプロイする必要がありました。先週、在庫モジュールがバグをリリースしてバックオフィス全体をダウンさせましたが、私たちのユーザーモジュールとは全く関係がなかったのに、全員のチケットシステムが崩壊しました。

チームの誰かが冗談で言いました:「このプロジェクトはもう1人で管理できない。」実は冗談ではありませんでした。

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

マイクロフロントエンドの概念は、本質的にバックエンドのマイクロサービスの考え方をフロントエンドに持ち込むことです。

バックエンドはすでにモノリスからマイクロサービスへの分割を経験しました。大きな Java アプリを複数の独立してデプロイ可能なサービスに分割し、それぞれが独自のデータベースと独自のリリースサイクルを持ちます。フロントエンドも今まさにその段階に来ています。

コアコンセプト:

従来のモノリシック SPA:
┌─────────────────────────────────────┐
│        モノリシックフロントエンドアプリ │
│  ┌──────┐ ┌──────┐ ┌──────┐        │
│  │ユーザー│ │注文  │ │在庫  │        │
│  └──────┘ └──────┘ └──────┘        │
└─────────────────────────────────────┘
1つのリポジトリ、1つのビルド成果物、1つのデプロイ単位

マイクロフロントエンド:
┌─────────────────────────────────────┐
│        ホストアプリ(シェル)          │
│  ┌──────────┐ ┌──────────┐          │
│  │ユーザーApp │ │注文App   │          │
│  │独自リポジトリ│ │独自リポジトリ│          │
│  │独自デプロイ │ │独自デプロイ │          │
│  └──────────┘ └──────────┘          │
└─────────────────────────────────────┘
各モジュールが独立して開発・ビルド・デプロイ

各サブアプリは独自の技術スタックと独自のリリースサイクルを持ち、チーム間で互いに干渉しません。

既存アプローチの比較

着手する前に、1週間かけていくつかのアプローチを調査しました。

アプローチ1:iframe

最も古くて直接的な方法。ホストアプリがナビゲーションフレームを提供し、サブアプリは iframe で読み込みます。

html
<!-- ホストアプリのシェル -->
<div id="layout">
  <sidebar-nav />
  <main>
    <iframe :src="currentAppUrl" frameborder="0"></iframe>
  </main>
</div>

分離性は非常に優れています — スタイル、JS、グローバル変数が完全に独立しています。デメリットも明らかです:ルート同期が困難、iframe の境界でモーダルが切り取られる、アプリ間通信は postMessage のみ、iframe のローディング体験が悪い。

アプローチ2:Nginx ルートディスパッチ

異なるパスを異なるフロントエンドアプリのデプロイにルーティング:

nginx
location /user/ {
  proxy_pass http://user-app-server/;
}
location /order/ {
  proxy_pass http://order-app-server/;
}

シンプルですが、パス切り替え時にページ全体がリロードされ、UX が悪化します。共通部分(ナビゲーション、ユーザー情報)を各サブアプリで再実装する必要があります。

アプローチ3:npm パッケージ分割

共通モジュールを npm パッケージとして抽出し、各チームが独自のリポジトリを管理して、最終的にホストアプリで組み立てます。

このアプローチはビルドパイプラインへの変更が最も少ないですが、本質的に独立デプロイの問題を解決しません — どのモジュールが更新されても、ホストアプリは再ビルド・再デプロイが必要です。

アプローチ4:single-spa

最近注目している single-spa というプロジェクトはランタイムフレームワークを提供し、複数のフロントエンドアプリが同じページに共存できるように、ライフサイクル管理でマウントとアンマウントを調整します:

javascript
import { registerApplication, start } from "single-spa";

registerApplication(
  "user-app",
  () => System.import("@org/user-app"),
  (location) => location.pathname.startsWith("/user"),
);

registerApplication(
  "order-app",
  () => System.import("@org/order-app"),
  (location) => location.pathname.startsWith("/order"),
);

start();

サブアプリは bootstrapmountunmount の3つのライフサイクルフックを公開する必要があります。コンセプトは良いですが、コミュニティはまだ小さく、ドキュメントも不十分で、Vue サポートには追加の適応が必要です。有望な方向性ですが、現時点では本番環境へのリスクは小さくありません。

私たちの試み:iframe PoC

チームの現在の技術力とリスク許容度を考慮して、最も保守的な iframe アプローチで PoC を実施し、ユーザー管理モジュールをメインアプリから分離することにしました。

全体アーキテクチャ:

┌────────────────────────────────────────────┐
│  ホストアプリ シェル(Vue 2)               │
│  ┌──────────────────────────────────────┐  │
│  │  トップナビ + サイドバー             │  │
│  └──────────────────────────────────────┘  │
│  ┌──────────────────────────────────────┐  │
│  │                                      │  │
│  │  <iframe :src="userAppUrl" />        │  │
│  │                                      │  │
│  └──────────────────────────────────────┘  │
└────────────────────────────────────────────┘
        │                        │
        ▼                        ▼
   ホストアプリデプロイ        ユーザーサブアプリデプロイ
(他の40以上のページ)    (Vue 2、独立リポジトリ)

重要な課題はログイン状態の共有でした。私たちのアプローチ:ログイン後、ホストアプリが .company.com ドメインスコープのクッキーにトークンを書き込み、サブアプリがクッキーから読み取ります:

javascript
// ホストアプリ:ログイン成功後
document.cookie = `auth_token=${token}; domain=.company.com; path=/`;

// サブアプリ:起動時に読み取り
function getAuthToken() {
  const match = document.cookie.match(/auth_token=([^;]+)/);
  return match ? match[1] : null;
}

// サブアプリ:リクエスト時にトークンを付与
axios.interceptors.request.use((config) => {
  const token = getAuthToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

サブアプリ間は postMessage で通信します:

javascript
// ホスト → サブアプリ:ユーザー情報を渡す
iframe.contentWindow.postMessage(
  { type: "USER_INFO", payload: { userId: 123, role: "admin" } },
  "https://user.company.com",
);

// サブアプリのリスナー
window.addEventListener("message", (event) => {
  if (event.data.type === "USER_INFO") {
    store.commit("setUserInfo", event.data.payload);
  }
});

// サブアプリ → ホスト:ルートナビゲーションをトリガー
window.parent.postMessage(
  { type: "NAVIGATE", payload: "/order/detail/456" },
  "https://admin.company.com",
);

踏んだ落とし穴

PoC 中にいくつかの印象的な問題に遭遇しました。

1. iframe の高さ自動調整

iframe はデフォルトでコンテンツに合わせて伸びません。height: 100% を設定するとダブルスクロールバーが発生します。対策として、サブアプリが postMessage でホストにコンテンツの高さを通知します:

javascript
// サブアプリ:コンテンツの高さが変化したときにホストに通知
const observer = new ResizeObserver(() => {
  window.parent.postMessage(
    { type: "RESIZE", height: document.body.scrollHeight },
    "*",
  );
});
observer.observe(document.body);

// ホスト:高さを受け取って iframe のスタイルを設定
window.addEventListener("message", (event) => {
  if (event.data.type === "RESIZE") {
    iframe.style.height = event.data.height + "px";
  }
});

2. モーダルとオーバーレイの切り取り

iframe 内のモーダルやトーストは iframe の境界に制限され、画面全体をカバーできません。これが iframe ベースのマイクロフロントエンドで最も頭を悩ます問題です。妥協策として、モーダルをホストアプリで実装し、サブアプリが postMessage でホストにモーダル表示を依頼します。ただし、これはカップリングを増やします。

3. ブラウザの前進/後退

iframe 内のルート変更はブラウザの履歴に記録されません。ホストとサブアプリのルートを同期させる必要があります — サブアプリがルート変更をホストに通知し、ホストが URL クエリパラメーターを更新し、iframe がクエリパラメーターに基づいてナビゲートします。ロジックが複雑で保守しにくいです。

マイクロフロントエンドはいつ使うべきか

これだけの落とし穴があっても、マイクロフロントエンドは価値がないのでしょうか?そうではありません。ただし、すべてのプロジェクトに適しているわけでもありません。

適したシナリオ:

  • 複数チームが大型フロントエンドアプリを保守していて、コラボレーションの衝突が頻繁
  • 異なるモジュールが独立してリリースする必要があり、デプロイリスクを低減したい
  • レガシーシステムを段階的に移行したい(例:jQuery から Vue へ)
  • モジュール間の境界が明確で、クロスモジュールのやり取りが比較的シンプル

適さないシナリオ:

  • プロジェクトが小さく、2〜3人で管理できる — 過剰設計はしない
  • モジュール間に複雑な連携がある — 分割後の通信コストが高くなる
  • 複数のリポジトリと複数のデプロイパイプラインをサポートする DevOps 基盤がない

つまり、マイクロフロントエンドが解決するのはチームコラボレーションと独立デリバリーという組織的な問題であり、純粋な技術的問題ではありません。モノリスに「首を絞められていない」チームは、急いでマイクロフロントエンドを導入する必要はありません。

まとめ

  • マイクロフロントエンドはバックエンドのマイクロサービスの考え方をフロントエンドに持ち込み、大規模アプリの各モジュールを独立して開発・ビルド・デプロイできるようにする
  • 2018年の主要アプローチ:iframe、Nginx ルートディスパッチ、npm パッケージ分割、JS ランタイム統合
  • iframe アプローチは最もシンプルで分離性も最高だが、ルート同期・モーダル切り取り・通信コストが課題
  • single-spa は有望なランタイム統合フレームワークだが、まだ成熟していない — 注目する価値はあるが採用は慎重に
  • マイクロフロントエンドは本質的にチームコラボレーションと独立デリバリーの問題を解決する。小さなプロジェクトには不要
  • 導入を決めた場合は、境界が明確な1つのモジュールから PoC を始め、最初から全量分割しない

MIT Licensed