この記事は、Bill One開発Unit ブログリレー2025の第5弾、および Sansan Advent Calendar 2025、3日目の記事です。

こんにちは。技術本部 Bill One Engineering Unit の柳浦です。2022年にSansanへ中途入社し、入社以来Webアプリケーション開発エンジニアとしてBill Oneの開発に従事しています。
Bill Oneのフロントエンド開発では、長年にわたって成長してきたモノリシックなReact SPAを分割し、マイクロフロントエンドアーキテクチャへ移行するプロジェクトに取り組んでいます。 本記事では、その過程で開発した自動化ツールと移行戦略について解説します。
背景: 巨大になったSPAが Bill One開発のボトルネックに
Bill Oneのフロントエンドは、請求書管理を中心とした複数の業務機能を提供するモノリシックなReact SPAとして成長してきました。 現在は27のチーム、約100名のエンジニアがこのSPAの開発に関与しており、ソースコードファイル数10,000以上、200以上の画面ページと500を超えるコンポーネントから構成されています。
この規模に達したことで、次のような深刻な問題が顕在化していました。
- リリースタイミングの制約: 単一アプリケーションのリリースとなるため、リリース時間枠の調整が難しく、リリースしたいタイミングでリリースできない状況が発生していました。
- チームごとのオーナーシップ醸成の困難さ: 単一コードベースであるがゆえにチームごとの責任範囲が曖昧になり、改善を進めたくても着手しづらい状況でした。
- コンテキストの肥大化: モノリシックなSPA構成は、コードベース全体が単一の巨大コンテキストとなる。このため、AIに対する効果的なコードベースの絞り込みが困難になっていた。
マイクロフロントエンドへの移行を決断
これらの課題を解決するため、業務単位でSPAを分割し、チームが独立してデプロイできるマイクロフロントエンドアーキテクチャへの移行を決断しました。
Bill Oneには大きく分けて次の3つの主要な業務ドメインが存在します。
- 請求書受領 (Accounts Payable, AP)
- 債権管理 (Accounts Receivable, AR)
- 経費精算 (Expense Reimbursement, EX)
これらの業務ドメインはそれぞれ明確に異なるユーザー業務を持っています。特にAPとARは請求書の受領と発行という明確に異なるドメインであったため、SPA分割の第一歩としてARドメインを独立したSPAとして切り出すことにしました。 マイクロフロントエンドアーキテクチャにおける垂直分割 (Vertical Split)と呼ばれる方式です。
分割することで、次の3つの効果を狙っています。
- リリースの分散・独立化
- 領域ごとのオーナーシップ醸成
- AI活用の促進
SPA分割の課題
巨大なSPAを業務単位に分割するためには、次の課題を解決する必要がありました。
- PBI開発を止めない: 日々数十〜数百の差分が発生する中、開発を止めずに分割を進める必要がありました。数日mainブランチを追従しないだけで多数のコンフリクトが発生してしまう状況でした。
- 複雑な依存関係: 長年の開発によって想定外の依存関係が多数生まれ、数千ものファイルを手作業で紐解くこと自体が現実的ではありませんでした。
- 段階的な分割の仕組み: 4つ以上のSPAに分割する計画があり、再現可能で自動化された仕組みが必要でした。AIに一度依頼するだけでなく、継続的に分割を進められる仕組みが不可欠でした。
課題解決のアプローチ: オリジナルSPAから新規SPAを動的に生成する
これらの課題を解決するため、次の方針を採用しました。
方針1: 既存SPAから新規SPAを動的に生成
既存のSource SPA(現在のモノリシックなSPA)を起点に、静的解析で必要なファイルだけを抽出して新規SPAを生成します。エントリーポイントから依存解析を行い、必要なファイルだけをコピーしてDestination SPA(新規SPA)を作成し、同時に元のSPAからAR関連ファイルを削除します。
これら一連の操作をスクリプト化することで、常に最新のコードから分割後のSPAを動的に生成できるようにしました。これにより、SPA分割に伴う動作検証やCI/CDの見直しなどの作業とPBI開発を並行して進めることができるようになりました。
方針2: 依存関係の問題は段階的に解決
依存関係が複雑なままでも、まずは業務単位で分割し、分割後の小さくなったコードベースで問題を1つずつ整理していくアプローチを採用しました。分割後のSPAはコードセットが小さくなるため問題の切り分けが容易になり、問題が見つかれば元のSPAにフィードバックして修正し、再度分割ツールを実行することで、段階的に依存関係を整理できます。
大規模システムでは依存関係の問題を事前に全て解決してから分割することは困難なため、まずは粗く領域ごとに分け、スコープの小さな環境で問題を逐次解決する方が現実的でした。
分割ツールの処理手順
分割ツールは大きく2つの操作を提供しているので、それぞれの操作の処理手順をご紹介します。なお、分割ツールはファイルの移動ではなく、コピーと削除を別々に行います。これは、Source SPAとDestination SPAで共通のファイル(共通コンポーネントやユーティリティなど)があるためです。
Destination SPA(ARのSPA)の生成操作
生成操作の処理手順は次のとおりです。
- Source SPAのrouting実装をAR専用に一時的に差し替える(source-overrides)
- エントリーポイントから依存関係を静的解析(madgeを利用)
- 必要なファイルのリストを生成
- rsyncで必要なファイルだけをコピーしてDestination SPAを作成する
- 分割先(target)の本番用の設定ファイルを配置(target-overrides)
- 不要なファイルの確認(knipを利用)
Step1: Source SPAのrouting実装をAR専用に一時的に差し替える
依存解析時にrouting実装を差し替えることで、Step2において必要最小限の依存関係(importによるコード参照)だけを抽出できるようになります。
分割ツールは、依存解析時と実行時で異なるファイルを使用します。source-overrides/ ディレクトリに配置されたファイルは、依存解析の直前に一時的に元のSPAに上書きコピーされ、解析後は自動的に元に戻されます。これにより、実際のアプリには影響を与えずに、解析対象を制御できます。
Bill OneのフロントエンドはReact Routerを使用しており、routing実装ファイル内で各機能をlazyでimportしています。例えば、元のrouting実装では次のようにすべての機能を読み込みます。
// 元のrouting実装 const AccountsReceivableRoutes = lazy(() => import('./accounts-receivable')) // AR機能 const AccountsPayableRoutes = lazy(() => import('./accounts-payable')) // AP機能 const ExpenseRoutes = lazy(() => import('./expense')) // EX機能
これをAR専用のrouting実装に差し替えることで、AR機能のみを依存解析の対象にします。
// AR専用のrouting実装(差し替え後) const AccountsReceivableRoutes = lazy(() => import('./accounts-receivable')) // AR機能のみ
Step2: エントリーポイントから依存関係を静的解析
依存関係の抽出には、TypeScriptの静的解析ツールであるmadgeを使用しています。
madgeによる依存解析では、まずエントリーポイントとなるファイルを決定します。entry-points.txt には、AR機能の起点および基本構成ファイル(package.json、tsconfig.json、.storybook/、config/、src/index.tsx など)をあらかじめ定義しています。
例えば、entry-points.txt には次のようなエントリーポイントと基本構成ファイルが記載されています。
+ package.json + tsconfig.json + .storybook/ + .storybook/** + config/ + config/** + src/ + src/index.tsx + src/global.d.ts + src/vite-env.d.ts + src/react-app-env.d.ts
これらは rsyncの--include-fromで使用可能な形式で記述しており rsync のdry-runでコピー対象ファイルを抽出できます。対象ファイルのうちの ts, tsxファイルすべてをエントリーポイントとして扱います。
個別のページコンポーネントやAPIファイルはエントリーポイントから参照されているため、madgeが自動的に依存として検出します。なお、Step1で差し替えたAR専用のrouting実装も、src/index.tsxから参照されているため自動的に含まれます。
ただし、.stories.tsx、.test.tsx、.spec.tsx といったテスト・Storyファイルはエントリーポイントから意図的に除外しています。これらのファイルは、本番コードから参照される場合のみ依存として自動検出されるため、不要なテストコードが含まれることを防げます。
madgeは、tsconfig.jsonのパスエイリアスを解決しながら、エントリーポイントから推移的な依存関係を追跡します。
Step3: 必要なファイルのリストを生成
madgeの解析結果から、エントリーポイントから依存関係にある全ファイルパスを dependency-list.txt に出力します。
例えば、index.tsx がエントリーポイントの場合、madgeは次のように依存を推移的に追跡します。
index.tsx
→ routes.ts
→ InvoiceListPage.tsx
→ InvoiceTable.tsx
→ InvoiceRow.tsx
→ Button.tsx
→ Invoice.ts
→ useInvoices.ts
→ invoiceAPI.ts
→ apiClient.ts
madgeはこのような依存関係をすべて辿り、最終的に dependency-list.txt に出力されます。出力されるのは、rsyncの--include-fromで使用可能な形式のファイルパスと上位ディレクトリのリストです。
+ src/ + src/routes.ts + src/pages/ + src/pages/accounts-receivable/ + src/pages/accounts-receivable/InvoiceListPage.tsx + src/components/ + src/components/InvoiceTable.tsx + src/components/InvoiceRow.tsx + src/components/atoms/ + src/components/atoms/Button.tsx + src/types/ + src/types/Invoice.ts + src/hooks/ + src/hooks/useInvoices.ts + src/api/ + src/api/accounts-receivable/ + src/api/accounts-receivable/invoiceAPI.ts + src/utils/ + src/utils/apiClient.ts
Step4: rsyncで必要なファイルだけをコピー
rsyncは、2回に分けて実行されます。
1回目のrsyncでは、entry-points.txt を使用して基本構成ファイルをコピーします。
2回目のrsyncでは、dependency-list.txt を使用して、madgeで解析されたTypeScript依存ファイルを追加コピーします。2段階に分けることで、1回目でコピーされたファイル(特にStep1で差し替えたrouting実装)を2回目で上書きせず、不足しているファイルだけを追加できます。
なお、Step1で一時的に差し替えたファイルは、madgeの解析完了後に自動的に元に戻されます。
この仕組みにより、ビルドに必要な設定ファイルと、コードから参照されるすべてのファイルを漏れなくコピーできます。
Step5: 分割先の本番用の設定ファイルを配置
分割先(target)の設定として、target-overrides/ ディレクトリのファイルが、生成されたDestination SPAに上書きコピーされます。これには次のようなAR専用の設定が含まれます。
- vite.config.ts(AR専用のビルド設定)
- index.html(AR専用のエントリーHTML)
- README.md(ui-arアプリの説明)
Step6: 不要なファイルの確認
分割後のSPAに不要なファイルが含まれていないかを確認するため、knipを使用しています。madgeはimport関係(コード参照)を追跡するツールですが、knipは未使用のexportやファイルを検出し削除する機能を持ちます。これにより、madgeでは拾いきれない不要なコードも削除できます。
Source SPAからの削除操作
削除操作の処理手順は次のとおりです。
- routing実装からARへのルートを削除
- あらかじめ定義しておいた削除対象のファイルパターンに基づいて削除
Step1: routing実装からARへのルートを削除
元のSPAのrouting実装から、AR機能へのルートを削除します。
Step2: 削除対象のファイルパターンに基づいて削除
あらかじめ定義しておいたAR関連のファイルパターンに基づいて、元のSPAからAR関連のファイルを削除します。
ファイルパターンはディレクトリ単位での指定(例: src/api/accounts-receivable/)に加え、! プレフィックスによる除外パターンもサポートしています。例えば、!src/pages/accounts-receivable/settings/ と指定すると、settings/ だけは残すことができます。
この操作はknipでも可能ではありますが、Source SPAはコードの整理が十分にできていないため、AR以外の機能(受領、経費など)についても多数の未使用コードが検出され、確認箇所が膨大になります。そのため、確実にルールベースで削除するようにしています。
これらの操作をしたうえで、最後にknipで削除漏れのファイルがないかを手動で確認し、削除しています。空になったディレクトリは自動的にクリーンアップされます。
結果
分割の数値結果
- 分割前: 10,421ファイル(2025/10/19時点)
- 分割後(2025/11/19時点):
- Source SPA (ui): 9,174ファイル(元の約88%)
- Destination SPA (ui-ar): 2,066ファイル(元の約20%)
ui-arには共通ファイルも含まれるため単純な足し算にはなりませんが、元のuiからは約1,300ファイル(約12%)を削減できました。 ui-arとしては元のuiと比べて1/5の規模にまで小さくすることができました。
実際の効果
リリースの分散・独立化
AR開発チームは分割したSPAを自律的にデプロイできるようになり、他機能の変更に巻き込まれることなく独立したリリースサイクルを管理できるようになりました。これにより、リリース頻度の向上が実現しています。
領域ごとのオーナーシップ醸成
AR関連コードが一箇所に集約されたことで、チームが責任範囲を明確に把握できるようになりました。チームや領域独自の改善や、新しい手法・ツールについての試みがしやすくなりました。
AI活用の促進
コードベースが元の約20%(2,066ファイル)に絞り込まれたことで、AIツールに対して効果的なコンテキストを提供できるようになりました。領域が明確なため、これからチーム内でのAI活用の展開も加速できそうです。
他の領域のSPA分割が容易に
分割ツールは汎用的に設計されているため、AR以外の領域(EXなど)についても同様の手順でSPA分割できます。 これにより、その後のSPA分割作業をより効率的に進められるようになりました。
まとめ
本記事では、Bill One FrontendのモノリシックなReact SPAを、静的解析による動的生成を用いてマイクロフロントエンドへ分割する取り組みについて解説しました。 大規模なモノリシックアプリケーションの分割は、技術面と組織面の両方で大きな挑戦です。しかし、明確な方向性と段階的な進め方があれば、開発速度を落とすことなく実現できます。
この記事が、同様の課題に直面しているチームの参考になれば幸いです。
Sansan技術本部ではカジュアル面談を実施しています

技術本部では中途・新卒採用向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話しします。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。