🏃

機能開発を止めずにNext.jsをApp Routerへ移行しました

2024/12/21に公開

この記事は、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.tsxtemplate.tsxによる柔軟なレイアウト機能なども備えています。

新機能のリリースも迫っていた状況だったので機能開発を担当しているチームの作業とコンフリクトを起こさない形で徐々に移行作業を進めました。
今回は、その手順や工夫したところを紹介したいと思います。

移行手順

不具合発生時の影響範囲を限定できるようにするため、全ページを一度にApp Routerへ切り替えるのではなく、1ページ単位で段階的に移行を進めました。

移行に必要な作業は、基本的にはNext.jsが公開している公式ドキュメント https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration を参考にしましたが、そのままの手順だと上手くいかない部分もあり、下記の手順で進めました。

1. useRouternext/router)の置き換え

useRouterは、Next.jsが提供するルーティング用のHooksで、URLのパスやクエリなどの情報の取得、ルーティング操作が行えます。

App Routerで動くコンポーネント内ではnext/navigationuseRouterを利用する必要があり、移行のためには従来のnext/routeruseRouterを置き換える必要があります。

router.replace router.push

useRouter が返すオブジェクトに対してreplacepushを呼ぶことでページ遷移を行うことができます。

Pages Router で next/routeruseRoter を使うとエラーになり、逆にApp Routerで next/navigationuseRouter を使うとエラーになるので移行する必要があります。

共通コンポーネントの場合 App Router と Pages Router から呼ばれる場合があるので、フルロードにはなってしまいますが、Next.js に一旦 window.location に置き換えました。

そして、全ページ移行後には next/navigationuseRouterを使うようにしました。

※このブログを書きながら公式ドキュメント https://nextjs.org/docs/pages/api-reference/functions/use-router#the-nextcompatrouter-export を再度読み返して気づいたのですが、移行用のnext/compat/routeruseRouter があったようです。こちらを使うべきでした。

router.pathname,router.queryの置き換え

URLのパス名やクエリパラメーターを取得する際に使うメソッドですが、next/navigation から usePathname , useSearchParams というHooksが提供されており、これらを使うように変更しました。返り値は完全に一緒ではないので少し実装を変える必要がありますが、Pages Router でも使うことができるので、移行前を行う前に置き換えることができます。

これらの置き換えの作業にあたっては、useRouter を変更するPRを少しづつ出すことで、コンフリクトを起こさないようにしました。

Jest や Storybook の mock なども行う必要があり、App Router への移行では一番大変な作業でした。

2. 各ページのコンポーネントに use client の付与

App Routerでは全てのコンポーネントがデフォルトでServer Componentとなります。

サーバー側でレンダリングされるのでHooksのuseStateやonChangeなどの処理は使えなくなるので、今までとかなり動作が異なります。

まずはApp Routerに移行をすることを優先し、一旦ファイルの先頭に "use client" を付与し全ページをClient Compoenentとして動かすことにしました。

Client Componentから呼ばれるコンポーネントは基本的にClient Componentとなるので、全コンポーネントに付与する必要はなく、各ページから最初に呼ばれるコンポーネントを変更していきました。

3. _document.tsx, _app.tsxlayout.tsx に移行

Pages Routerでは_document.tsxhtmlタグや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