🗂

Next.js App Router移行計画を進める上で考えていること

2024/05/14に公開

4月から株式会社マイベストにjoinしました。フロントエンドエンジニアのsena-vです。
本記事は「mybest BlogKaigi 2024」の19日目の記事です。

はじめに

弊社が運用するmybestのフロントエンドではReact / Next.jsを使用しており、現在Next.jsのアップデートに対応するための段階的な計画を進めています。

Next.jsのApp Router登場に伴う移行計画については前職までに何度か経験していますが、要所を理解しているかどうかで難易度がかなり異なってきます。

今回はNext.jsのApp Router移行に関して実施前に最低限知っておきたいことと、mybestの現在の状況とこれからやっていきたいことについて紹介したいと思います。

App Routerに移行するために必要なこと

App Routerへの移行は終わりではなく、App Router最適化への前提作業になります。
最大限にApp Routerの恩恵を引き出すために以下の環境を整えるべきだと思っています。

  • App Router移行・最適化に必要な知識を習得する
  • 品質担保のためのテスト戦略
  • ライブラリアップデート、数年後を見据えたライブラリ移行

App Router移行に関連する必要知識

PagesからAppへ

Next.jsはv13.3まで/pages以下のディレクトリ配置で自動的にルーティングを行なっていました。

v13から新たにApp Routerという機能が利用可能になり、v13.4でstableになりました。
これにより/appというディレクトリが追加され、Pages側でのルーティングと別にApp側でのルーティングも実施できるようになりました。それぞれのディレクトリがルーティングを司っていることから、区分けとしてPages RouterとApp Routerという呼称が一般的になっています。

Pages Router

Pages Directory側でコンポーネントを利用すると通常コンポーネントはクライアント側のものとして処理されます。サーバー側の処理はgetServerSidePropsなどNext.jsが提供するサーバー側でReactの一部処理を動かす関数が必要であり、それ以外の処理は全てクライアント側で実行されます。

App Router

App Directory側ではコンポーネントは通常サーバー側のものとして処理されます。

後述のReact Server Componentという仕組みを用いてサーバー側でコンポーネントを計算・クライアント側に送れる形に変換して送信します。クライアント側では「サーバー側からきたコンポーネントのデータ」と「クライアント側で処理するよう明示的に書かれたコンポーネント」を組み合わせて画面を表示します。

React Server Component

React Server Component(以降、RSC)はNext.jsに採用された段階ではまだReact-canaryと呼ばれる新機能を安定版リリース前に試すことができるテストチャンネルから使用されており、React v19で正式に利用が可能になる機能です。

その名の通りRSCに対応した環境で作成したコンポーネントはデフォルトでサーバー側で実行され、RSC Payloadという形式に変換されクライアント側に渡されます。

RSCではコンポーネントをasync形式で定義することができ、asyncで定義したコンポーネントはサーバー側でのみ実行が可能になります。asyncを用いず通常の定義をすることも可能で、その場合はコンポーネントがネストされクライアント側で使用されることになっても問題なく動作します。

コンポーネントファイル先頭に"use client"と記述すると、コンポーネントは例外的にクライアント側で実行されます。クライアント側ではRSC Payloadとクライアントコンポーネント(以降、CC)が処理されて、画面の表示が実施されます。

"use client"が記述されたコンポーネントはサーバー側とクライアント側の境界になるため、CC内で使用されるコンポーネントは全てクライアント側で処理されます。

※ コンポーネント上でcommand + クリックを実行すると対象のファイルが開きます

サンプルコードを見るとサーバー側のログであるターミナル側に<ServerComponentA />で記述したログが出ることが確認できます。また<ServerComponentB />"use client"がないため基本的にはRSCとして動作しますが、「定義にasyncがない=async componentではない」ためクライアントコンポーネントとして使用されます。

RSCは従来までのReactアプリケーションにライブラリ側でサーバーサイドで計算されるステージが追加されたという考え方が一番受け入れやすいと思います。ページを構築するにあたってサーバー側での実行ステージが増えたことにより本来クライアント側に送られたのちに処理をしなければいけなかった部分をサーバー側で実行できるようになり、クライアント側への転送量を抑えブラウザ環境のスペックに依存しにくいフロントエンドコードを生成することが可能になりました。

https://zenn.dev/uhyo/articles/react-server-components-multi-stage

