機能開発を止めずにNext.jsをApp Routerへ移行しました
この記事は、Money Forward Kansai Advent Calendar 2024の12月21日の記事です。
こんにちは Dammatsu です!
普段はマネーフォワードクラウド会計PlusのSREとして、インフラや運用の改善に取り組んでいます。
現在、会計Plusチームでは、Next.jsを用いたフロントエンドのフルリプレイスを進めており、すでに複数のページをNext.jsへ移行しています。その中で、インフラの改善のためにServer Componentsを利用したいケースがあり従来のPages RouterからApp Routerへの移行を行いました。
App Routerは、Next.js 13で導入された新しいルーティングシステムです。デフォルトでServer Componentsを利用できるほか、fetch関数のキャッシュ管理が改善されたり、layout.tsx
やtemplate.tsx
による柔軟なレイアウト機能なども備えています。
新機能のリリースも迫っていた状況だったので機能開発を担当しているチームの作業とコンフリクトを起こさない形で徐々に移行作業を進めました。
今回は、その手順や工夫したところを紹介したいと思います。
移行手順
不具合発生時の影響範囲を限定できるようにするため、全ページを一度にApp Routerへ切り替えるのではなく、1ページ単位で段階的に移行を進めました。
移行に必要な作業は、基本的にはNext.jsが公開している公式ドキュメント https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration を参考にしましたが、そのままの手順だと上手くいかない部分もあり、下記の手順で進めました。
useRouter
(next/router
)の置き換え
1. useRouter
は、Next.jsが提供するルーティング用のHooksで、URLのパスやクエリなどの情報の取得、ルーティング操作が行えます。
App Routerで動くコンポーネント内ではnext/navigation
のuseRouter
を利用する必要があり、移行のためには従来のnext/router
のuseRouter
を置き換える必要があります。
router.replace
router.push
useRouter が返すオブジェクトに対してreplace
やpush
を呼ぶことでページ遷移を行うことができます。
Pages Router で next/router
の useRoter
を使うとエラーになり、逆にApp Routerで next/navigation
の useRouter
を使うとエラーになるので移行する必要があります。
共通コンポーネントの場合 App Router と Pages Router から呼ばれる場合があるので、フルロードにはなってしまいますが、Next.js に一旦 window.location
に置き換えました。
そして、全ページ移行後には next/navigation
のuseRouter
を使うようにしました。
※このブログを書きながら公式ドキュメント https://nextjs.org/docs/pages/api-reference/functions/use-router#the-nextcompatrouter-export を再度読み返して気づいたのですが、移行用のnext/compat/router
の useRouter
があったようです。こちらを使うべきでした。
router.pathname
,router.query
の置き換え
URLのパス名やクエリパラメーターを取得する際に使うメソッドですが、next/navigation
から usePathname
, useSearchParams
というHooksが提供されており、これらを使うように変更しました。返り値は完全に一緒ではないので少し実装を変える必要がありますが、Pages Router でも使うことができるので、移行前を行う前に置き換えることができます。
これらの置き換えの作業にあたっては、useRouter を変更するPRを少しづつ出すことで、コンフリクトを起こさないようにしました。
Jest や Storybook の mock なども行う必要があり、App Router への移行では一番大変な作業でした。
use client
の付与
2. 各ページのコンポーネントに App Routerでは全てのコンポーネントがデフォルトでServer Componentとなります。
サーバー側でレンダリングされるのでHooksのuseStateやonChangeなどの処理は使えなくなるので、今までとかなり動作が異なります。
まずはApp Routerに移行をすることを優先し、一旦ファイルの先頭に "use client"
を付与し全ページをClient Compoenentとして動かすことにしました。
Client Componentから呼ばれるコンポーネントは基本的にClient Componentとなるので、全コンポーネントに付与する必要はなく、各ページから最初に呼ばれるコンポーネントを変更していきました。
_document.tsx
, _app.tsx
を layout.tsx
に移行
3. Pages Routerでは_document.tsx
でhtml
タグやhead
タグの設定を行なったり、_app.tsx
でページ間で共通のコンポーネントの表示や処理を行うことができますが、App Routerでは、layout.tsx
を使う必要があります。
Metadataの設定方法などは少し変わりますが、ほとんどコピペで対応が可能でした。
4. not-found.tsx の作成
存在しないパスへアクセスした場合、Pages Routerでは、pages/404.tsx
が呼び出されますが、App Router では app/not-found.tsx
が呼び出されます。app ディレクトリを作ると自動でこのような動作になるため、カスタムの404ページを表示させたい場合、各ページを Pages Router から移行する前に、まず app/not-found.tsx
を用意する必要があります。
3 の layout.tsx
の作成と not-found.tsx
の作成に関しては同じ PullRequest で行いました。
5. 1ページずつ移行
これで移行できる準備が整ったので、1ページづつ移行しました。
工夫した点としては安全に移行が行えるように middleware.ts
を使って環境変数を使って本番環境以外でのみApp Routerのページにアクセスできる仕組みを用意しました。
app
ディレクトリ配下に/AppRouterTest
という移行用のパスを設け、middleware.ts
でアクセス制御することで、機能開発チームが同じ機能をPages Router・App Routerの両方で動作確認できる環境を作りました。
一定期間触ったりテストをした後、問題がなければPages Router側を削除してApp Routerへ正式移行するという手順を繰り返し、全ページの移行を終えました。
振り返り
2ヶ月ほどでApp Routerの移行を終えることができました、機能開発チームとの作業がコンフリクトすることもなく App Routerに移行できてよかったです。
まだ移行が完了したのみでApp Routerの機能をフル活用できていませんが、今後Server Componentsを通じたパフォーマンス向上や、開発者体験の向上ができればと思います。
Discussion