こんにちは。開発3センターの児玉です。現在は出前館に出向して、主に出前館のコンシューマ向けアプリケーションのフロントエンド開発を担当しています。
現在、出前館フロントエンドはPHPからNext.jsへの画面単位でのリプレイスを進めています。
技術スタックやプロジェクトの詳細などは、出前館ブログに書いていますので、興味のある方はぜひご覧ください。
出前館Webのリプレイスが始まってから1年半ほど経過しました。現在、注文周りや店舗一覧などの画面のリプレイスが終わり、主要画面のリプレイスがあと少しで完了予定です。
また、リプレイスを進める中で技術的な課題にも何度か直面し、アーキテクチャなどの大きな変更も行いながら解消していきました。
本記事ではどのような技術的な問題に直面し、それをどのように解決していったかを一部ご紹介したいと思います。
パッケージマネージャー
Yarn v1
当初はチームメンバーが使い慣れていたということもあり、Yarnのv1をパッケージマネージャーとして採用していました。
出前館には現在多くのLINEからの出向者もおり、フロントエンドではLINE内部で管理されているESLintやPrettierを採用しています。
そこで問題になったのが、出前館のCI環境がLINEのプライベートネットワークと疎通できない制約があり、CI上でLINEのprivate npm registryにアクセスできませんでした。
この問題を解決するために導入したのがYarnのZero-Installsです。
Yarn v2 with Zero-Installs
Zero-Installs とは Yarn v2で導入された PnP (Plug'n'Play) を利用し、依存パッケージも一緒にコミットしてしまう手法です。
これによってCI上で yarn install
が不要になり、問題が解決したのに加え、CIの実行時間も短縮することができました。
ただその一方で、問題になったのがライブラリのアップデート時に PnP 特有の問題に直面し、ライブラリ側の修正を待つ必要がありました。
例えば以下のようなことがありました。
- TypeScriptをアップデートしようとした時にYarn側のPnPの問題でアップデートが出来ず、修正を待つ必要がありました。
- Next.jsではNext.js側の問題でアップデートが出来ず、調査すると2ヶ月ほど前からissueが上がっていた。
この問題はなかなか修正されずに放置されていたので、自らcontributeし修正しました。
原因はwebpackの依存パッケージであるenhanced-resolveの向き先がregistry.yarnpkg.comからcodeload.github.comに変わっていたためでした。
PnP に切り替えてから短い期間でこのような問題に直面したのに加え、特にNext.jsでは複数の同じissueが上がっているのにも関わらず、長い間修正されていなかったのは、Next.js側の PnP 対応・修正の優先度が低いように感じました。
今後のことを考えると、PnP 特有の問題でライブラリのアップデートができないのは良くないと思い、PnP での管理をやめることにしパッケージ管理を node_modules に戻すことにしました。
Yarn v3 with node_modules
PnP をやめるということは Zero-Installs もやめることになるので、Yarn v1での問題だった CI 上でのprivate npm registryへのアクセス問題があります。
これを解決するために、LINEのESLintやPrettierだけパッケージごと一緒にコミットし、package.jsonからはYarn v2から新しく追加されたPortalというプロトコルを使って portal:../my-package
のように参照しています。
ローカルのファイルをpackage.jsonから参照する場合Fileプロトコルを使って file:../my-package
のように参照することもできますが、これだとCI上で yarn install --immutable した時にlockファイルのchecksumの値が毎回変わりエラーになるため、Portalを使っています。
現在はこのパッケージ管理方法を採用していますが、これはこれでパッケージ更新が手間だったりと課題もあるので、他に良い方法がないか検討して行きたいと思っています。
アーキテクチャの変更
出前館WebリプレイスプロジェクトではBFF (Backend For Frontend) を新たに設け、WebとAppの両方からのリクエストを捌いています。
当初の構成はNext.jsのAPI RoutesでBFFを実装しており、以下のようなアーキテクチャでした。
ただこれだと以下のような問題がありました。
- WebとBFFのコードが混在しており、コードベースのメンテナンス性が低くなっていた
- WebとBFFで同じサーバーなので負荷に対する台数調整が別々にできなかった
- デプロイやロールバック時もWebとBFFでの切り離しができなかった
こうした問題を解決するためにWebとBFFのサーバーを分けることにしました。
Next.jsのAPI RoutesをBFFとして使っていると、WebとBFFは同じサーバーになります。
Webの負荷は主にSSR (Server Side Rendering) で、BFFはWebとAppの両方からのリクエストを捌いてるためWebよりも高負荷など、1つのサーバーで管理していると柔軟性がなく最適化などが難しい状態でした。
そこでNext.jsのAPI RoutesをBFFとして使うのをやめ、BFF用の新しいサーバーを設け、Webと切り離すことにしました。
新たなBFFはExpressで実装されており、コンパイルにはSWCを採用しています。以前まではNext.jsと同じビルドだったので、Docker イメージの作成に5 ~ 6分掛かっていたのが、現在は約1分と大幅に短縮も出来ました。
さらにWebとBFFを分離することにより、課題となっていた、サーバーの台数調整、デプロイとロールバックの切り離しも柔軟に行えるようになりました。
monorepo化
アーキテクチャ変更でWebとBFFが分離され、コードベースもどのように管理するか考える必要があります。
当初のディレクトリ構成は以下でした。
.
├── next.config.js
├── package.json
├── ...
└── src
├── pages
│ └── api # BFF (Backend for Frontend)
│ └── ...
├── components
├── ...
Next.jsのAPI Routesをやめるということは、コードベースも切り離すことになります。そこで採用したのがmonorepoです。もちろんpolyrepoにする選択肢もありましたが、WebとBFFの開発者は同じということもあり、あまりpolyrepoにするメリットを感じませんでした。
他にもライブラリのバージョン管理やESLintなどのルールが統一できるのでmonorepoを採用しました。monorepoを採用する上でTurborepoやNxを導入するべきかなども考えましたが、後からでも導入は可能なので、一旦monorepo管理ツールは入れずにシンプルにYarnのWorkspacesで管理することにしました。
ディレクトリ構成は以下のようになっています。
.
├── .github
├── docker
├── documents
├── ...
└── packages
├── web
└── bff
monorepoにすることによってコードベースが把握しやすくなり、メンテナンス性を向上できたと思います。
またパッケージごとにディレクトリが分かれているので以前よりも新規参画者にもわかりやすいコードベースになったと思います。
現在packages配下はwebとbffの2つだけですが、共有モジュールを導入中なのとmonorepo管理ツールの導入も検討していきたいと思っています。
おわりに
この記事ではどのような技術課題に直面し、どのように解決・改善していったかをご紹介しました。
振り返って見ると、反省すべき点もあります。
例えば、技術的な大きな変更を入れるタイミングはもう少し慎重にやるべきだったと感じています。
特にmonorepo化によって、ディレクトリ構造が大きく変わっているので、途中で他のものがリリースされるたびにバックパージをする時は多々マージ漏れなどが発生し苦労しました。
なのでコードベースに大きな変更を伴うものは、コードフリーズの後に行うべきだったと思いました。
出前館Webではリプレイスや案件などを進めながら、このような技術的改善も積極的に行なっています。
技術やアーキテクチャはより良いプロダクトを作るために、状況に応じて常に変化し続けるものです。
現時点でもデザインシステムの導入、GraphQL Clientの移行、フィーチャーフラグの検討などが進行しており、技術の変化・改善が引き続き行われています。
上記の取り組みも別途紹介できればと思います。