以下の内容はhttps://kakehashi-dev.hatenablog.com/entry/2025/12/02/150000より取得しました。


爆速でプロダクトをリリースしようと思ったらマイクロフロントエンドを選んでいた

こんにちは。生成AI研究開発チームでソフトウェアエンジニアをしているNokogiri(@nkgrnkgr)です。

本記事は、2025年9月21日に開催されたフロントエンドカンファレンス東京での発表内容をブログ記事としてまとめたものです。

発表資料はこちらです。

はじめに

カケハシでは、約10年運用されているクラウド型電子薬歴システム「Musubi」に生成AI機能を搭載するプロジェクトに取り組みました。その際に採用したのがマイクロフロントエンドというアーキテクチャです。

本記事では、なぜマイクロフロントエンドを選んだのか、どのように実装したのか、そして実際に導入して見えてきた課題と工夫について紹介します。

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

マイクロフロントエンドは、Martin Fowlerのブログで以下のように定義されています。

"An architectural style where independently deliverable frontend applications are composed into a greater whole"

「独立してリリース可能なフロントエンドアプリケーションをより大きな全体に構成するアーキテクチャスタイル」

引用:https://martinfowler.com/articles/micro-frontends.html

私たちはこれを「独立して開発、デプロイ、テスト、保守できるアーキテクチャ・組織戦略」として解釈しました。

なぜマイクロフロントエンドを採用したか

Musubiの特徴

Musubiは全国の薬局の約20%で利用されているクラウド型電子薬歴システムです。薬局の業務を支えるミッションクリティカルなシステムであり、高い品質と安定性が求められます。そのため、丁寧な検証と月次の計画的なリリースサイクルで運用されています。

生成AI機能開発の要件

一方で、今回開発する生成AI機能には以下のような特性がありました。

  • ユーザーの価値検証をすばやく繰り返したい
  • 技術的な不確実性が高い
  • 必要なスキルセットが既存と大きく異なる

つまり、小さく試して小さく失敗するトライを繰り返したいという要件がありました。

相反する要件を両立させる

安定性を重視するMusubiに、試行錯誤をしたい生成AI機能を搭載するにはどうすればよいか?

この問いに対する答えとして、両者を独立して開発、デプロイ、テスト、保守できるアーキテクチャ・組織戦略が必要でした。

結果として選択した戦略は以下の通りです。

戦略 内容
体制 Musubiと生成AI機能は開発チームを別にする
デリバリー リリースをMusubiと別にし、生成AI機能単体でリリース可能
UI Musubi非依存のUIとし開発し影響を受けにくくする
実装 Angularではなく、Reactに長けたメンバーがReactで開発する

チーム・アーキテクチャの独立性を重視した結果、マイクロフロントエンドという選択に至りました。

マイクロフロントエンドを支える技術

AngularとReactアプリを共存させるアーキテクチャ

MusubiはAngularで構築されていますが、生成AI機能はReactで開発することにしました。これを共存させるために以下のアーキテクチャを採用しています。

  1. ViteでビルドしたReactアプリのJSとCSSを事前にCDNにホスティング
  2. Angularアプリにあらかじめ <div id="react-component" /> を用意
  3. JS、CSSファイルをロードし、divタグに対してReactのJSがレンダリング

これにより、AngularとReactを共存させています。

Musubi非依存UI

生成AI機能のUIは、MusubiのUIの中に組み込むのではなく、"上"に配置して動作するアプリを目指しました。これにより、Musubiの変更の影響を受けにくくしています。

アプリケーション間通信

AngularとReact間の通信にはCustomEventを使用しています。

// 送信側
const event = new CustomEvent("contextChanged", {
  detail: {
    payload: {
      pharmacyId: "pharmacy-123",
      patientId: "patient-456"
    }
  }
});
window.dispatchEvent(event);

// 受信側
window.addEventListener("contextChanged", (event) => {
  const { pharmacyId, patientId } = event.detail.payload;
  console.log("受信:", pharmacyId, patientId);
});

CustomEventを選んだ理由は以下の通りです。

  • Angular、Reactに依存しないWeb標準の技術
  • アプリケーション固有のイベントを定義して情報のやりとりができる
  • 直接的な依存関係がなくともやりとりができるので疎結合にできる

実践編:導入して見えた課題と工夫

型定義で通信仕様を固める

CustomEventのキーとペイロードの組み合わせを型定義し、Mapped Typeを使って型安全に管理しています。

