こんにちは。ソフトウェアエンジニアの坂井 (@manabusakai) です。
カミナシのプロダクトは、管理者の方が使う Web アプリに React、現場の方が使う iPad / iPhone アプリに React Native を採用しています。
どちらもフロントエンドの技術スタックを採用していることもあり、先日までは Monorepo と Yarn Workspaces の構成で運用されていました。
最近では Monorepo 化を進めている事例もよく見かけるようになってきました。
ですが、カミナシでは Monorepo をやめてリポジトリ分割をする意思決定を行いました。
具体的には、harami_client という Monorepo を harami_web と harami_mobile というリポジトリに分割することにし、先日ついにリポジトリ分割を完了することができました 🎉
ちなみに、harami というのはプロダクトのコードネームです。カミナシでは肉の部位がコードネームに採用されています 🍖
今回は Monorepo をやめた背景やどういう風に作業を進めたのかをご紹介します!
なお、この記事では Monorepo の詳細には触れないので、参考になりそうなページをご紹介しておきます。
Monorepo における課題
Monorepo ゆえの密な依存関係
事の発端は、iPad / iPhone アプリ(以後 mobile と表記します)で利用している Expo SDK のアップグレードでした。
先ほどご紹介したようにカミナシのプロダクトは React をベースにしていますが、React のバージョンは web と mobile で密な依存関係にあり、Expo SDK のアップグレードのために mobile だけ React のバージョンを上げるということが難しい状況でした(Monorepo 構成で複数の React を共存させることができないため *1)。
この依存関係は本来 Monorepo のメリットでもありますが、裏を返すとどちらか片方の変更がもう片方にも影響を与えてしまうというデメリットにもなり得ます。
また、あるパッケージで使用しているライブラリを別のパッケージから利用できてしまうため、暗示的な依存関係も生まれていました。
アプリケーションのライフサイクルの違い
web と mobile で利用しているフレームワークのライフサイクルの違いもさらに状況を難しくしていました。
mobile で利用している Expo SDK はおよそ 3 か月おきにメジャーリリースが行われ、その後は 6 か月しか後方互換性や脆弱性対応を含めたサポートが提供されません(執筆時点では Expo SDK 48 が最新ですが、サポートされているのは 46 〜 48 の 3 バージョンです)。
そのため常に最新バージョンを追いかけ続けることが重要ですが、お互いが密に依存している状況だと対応コストが増えてしまい、通常の機能開発を圧迫してしまっていました。
エンジニアの人数が限られるスタートアップにおいて、この状況は何としても改善したいポイントでした。
Monorepo では解決できないと判断
フロントエンドに詳しいエンジニアを中心に Monorepo のまま上記の課題を解決する方法がないか検討を行いましたが、最終的にはリポジトリ分割を行うことが最も妥当であるという結論に辿り着きました。
リポジトリ分割のステップ
読者の方に向けて作業前のリポジトリを簡略化してお伝えすると、次のような構造になっていました。
harami_client ├── node_modules ├── package.json ├── packages │ ├── api_docs // OpenAPI 定義 │ │ ├── package.json │ │ └── spec │ │ └── openapi.yaml │ ├── mobile // React Native │ │ └── package.json │ └── web // React │ └── package.json └── yarn.lock
Yarn Workspaces を使っているので、node_modules と yarn.lock はトップレベルに配置されていて、各アプリケーションからは hoisting されて参照されています。
OpenAPI 定義の移行
リポジトリ分割を行うことが決まりましたが、解決しなければいけない問題は他にもありました。 packages 配下に web と mobile のソースコードがありますが、なぜか OpenAPI 定義もクライアント側のリポジトリに存在していました。
過去の経緯の詳細は分かりませんが、当時はクライアント側しか OpenAPI 定義を参照していなかったため、ここにあったほうが都合が良かったようです(現在はサーバ側にもスキーマ駆動開発を導入しつつあります)。
リポジトリ分割の前に、まずはこの OpenAPI 定義を適切な場所に移すことにしました。 OpenAPI 定義だけを持つリポジトリを作ることも検討しましたが、ひとまずは管理の手間を減らすために API サーバ側に移すことにしました *2。
これまでは hoisting されてトップレベルにある node_modules にある OpenAPI 定義が参照されていましたが、このタイミングで npm パッケージ化することにしました。
OpenAPI 定義を変更する Pull request がマージされると、npm パッケージを作成し GitHub Packages に登録するワークフローを追加しました。そして web と mobile の package.json はその npm パッケージを参照するようにしました。
これにより harami_client のリポジトリは web と mobile だけのソースコードになりました。これが最初のステップです。
リポジトリ分割
ここからが肝心のリポジトリ分割です。
方針として、harami_client のリポジトリから比較的影響の少ない web を引き抜き、harami_client には mobile だけを残してリネームすることにしました。
また独立したリポジトリにすることでディレクトリを下げる必要がなくなるので、合わせてディレクトリの再配置も行いました。
このリポジトリ分割を進めながら、mobile では Expo SDK のアップグレード作業も進めていました。そのため 2 回のステップに分けて、まずは web だけ先行して進めて、mobile は別日に実施することにしました。
全体の流れとしては以下のような感じです。
開発への影響をできる限り減らすために、レビューが終わってリリースできるものは早めにマージしてもらったり、分割後に想定される FAQ をドキュメントにまとめるなど、丁寧なコミュニケーションを意識しました。
あとは当日にコードフリーズを行い、事前に準備した手順を淡々と進めるだけでした。幸い大きなトラブルもなく無事に終えることができました 🙌
リポジトリ分割してどうなったか
解決したい課題に対して適切なアプローチだったのか振り返ってみます。
まず密な依存関係については、物理的にリポジトリを分けたことで完全に依存関係がなくなりました。これまではどこで依存し合っているのか把握できていなかったため、とあるパッケージの変更が他にも影響を与える可能性がありましたが、そういう不安も解消されました。
また、リポジトリを分けたことでフレームワークのライフサイクルにも追従しやすくなりました。その証拠に 3 月に Expo SDK 46、4 月に Expo SDK 47 にアップグレードすることができ、5 月中には Expo SDK 48 にアップグレードする予定で作業を進めています。
リポジトリ分割以降はアップグレードにかかる工数もぐっと短縮することができており、今後は最新バージョンを追いかけ続けることができそうです。
最後に余談ですが、カミナシでは Pull request をマージする前に最新のコードになっていることが要求されますが(GitHub の ”Require branches to be up to date before merging” の設定)、エンジニアリング組織が急拡大しているので Pull request をマージしようとすると rebase 合戦が起きていました。適切にリポジトリを分割したことで、この問題も緩和することができました。
おわりに
当初の目論みどおりの結果を出すことができており、今のカミナシにとっては Monorepo をやめることが最善のアプローチだったと言えそうです。
エンジニアリング組織やプロダクトのフェーズによって最適解は変わってくると思いますが、カミナシではこれからも流行り廃りに流されず技術選定していこうと考えています。
最後に宣伝です 📣 カミナシでは絶賛採用中です! 一緒に強いチームを作っていく仲間を募集しています!
*1:https://legacy.reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
*2:Git の履歴を維持したままリポジトリ間でファイルを移す手順は、「Git リポジトリの特定のファイルを別の Git リポジトリに移す」にまとめていますので興味がある方はご覧ください。