Server Actions

Next.js v14からRSCと別の独立した機能としてServer Actionsがstableになりました。
コンポーネントから実行する処理に"use server"を付与することで、クライアント側から処理を実行するのではなくサーバー側で任意の処理を実行できるようになりました。

function Bookmark({ slug }) {
  return (
    <button
      formAction={async () => {
        "use server";
        await sql`INSERT INTO Bookmarks (slug) VALUES (${slug})`;
      }}
    >
      <BookmarkIcon>
    </button>
  )
}

Next.js開発元のVercelから発表された際に上の極端なコードが話題になりました。こういった形での利用ではなくクライアント側からのアクションで実行される処理をサーバー側に寄せることができるようになり、重い処理のサーバー側移譲やサーバー側専用ライブラリをクライアントから利用できることなどメリットが多く、RSCと並んでApp Routerを利用する上で非常に重要な機能になります。

サンプルコードではRSCでServer Actionを実行できるようにしています。
これまでのReactコードでは画面で実行されたコードはすべてブラウザ側で動作し、開発者ツールにログが出力されていましたが、ボタンを押すと開発環境のターミナル側(サーバー側)にログが出力されることがわかります。

勘違いされやすいのがServer Actionsはサーバー側でActionを実行する機能のため、RSC内部ではもちろん、クライアント側のコンポーネントからもServer Actionsを実行することができます。

Sever Actionsで定義された処理は内部的に、$ACTION_ID_が付与された上でアクションリストに登録されて実行可能になるようになっています。そのためRSCからもCCからもServer Actionで定義された処理は一律にサーバー側の実行可能処理として扱われ、クライアント側からServerActionの実態は参照できない形になっています。

https://zenn.dev/cybozu_frontend/articles/server-actions-deep-dive

App RouterとRSCとServer Actionsで実現できること

App Router環境では従来の環境と異なり、サーバー側でコンポーネントが処理されることがデフォルトになります。その上で単純な移行をしたいだけであればすべてのコンポーネントに"use client"をつけ、App Router環境で動作しないライブラリを乗り換えるだけで完了になります。

一方でApp Routerのパワーを最大限に引き出すためにはRSC化が必須であり、Server Actionsの積極的な利用が推奨されます。個人の端末やブラウザの性能に依存するクライアント側の処理を可能な限り減らすことで全体の高速化を実施すると同時に、処理をサーバー側に集約することによりログの収集性が高くなり、おそらく今後Next.jsで追加されるであろうRSCの開発体験を上げることのできるアップデートの恩恵を直接受けることが可能になります。

Next.jsが提供する処理に関してはstable状態のものでも後のバージョンで仕様が変更されたりReact側のリリースに合わせて変更される場合もあり、早い段階で利用すると別の仕様になってしまうなど難しいところもあります。

とはいえReactを使っていく上でRSCは必須になるためApp Routerへの移行はあくまで「サーバー側デフォルトの強制設定をOnにする手段」としてNext.jsの便利機能とうまく付き合いながらRSC側の機能を用いて開発を進めていくのが良いと思っています。

品質担保のためのテスト戦略

  • 関数単位のテストはJestによるUnit Test
  • コンポーネント単位のテストはReact Testing LibraryによるIntegration Test
  • 全体の動作のテストはPlaywrightによるEnd to end Test(E2E)

RSC登場以前では上記のテストをTesting trophyという考え方で行うことが推奨されてきました。

Testing trophyは図で示す通りのバランスでテストを追加することが推奨されており、厳密にテストを書いているプロジェクトではReact Testing Library(Integration Test)でとても多くのテストが書かれていると思います。

しかし現時点でReact Testing Libraryは公式でRSCのサポートをしていないため、App Routerに移行した上で最適化を進めた上で品質を担保するためにはPlaywrightを用いたE2Eテストを整備していく方法が現在では主流になっています。

E2EテストはflakyになりがちなことやCI知識が必要になること・環境整備が大掛かりになりがちなどのマイナス面もありますが、RSCのテストだけではなく周辺ライブラリのアップデート時やライブラリ切り替え時のデグレチェックなどで強力なため自分たちのプロダクトにあった粒度で早めに運用に乗せることをお勧めします。

ライブラリアップデートと数年後を見据えたライブラリ移行