export const CustomEventName = {
  ContextChanged: "context_changed",
  NavigationRequested: "navigation_requested",
} as const;

export type ContextChangedPayload = {
  patientId: string | null;
}

export type NavigationRequestedPayload = {
  pageId: string;
}

export type CustomEventMap = {
  [CustomEventName.ContextChanged]: ContextChangedPayload;
  [CustomEventName.NavigationRequested]: NavigationRequestedPayload;
};

この定義内容をチーム間で合意することで、型安全な通信を実現しています。

アプリ間のページ遷移の同期

Angular(メイン)とReact(サブ)の両方にページ概念があります。例えば、メインで患者Aのページを表示するときは、サブも患者Aの情報を出す必要があります。

基本的にはメインのページ遷移時にサブが追従するルールにしていますが、サブがメインにページ遷移を要求したい場合もあります。両方がページ遷移を要求しあっても上手く制御できる機構が必要でした。

CustomEventの受領確認

CustomEventは基本的に「投げっぱなし」です。しかし送信側が「受信側が受け付けたか」を知りたい場面があります。

そこで、以下のような仕組みを実装しました。

  1. 送信側:一意なID付きでEventを発火
  2. 受信側:イベントを受け取り、処理を実行
  3. 受信側:同じIDを含む応答イベント(受領通知)を musubi_ack_${id} で返す
  4. 送信側:ackを待ち受け、結果を受け取ってUIやログに反映
// 送信側
const emitEvent = (payload: Payload) => {
  return new Promise((resolve, reject) => {
    const eventId = generateRandomId();
    window.addEventListener(getAckId(eventId), (e) => resolve(e), { once: true });
    window.dispatchEvent(new CustomEvent(EVENT, { detail: { id: eventId, payload } }));
  });
}

const result = await emitEvent({ hoge: 'hoge' });
console.log(result); // => { status: "OK", message: "受信しました" }
// 受信側
window.addEventListener(EVENT, (e) => {
  const { id, payload } = (e as CustomEvent).detail;
  // payload に応じた処理
  const result = { status: "OK", message: "受信しました" };
  window.dispatchEvent(new CustomEvent(`${getAckId(id)}`, { detail: { payload: result } }));
});

CustomEventのデバッグ用Chrome拡張機能

CustomEventのやり取りは、どんなイベントが発生したかを確認しづらいという課題がありました。そこで、イベントの流れを可視化するChrome拡張機能を作成しました。

DevToolsに「Custom Events」タブを追加し、イベント名、タイムスタンプ、ペイロードの内容をリアルタイムで確認できるようにしています。

ローカル開発環境:読み込むJS/CSSを差し替え

AngularがReactのJSとCSSを動的に読み込む仕組みにしているため、URLを切り替えることで任意の環境の組み合わせができます。

chrome.declarativeNetRequest というAPIを使い、AngularがロードするJS/CSSファイルのURLをランタイムで動的に差し替えるChrome拡張機能を作成しました。

例えば、以下のような組み合わせで動作確認ができます。

Angularはデプロイされた検証環境、Reactはローカル開発環境

これにより、効率的な開発が可能になっています。

その他の泥臭い工夫

マイクロフロントエンドの導入には、他にも以下のような泥臭い対応が必要でした。

  • 一部でネットワーク通信のような振る舞いが必要
  • 生成AI機能チームが自らMusubi側のコードを修正し、PRを投げる
  • グローバルCSSの影響で意図せずレイアウトが崩れる
  • z-indexの管理が煩雑

まとめ

マイクロフロントエンドを選んだ結果、以下の成果を得ることができました。

  • 多くの苦労はあったものの、開発着手から4か月でリリースできた
  • ユーザーからのフィードバックを受け、毎週リリースして素早く改善している
  • 1つのアプリケーションにAngularアプリとReactアプリの共存ができた
  • 異なるミッションを持ったチームが、独立して開発を継続できる体制が整った

伝えたいこと

  • まずビジネスとして達成したいゴールを定め、そこからアーキテクチャ・戦略を選ぶことが大切
  • ビジネスの要求に応えるために、このような選択肢もあることを知っておいてほしい

マイクロフロントエンドは銀の弾丸ではありませんが、適切なコンテキストでは非常に強力な選択肢になります。皆さんのプロダクト開発の参考になれば幸いです。

参考リンク

CustomEventやローカル開発環境の詳細は、以下のブログにも詳しく記載しています。




以上の内容はhttps://kakehashi-dev.hatenablog.com/entry/2025/12/02/150000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14