🔖

1年かけてNext.jsのapp routerへ完全移行した話

2023/12/15に公開

App Router移行は進んでいますか?

Next.js v13から一年以上経過しました。
ReactとNext.jsは、React Server Component(以下RSC)やServer Actionを筆頭にどんどん新機能がリリースされています。

私も仕事でNext.jsを採用しています。
App Router移行するか。するとしたらいつするか。
どのようなアーキテクチャにするか考えついに先日、全ページApp Router移行できました。
一緒に働くフロントエンドエンジニアのみなさま、工数を確保してくださったPMありがとうございました。

対象読者

  • Next.jsで開発しており、Pages Routerの資産を持っている人
  • これからApp Routerに移行したい人
  • App Routerの移行で困っている人

なぜ移行しようと思ったか

App Routerが公開されましたがPages Routerがdeprecatedになったわけではありません。
App Routerに移行しなくても問題ありません。
しかし我々は以下の理由でApp Routerへの移行を決断しました。

  • ディレクトリ構成をコロケーション化したかった
  • RSCの方が考慮することが減る
  • Next.jsの最新に追従した方が今後楽になると考えた

また、テックリードの私が元々spring bootなどでバックエンドを書いていたのもあって、RSCの思想に非常に惹かれていました。

詳しくは後述しますが、App Router移行を経てほとんどの部分で満足しています。
もちろんApp Router特有の仕様に悩まされることもありますが、App Routerは今も進化を続けているので、いずれ解消すると信じています。

移行プロジェクトでやってはいけないこと

いわゆるしくじり先生です。

  1. 依存ライブラリの対応状況を調べずに移行する
  2. App Routerと同時にライブラリも移行する
  3. App Routerらしい書き方にこだわる

一言で言うと、「移行だけでも大変だから、それ以上のことはやるな。」です。

1. 依存ライブラリの対応状況を調べずに移行する

ライブラリによってはApp RouterやRSCで動かない場合があります。
移行途中に気づいた場合、移行計画が頓挫する可能性もあります。
移行を実施する前にライブラリがRSCに対応しているか調査してください。

RSC移行時に問題になりそうなライブラリを挙げておきます。

  • CSSライブラリ各種
  • データフェッチライブラリ
    • GETやPOSTならfetchをそのまま使うのが楽そう
    • graphqlクライアントライブラリは、urqlが対応済
  • グローバル状態管理ライブラリ

2. App Router移行と同時にライブラリも移行する

採用ライブラリがRSCに対応していなくてもApp Router移行と同時にライブラリ移行を進めないでください。
全てのコンポーネントにuse clientを付与してクライアントコンポーネントとして扱ってもApp Routerとして動きます。
こうすればRSCで動かないライブラリでも移行可能です。

3. App Routerらしい書き方にこだわる

App Router移行時にApp Routerらしい書き方にこだわらないでください。
App Routerらしい書き方にリファクタリングするのはApp Routerで動くようになってからで大丈夫です。
まずは動かすこと。これを最優先にしてください。

App Router移行手順

反省点も踏まえてApp Routerの移行のおすすめ手順を紹介します。
Next.jsのサンプルなどはすでに触っており、App Routerはある程度理解している前提です。
特に公式のマイグレーションガイドは一読ください。

  1. プロダクトにApp Routerのサンプルページを作成する
  2. 新規ページはApp Routerで作成する決まりにする
  3. 既存ページを1ページずつ/appに移動する
  4. 各ページをApp Routerに最適化する

順番に説明していきます。

1. プロダクトにApp Routerを使ってサンプルページを作成する

サンプルページを作成してライブラリやLinterがApp RouterとRSCに対応しているか確認します。

Next.jsのサンプルプロジェクトとは違い開発中プロダクトにはライブラリやLinterやCIなど、色々なものがすでに入っているはずです。
それらがApp Router導入の妨げになる可能性があります。
(e.g. すでに/appを利用していたり、App Router特有の記法がLinter違反になったり)

よく使うライブラリはサンプルページで動かしてRSCで動作確認をしてください。
データフェッチやCSSライブラリは入念に。

今利用している物がRSC未対応の場合は、移行先の検討もこのフェーズで行ってください。
移行先のライブラリがちゃんとRSCで動くか試してください。

サンプルページの作成はテックリードやApp Router移行リーダーが1人で実施するのが良いでしょう。
1人で作成できない規模のサンプルページを作成する必要はありません。
作成後はチームに共有し、一緒に画面を触るのも良いと思います。

2. 新規ページはApp Routerで作成する決まりにする

サンプルページを参考にして新規ページを作成することをチームで合意します。
App Routerらしい書き方やRSCを活かしたコードになっているのがベストですが、一番大切なのはこれ以上Pages Routerの資産を増やさないことです。

この約束が守られれば、これ以降は/pages以下に新規ページが増えなくなります。
守れないと既存のページを移行している横で/pages以下のコードが増えます。
移行へのモチベーションが下がるので避けてください。