Next.jsがアップデートされていない環境では他のライブラリもアップデートが滞っている環境も少なくないと思われます。バグの切り分けが難しくなることもあり、App Router移行作業前にライブラリはできるだけ最新化しておく必要があります。

一番影響を受けるのがCSS関連のライブラリで、Next.jsがv13.4でApp Router使用可能になった直後はRSCに対応しているCSSライブラリが少なくビルドができなかったりRSC側での描画がうまく更新されないなどの状態になっていました。

現在は対応するCSSライブラリも増えてきていますが、メンテナンス頻度が低かったりNext.jsアップデートに追従が遅かったライブラリもあるため、今後のReact/Next.jsのアップデートにどのくらいの速度で追従していくかを考慮して選択する必要があります。

他のライブラリに関してもApp Routerに移行したことでクライアント側でも使用できなくなるパターンや、クライアント側では動作するがRSCにすると使用できなくなるパターン、利用はできるが期待通りに動作していないなどのパターンもあるためアップデート・移行後の確認は厚めに行う必要があります。

ここで前述のE2Eテストが実装されていると確認にかかるコストはかなり軽減できます。可能な限り単純なテストシナリオはE2E側に集約することで、確認者の負担を軽減してバグ発見を早期に行うことができるようになります。

移行を何度か経験して感じたこと

App Router化はあくまでRSC最適化の始まりである

CSSライブラリの移行やテスト整備などが障壁になるため、作業としてはNext.jsのアップデートを実施し、App Routerへ移行、"use client"を付与して以前と同じ動作をさせるだけでも現在のコード量によってはかなり大きい工数が必要になります。

App Router化をした上で、初めてRSCにするべきコンポーネントの切り分け・Server Actionsを使った処理のサーバー側一元化などのアプローチを取ることが可能になるため、App Routerに移行してからもCCからRSCへのリファクタリングの時間はかなりかかることが想定されます。

Server Component自体はReact v19-betaの時点でほぼReact v19のアップデート内容として追加されることは決まっており、今後もReactを使っていく場合はRSC側のみ開発体験が向上するようなアップデートが追加される可能性もあることからRSCに最適化されたコンポーネント設計を身につけることやActionが書けることは必須になってくると思われます。

Next.jsの癖と不安定性との付き合い方を考える

キャッシュ関連

App Routerで一番わからないと言われる部分として、Next.js(App Router)には4つのキャッシュがあることがあげられます。

キャッシュについては現状App Router登場からある程度経過しているため解説の記事なども多く書かれていますが、デフォルトで無効化設定を実施した上で自身のプロダクトに必要と思われるものを適応していくのが良いと思います。

https://rightcode.co.jp/blogs/45465

ここに関してはプロダクトのサーバー有無・コンテンツ種別(頻繁に更新されるか)・CDNなど別のキャッシュが組み込まれているかにも左右されるためApp Routerの設定関連では一番気をつけたい部分です。

Canaryコードとの付き合い方について

React公式は現在でもRSCは安定版リリースされておらず、React側の機能に依存する処理については例えCanaryの物であっても変更が入る可能性があります。

ReactであればCanaryのコードを積極的に使わない場合大きな影響はないですが、Next.js側でReactのCanary機能を使用した場合にReact側のStable時に変更が加えられる可能性があります。

そのためNext.jsのStable=React側のCanaryの場合は世間的な利用率やissueなどでその機能がどれだけ議論されているか?(議論の上で変更される可能性があるか?)を見ながら利用していくことが大切になります。

これからmybestでやっていきたいこと

現在mybestでは、以前から存在したCypressでのE2EテストをPlaywrightへ移行した上で品質向上に向けてテスト項目の拡充やCIのチューニングを実施しています。

Next.jsをはじめとしたライブラリについては更新頻度が高くなく、弊社のRubyバージョン利用状況と比べてもあまり最新化できていないためライブラリのアップデート時にデグレ確認ができるようなE2Eシナリオを整備しつつ、dependabotとGitHub Actionsによる自動アップデートが実施できるようCIの整備を進めています。

ライブラリアップデート完了後も「品質担保目的のE2Eテスト拡充」「CSSライブラリの検討・移行」「App Routerへの段階的移行」「RSCへのリファクタリング」など対応を予定しているため、ここまでに触れた内容を全社展開した上で順次計画的に実施していければと思っています。

Discussion