[Monorepo] React+Node.js+Typescript モノレポ構築備忘録

今回はアーキテクチャ的なお話です。

モノレポってなに?

Monorepo(モノレポ)という構成をご存じでしょうか?
モノレポとは1つのリポジトリで複数のプロジェクトを管理する手法のことを指します。

モノレポ構成は、各マイクロサービスを分離して管理しつつ、必要に応じてパッケージとして参照することでDRY(※1)な開発ができ、GoogleやMicrosoftなどの企業でも採用されている実績のあるアーキテクチャです。



モノレポに対応する用語としてPolyrepo(ポリレポ)がありますが、こちらはプロジェクトとリポジトリを1対1にして管理する手法です。

弊社では通常アプリケーションを開発する際には1アプリケーションにつき1gitリポジトリで管理・開発をしているのですが、今回はこのモノレポ構成を採用してみることにしました。

モノレポのメリット

  • 複数のプロジェクトからほかのプロジェクトを共有でき、コードを再利用できる。
      (UIコンポーネントの共通化、汎用ロジックの共通化、APIのインターフェースや型の共通化等)
  • リンター、 フォーマッター、 huskyなどを使ったコード検査、Vscodeの設定など、プロジェクト横断的に共通化したい設定を共有できる
  • コマンドをまとめて実行するようにすれば、複数のプロジェクトでも環境のセットアップが楽になる(後述)

モノレポのデメリット

  • 複数のプロジェクトを単一リポジトリで扱うため、リポジトリのサイズが大きくなる可能性がある
  • プロジェクトに破壊的な変更がある場合、依存している側に修正が必要になる
  • プロジェクトが大きくなると、依存関係が管理しにくくなる

モノレポを採用した背景

  • 直近かかわった案件でモノレポを採用していた
  • 社内ではまだ採用していない構成だった
  • 今回の案件では、AWSのCloudfrontでCDNとして2つのアプリケーションをリリースする必要があった。両プロジェクトでレイアウトや関数などを共通化して使いたいし、複数のgitリポジトリで個別に管理するのが嫌だった

モノレポ構成のつくり方

パッケージマネージャの機能を使う方法

node.jsのパッケージマネージャとしてよく知られるnpmですが、version7.24.2からワークスペース機能をサポートしています。

同じくパッケージマネージャとしてメジャーなyarn, pnpmでもそれぞれワークスペース機能を使うことができるため、特にパッケージやフレームワークをインストールしなくてもモノレポを構築することができます。

これらの機能を利用してワークスペースを作成すると、プロジェクトのトップレベルから個別のプロジェクトを操作することができます。

また、Concurrentlyなどのnpmパッケージを併用することで、各タスクを並列実行することも可能です。

以下はpnpmでのコマンド例です。

// 個別のワークスペースへのパッケージ追加
// pnpm -F {ワークスペース名} {コマンド}
$ pnpm -F @workspace/app1 add react

// プロジェクト全体へのパッケージ追加
$ pnpm add -W eslint

// app1, app2のビルドコマンドを並列実行する
$ concurrently 'pnpm -F @mono/app1 build' 'pnpm -F @mono/app2 build'

ライブラリを使う方法

また、モノレポを構築することに特化したnpmライブラリなども存在します。

よく見かけるものとしては、TurborepoLernaNxRushなどがありますが、以下のような特徴を持つものが多いようです。

  • プロジェクト全体の依存関係を可視化(グラフ化)したりチェックする機能がある
  • プロジェクトの依存関係に従って、タスクの実行順序を制御
  • 前回ビルド時の情報をキャッシュして、変更分だけ差分ビルドしてくれるため高速
  • コードの生成機能

今回はモノレポ導入初の試みということもあり、また大して調査する時間もなかったので、シンプルにパッケージマネージャのワークスペース機能を利用した構成を採用しました。

ちなみに、これらのライブラリは必ずしもプロジェクト開始時点で導入しなければいけないものばかりではなく、後から既存のプロジェクトに導入することができるというものもあります。
継続的に調査を進めて、乗り換えも検討してみようかと考えています。

