🎃

フロントエンドのリポジトリをmonorepoにしてよかったことと失敗したこと

2023/12/11に公開

この記事は、Money Forward Engineering 2 Advent Calendar 2023 11 日目の投稿です。
8 日目は亀井亮介さんで AIアシスタントChatGPTを活用:境界値分析による効果的なテスト設計 JSTQB Foundation Level新シラバスのキーワード でした。
本日は「フロントエンドのリポジトリを monorepo にしてよかったことと失敗したこと」について書いていきます。


この記事はバラバラのフロントエンドのリポジトリを 1 つの monorepo にした際の成功と失敗をまとめたものです。実際に行ったのは 2023 年の 6 月くらいで、当時のことを思い出しながら記事にしています。これから monorepo への移行を検討している方の参考になれば幸いです。

具体的な移行方法や運用方法は記載しませんので、参考文献の記事を参考にしてください。

monorepo移行前の各リポジトリの構成

  • 管理画面
    • 使用技術: React 16, Next.js 12
    • 状況: 根幹機能に React 16 に依存するライブラリを利用していたため、React 18 へのアップデートが困難。
  • アプリ画面
    • 使用技術: React 18, Next.js 12
  • 社内管理画面
    • 使用技術: React 18, Next.js 12
    • 状況: monorepo に移行するのと同時に新しく作成したパッケージ。
  • 共通コンポーネント
    • 使用技術: React 18, Vite 4
    • 状況: Vite のライブラリモードでのビルド後、AWS Code Artifact に publish し、他のリポジトリで使用していた。

monorepo移行前に抱えていた課題

リポジトリの増加による管理コストの増加

元々CS が利用する社内用の管理画面を管理画面の中の隠し URL として実装していた物を、別のサービスとして切り出す必要がありました。それに伴いリポジトリを追加する必要があり、管理コストが増加しました。リポジトリはそれぞれで開発サーバーを起動しなければならず、開発前にはそれぞれのリポジトリに移動して起動コマンドを実行していたため大変でした。

共通コンポーネントの管理が難しい

共通コンポーネントは別のリポジトリに切り出していたため、他のリポジトリから参照する際には、一度ビルドして AWS Code Artifact に publish してから参照する必要がありました。また、修正が必要になった際にはワークスペースを変更して修正し、レビュープロセスを通し npm publish した後に、各プロジェクト側でバージョンを上げて参照する必要がありました。

リポジトリ毎に設定ファイルを管理していたため、設定ファイルの変更があった場合、それぞれのリポジトリの設定ファイルを変更する必要があった

リポジトリ毎に設定ファイルを管理していたため、設定ファイルの変更があった場合、それぞれのリポジトリの設定ファイルを変更する必要がありました。開発が進むにつれて少しずつルールが足された結果、それぞれのリポジトリの設定ファイルが微妙に違ってきてしまいました。

各リポジトリ毎に異なるバージョンの同じライブラリを参照していた

依存関係の更新に Renovate を利用していますが、定期的にランダムで PR が作成されるようにしていました。各リポジトリでそれぞれ設定をしていたため、同じライブラリでも PR が作成されるタイミングによってバージョンが異なってしまうことがありました。

monorepo移行後のディレクトリ構成

root/
├── apps/
│   ├── admin/
│   │   └── package.json
│   ├── app/
│   │   └── package.json
│   └── internal_admin/
│       └── package.json
├── packages/
│   ├── config/
│   │   └── package.json
│   └── common/
│       └── package.json
└── package.json

monorepoに移行してよかった点

リポジトリを monorepo にする際、やってよかったことや monorepo にすることで得られたメリットをまとめました。

turborepo を使い、npm script を一元管理しタスクの実行が楽になった

monorepo にして turborepo を導入したことで、タスクの実行を一元管理できるようになりました。ルートディレクトリで yarn dev を実行するだけで各リポジトリの開発サーバーを一斉に実行できるので、開発時の手間が減りました。また CI/CD でも同様に 1 つのコマンドで全パッケージのテストやビルドを走らせることができるようになりました。

turborepo にはcacheの機能があり、turbo.yml の pipeline.{タスク名}.inputs に指定した変更がない場合一度実行した結果をキャッシュしてくれるので、ローカル環境の実行時間の短縮にも繋がりました。

Tips: 便利なscriptsの書き方

turborepo CLI の --filter オプションを利用することで、特定のパッケージのみ実行できます。package.json の script に "{パッケージ名}": "turborepo --filter {パッケージ名}" のように記載すると、yarn admin devyarn admin build のようにそれぞれのタスクをパッケージ毎に実行できました。

{
  "name": "frontend-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "admin": "turborepo --filter admin",
    "app": "turborepo --filter app",
    "internal_admin": "turborepo --filter internal_admin",
    "common": "turborepo --filter common",
    ...
  }
  ...
}

共通で利用するモジュールを別のパッケージに切り出すことで、他のパッケージから容易に参照ができるようになり、修正も容易になった

共通で利用するコンポーネントを monorepo の 1 つのパッケージに切り出したことで、他のパッケージから容易に参照ができるようになり、修正も容易になりました。また直接コード参照できるようになったため、ビルドを走らせる必要がなくなりました。