移行中はPages RouterとApp Routerが同居します。
Pages RouterからApp Routerへのページ遷移は、SPAの遷移ではなく通常のURL遷移になります。
Contextベースのグローバル状態管理ライブラリを入れている場合は注意してください。(e.g. redux, recoil, etc...)
Pages RouterとApp Router間の遷移ではContextベースのグローバル状態を引き継ぎません。

私のチームではContextベースのグローバル状態管理は使用していなかったので問題になりませんでした。

3. 既存ページを1ページずつ/appに移動する

ようやく既存ページを/appに移動していきます。

ここではApp Routerっぽい書き方には寄せません。(だから移行ではなく移動)
既存のコンポーネントにuse clientをつけて、なるべく変更量が少ない状態で/appに移動します。

前の章でも書きましたが、App RouterはURL単位で移行可能です。
まずはサーバーから取得したデータを表示するだけのページなど、ユーザーからのinputがない画面から移行をお勧めします。
逆に現在開発が積極的に行われているページの移行は難しいです。開発が落ち着くまで待つのが賢明です。

このフェーズでは/appにファイル移動してデグレを起こさなければ100点です。
それ以上は目指してはダメです。

4. /app以下のページをApp Routerっぽい書き方にする

フェーズ3で移動完了したページをApp Routerっぽい書き方にしていきます。
フェーズ3を全て実施してからフェーズ4に取り掛かっても良いですし、フェーズ3とフェーズ4を交互に進めても良いです。

私が思うApp Routerっぽい書き方とは何か。
参考資料をいくつか紹介します。

Pages Routerではファイル名がパスとして扱われていたので、page以外のファイルを置けませんでした。
App Routerでは任意のファイル名を配置できるようになったので、コロケーションパターンの実現できます。
コロケーションパターンにすべし!とまでは言いませんが、App Routerはコロケーションパターンに向いてます。

App Routerに移行した感想

初めにも書きましたが、体感はかなり良いです。不満点はほとんどありません。
良かったところをいくつか共有します。

ユーザー視点

明らかに初期レスポンスが速いです。
従来はページのやり取りの後にデータフェッチが行われてレンダリングされていましたが、RSCになってからはページのやり取りで完結しています。
また、urqlのfragment-maskingを同時に利用するようになったのもあって、レイアウトシフトが発生しなくなりました。

また、開発者ツールで見ても明らかに通信回数が減りました。
移行前の通信回数が多いページは開くだけで10回ほどリクエストを飛ばしていましたが、RSCに寄せることで通信1回でページが表示できるようになりました。

開発視点

いわゆるDX(Developer eXperience)は非常に向上しました。
ここについては語りたいことが多いので、いっぱい書かせてください!

  • ダイアログをpage.tsxを使って記述できる
  • コロケーション化されて管理しやすくなった
  • layout.tsxなど痒い所に手が届く機能群

ダイアログをpage.tsxを使って記述できる

今まではダイアログを管理する場合、上位のコンポーネントでuseStateを用いて開閉時の状態を管理していました。
ダイアログ内でデータフェッチする場合、ダイアログの中にsuspenseを配置して...とかやっていると、ダイアログ1つ置くだけでページが複雑になってしまう課題を抱えていました。

App Routerのintercepting-routesを使うことで、ダイアログをRSCでページとして記述できるようになり、ダイアログがかなりスッキリしました。

コロケーション化されて管理しやすくなった

ファイルは使う場所の近くに置いた方が見やすいです。
また、配置場所でどこで利用されるコンポーネントか分かるので、実装の心理的負荷が下がりました。
汎用コンポーネントでないならば、設計が少々ずさんでもレビューを通しやすいです。

App Routerのおかげというより、App Routerとeslint-plugin-import-accessの合わせ技のおかげですね。

layoutなど痒い所に手が届く機能群

URLの階層とデザインの階層はおおよそ一致していると思います。
それを表現できるlayoutや、エラー発生時のerrorloadingなど、今までいちいち自前で実装していたものをNext.jsが準備しています。
これらを活用することで記述もスッキリしましたし、面倒で省略していた処理を書くようになりました。

逆に困ったこと

cache周りはかなり複雑です。
この記事ではApp Routerのcacheについて深く記述しませんが、移行時にいくつかトラブルになることもありました。
リアルタイム性が必要になる場合はRSCに加えてクライアントサイドでもデータフェッチする判断を最終的にはしました。
それなら最初からRSC要らないのでは?と思われるかもしれません。
が、やはりそれでも初期表示が早いメリットは大きいです。

App Routerのcache周りの参考資料を置いておきます。

最後に

私とチームのApp Router移行の手順と反省点をまとめてみました。
結局発表から1年くらいかかりました。機能開発と並行だったので、工数が1年というわけではないです。
graphqlクライアントをurqlに移行したり、MUIがRSCに対応するまで待ったり、色々な段階を経て無事に移行完了できました。
私たちは運よく(先見の明があった?)Next.jsをサーバーとして動かして運用していたので、移行しやすかったのもあります。next exportしていたらもっと大変だったと思います。
App Router移行は必須ではありませんが皆さんも挑戦してはいかがでしょう。

Discussion