ポイント

フロントエンドフレームワーク(ビルドパッケージ)の選定

フロントエンドのフレームワークを選定するにあたって、検討したのはCRA(CreateReactApp)ViteNext.jsの3つでした。

調査していると、CRAの場合プロジェクトルート以外のファイルを解決する場合はWebPackの設定をいじるような修正が必要になることがわかりました。

今回のように複数のプロジェクトにまたがる開発は向かなそうだったため、早々に候補から外れました。

残るはViteとNext.jsです。

ViteもNext.jsも、今回の要件となるS3での静的ホスティングは可能なよう。
ただ、今回メインでアサインするメンバーがReactに慣れていないこともあり、React以外の知識が必要になる可能性もあるNext.jsは今回パスしました。

最終的に、Reactの習得以外の学習コストがあまりかからず、過去の案件で静的ホスティングの機能を使った実績があるViteを選択しています。

パッケージマネージャの選定

今回の悩みどころの一つとして、採用するパッケージマネージャをどれにするか(npm, yarn, pnpm)という問題がありました。

これまで社内の開発ではyarnをメインで使っていたのですが、調べていくうちにyarnとpnpmではnode_modules内でのインストールしたパッケージのレイアウトが異なるということがわかりました。

そのため、採用するパッケージマネージャによっては、依存関係が複雑になりうるモノレポの構成には向かないのではないかという懸念が生まれてきました。

熟慮検討の結果、今回のプロジェクトではpnpmを採用しています。

yarnを使う場合の問題点

yarnを使ったモノレポ構成の問題点としては、「npmパッケージの依存関係がうまく管理できなそう」ということが挙げられます。

たとえば、以下のような構成のプロジェクトで、helper1プロジェクトのみにzodパッケージをインストールした場合。

yarnの場合はルートのnode_modulesに各種パッケージがフラットにインストールされます。

zodをインストールしてみたところ、ルートのnode_modulesにzodがインストールされ、個別のプロジェクトのnode_modulesにはzodが入ってきませんでした。

左(pnpm):プロジェクトルートのnode_modulesにはプロジェクトルートでインストールしたパッケージだけ。
右(yarn):全プロジェクトでインストールしたパッケージが全部プロジェクトルートのnode_modulesに入ってきている。。。

npmでは、パッケージをimportしているプロジェクトのnode_modulesに該当のパッケージが存在しない場合は、さらに上の階層にさかのぼってnode_modulesを参照するという性質があります(これをホイスティングといいます)。

上記構成の場合、helper1でのみこのホイスティングが発生してくれれば問題はないのですが、helper2プロジェクトでも同様にホイスティングが発生します。

helper2プロジェクトではzodをインストールしていないので本来importできるべきではありません。

ところが、ホイスティングの機能によってpackage.jsonに記載されてもいないのにimportできてしまうという状況が発生します。

本来package.jsonに依存関係がないパッケージを読み込めるべきではないのですが、npmの仕様上読み込めてしまうため、いざzodパッケージを削除した際に、依存関係が明記されていないパッケージでエラーが発生する可能性があるのです。

この点、pnpmでは個別のプロジェクト単位で依存関係が管理されるため、予期しない依存を生み出すことがありません。

インストールしていないパッケージを参照しようとしている場合は、ちゃんとエラーします。

また、重複するパッケージなどがある場合は、内部的にシンボリックリンクを張るなどしてモジュールサイズの最適化を行っているということもあり、pnpmのほうがモノレポ構成に向いていそうだという結論に至りました。

まとめ

  • モノレポ構成を採用すると複数のプロジェクトを1リポジトリで管理できる
  • 各種周辺ツールもありかなり便利そう
  • pnpmを使うことで、パッケージ管理がうまく行えそう

まだプロジェクト開始段階で、開発を進める中で使い心地などを見ていく必要はありますが、今のところは今回の要件にあった形になっているのではないかと思います。

前へ

VSCode Dev Containersでこれからの開発環境構築

次へ

Lexical PlayGroundから仕組みを調べてみた