各種設定ファイルも 1 つのパッケージとして集約したことで、設定ファイルの変更があった場合、1 つの設定ファイルを変更するだけで済むようになりました。

root/
├── apps/
│   └── パッケージ名/
│       ├── eslint.config.js
│       ├── prettier.config.js
│       ├── stylelint.config.js
│       └── tsconfig.json
└── packages/
    ├── config/
    │   ├── eslint.base.js
    │   ├── package.json
    │   ├── prettier.base.js
    │   ├── stylelint.base.js
    │   └── tsconfig.base.json
    └── common

packages/config 内に Typescript, ESLint, Prettier, Stylelint 設定ファイルを配置しそこに必要なルールを記載しました。各パッケージ側ではそれらを "@tongari07/config": "workspace:*" のように workspace 内で参照し、それぞれの設定ファイルで extends することで、共通のルールを利用できるようにしました。

より詳しくはこちらの記事が参考になりました。

https://zenn.dev/shinnoki/articles/3f008f53b2312f

Renovateで全パッケージの依存関係を更新できるようになった

Renovate ってすごいなと思いました。1 つのライブラリのバージョンアップ PR でそれらを利用している全ての package.json が更新されるようになったため、PR の数が減り依存関係更新の負担がかなり減りましたし、それぞれのパッケージで利用しているバージョンも統一されました。

monorepoに移行した際に失敗した点

リポジトリを monorepo にする際、やっておけばよかったことや失敗してしまったこと、今後の課題をまとめます。

React バージョンが異なるパッケージを 1 つの monorepo にすると、依存関係の解決が難しくなる

今回のプロジェクトでは依存しているライブラリが React 18 に対応していないこともあり、React の 16 と 18 のバージョンが混在してしまいました。monorepo の利点の 1 つとして依存関係をルートディテクトリの node_modules にまとめることができるというものがありますが、React のバージョンが異なるパッケージを 1 つの monorepo にすると、依存関係の解決が難しくなるということがわかりました。

元々共通コンポーネントのパッケージは React 18 で実装していましたが、monorepo にしてビルドをせずに直接別のパッケージのから利用するようにしました。すると React 18 のパッケージでは問題なく利用できたのですが、React 16 のパッケージでは React 自体の型の違いによって利用ことができませんでした。そのため、共通コンポーネントのパッケージを React 16 にダウングレードせざるを得なくなりました。

また、バージョンを上げると React 16 に対応しなくなるライブラリがいくつかあったため、それぞれのパッケージで同じライブラリの異なるバージョンを利用する必要がありました。その結果各バージョンの依存関係が node_modules にダウンロードされる結果となり、monorepo のメリットが薄れてしまいました。

Vercelのデプロイキューがたまる

Vercel の Github インテグレーションを利用していたため、毎回 push 時に Vercel へのデプロイが走ります。monorepo にしたことで、1 回の push でそれぞれのパッケージのデプロイが走るようになり、デプロイキューがたまるようになりました。Vercel の同時デプロイ数は初期状態では 1 つのみで、1 つのデプロイメントが走っている最中はそれが終わるまで次のデプロイメントが走らないようになっています。そのため、並列でデプロイを行なっていた monorepo にする前よりデプロイ完了までに時間が掛かるようになってしまいました。

Vercel の Additional Concurrent Builds を追加すると並行してデプロイできる数が増えるのですが、1 つにつき 50$となかなかの出費のため、今回は追加しませんでした。

少しでもデプロイにかかる時間を減らすために、Ignored Build Step を設定できます。これはデプロイをするかしないかを判別するコマンドを設定できるもので、デプロイメントの一番最初に実行され、結果が exit 0 の場合移行の処理を行わないことができます。この設定に turbo-ignore を利用することで、パッケージ単位で差分があるかどうかを判別し、差分がない場合はデプロイを行わないようにできました。turbo-ignore に関しての詳細は以前書いた記事があるので、もしよかったら読んでいただけると幸いです。

なお Ignored Build Step を設定しても、キューには追加されるため、デプロイキューがたまる問題は解決しません。より速さを求めたい場合は素直に課金するか、自分で Github Action 等でデプロイを行う必要がありそうです。

まとめ

monorepo への移行は、複数のリポジトリを管理する際の多くの課題を解決する効果的な手法であることが明らかになりました。特に開発時の手間の削減や、依存関係の更新の負担の軽減といったメリットは、想像以上に大きなものでした。

しかし、異なるバージョンのライブラリやフレームワークを持つプロジェクトを統合する際には、依存関係の複雑化による問題が発生する可能性があります。また、monorepo にすることで、Vercel デプロイキューがたまるといった問題も発生する可能性があるため、プロジェクトによって monorepo への移行のメリットとデメリットをよく検討した上で移行を検討する必要があります。

この記事が、monorepo への移行を検討している方々にとって少しでも参考になれば幸いです。

参考文献

https://zenn.dev/burizae/articles/c811cae767965a
https://zenn.dev/moneyforward/articles/migrating-to-a-monorepo
https://zenn.dev/shinnoki/articles/3f008f53b2312f

逆に monorepo から分割したという記事も勉強になりました。

https://kaminashi-developer.hatenablog.jp/entry/2023/05/22/goodbye-monorepo

GitHubで編集を提案

Discussion