Next.js App Router時代のベストプラクティス

PlayGround-AdventCalendar2024、3日目担当のluckです!よろしくな!

0. 本記事の目的

2023年ごろにNext.js 13が発表され、従来のルーティング機能であるPage Routerに加え、新たにApp Routerが導入されました。公式ドキュメントでもApp Routerの使用が推奨されており、発表から1年以上が経過した現在、関連する記事も増えてきています。App Routerを採用する人も増えてきたでしょう。

App Routerは、これまでのReactやPage Routerの書き方とは大きく異なる点が多くあります。その背景には、React Server Components(RSC)という新しいアーキテクチャの導入があり、それに伴い開発の進め方にも大きな変化が生じています。

本記事では、App Router時代におけるNext.jsのベストプラクティスを紹介します。本記事の内容は、さまざまな記事や動画を参考にしてまとめたものです。

なお、「Next.jsやApp Routerがそもそも何かよくわからない」という方は、こちらの記事をご覧いただくと、基本的な概要を把握できると思います。

また、もし本記事の内容に誤りや補足が必要な点がありましたら、ご指摘いただけると幸いです。


1. App Routerにおけるベストプラクティス


1. 1 ルーティング


1.1.1 ルーティング設計

1つ目の観点:appディレクトリの役割

  • app外にプロジェクトファイルを配置する場合
    appディレクトリは主にルーティング機能(例: page.tsxlayout.tsxの配置)を担うためのディレクトリとします。
    ルーティング以外のプロジェクトファイル(例: コンポーネントやユーティリティなど)は、srclibといった同階層のディレクトリに配置する構成が一般的です。
  • app内にプロジェクトファイルを配置する場合
    全てのプロジェクトファイルをappディレクトリ内にまとめる方法もあります。この場合、ルーティング機能とプロジェクトのロジックが混在するため、ファイルの分け方に注意が必要です。

2つ目の観点:関連するものをまとめて、機能やルートごとに分ける

構成方法はさまざまありますが、以下のような例が挙げられます。

  • 例1: components, features, lib, hooks, typesなど、用途ごとにディレクトリを分ける。
  • 例2: ルートや機能単位で必要なファイル群をまとめる(例: features/homeに関連ファイルを集約)。

プロジェクトが大規模になりファイル数が増える場合、初期段階で構成を慎重に検討することが重要です。これにより、管理しやすく、後から変更を加えやすくなります。


1.1.2 プロジェクト配置設計

a. Atomic Design

概要: UIコンポーネントを「atoms」「molecules」「organisms」といった単位で細かく分類する手法です。これにより、コンポーネントの再利用性が非常に高まります。

メリット:同じUI要素を複数箇所で使い回せるため、保守性や効率性が向上します。

課題:「どこまでが1つのコンポーネントか」「どの粒度で分割すべきか」といった基準が難しい。分割しすぎると、全体像を把握しにくくなる場合があります。

b. Bulletproof-React

概要:featuresディレクトリを作成し、機能やルートごとにコンポーネントやロジックを集約する構成方法です。

メリット: 各ルートや機能単位で関連ファイルをまとめるため、変更や拡張がしやすくなります。

筆者の意見: Atomic Designは粒度の判断が難しく感じるため、個人的にはBulletproof-Reactの構成を推奨します。


1.2 コンポーネント

React Server Components(RSC)の特徴が顕著に現れるのが、コンポーネントの設計です。RSCでは、コンポーネントを以下のように Client Component(CC)Server Component(SC) に分類します。分類の基準は、レンダリングがクライアント側で行われるか、サーバー側で行われるかです。


1.2.1 Client Component(CC)

定義方法: ファイル先頭に"use client"と記述することで宣言します。

特徴: クライアント側でレンダリングされるコンポーネントです。

注意点:

  • CCの子コンポーネントは、たとえ"use server"を記述しても、親がCCである限りクライアント側でレンダリングされます。
  • CCを使用すると、その影響が親から子へと伝播するため、ツリー全体がクライアント側でレンダリングされる可能性があります。

基本的な利用方針:

  • CCの使用は可能な限り避けるべきです。特に、アプリケーションのルート付近での使用は推奨されません。
  • 使用する場合は、アプリケーションツリーの末端(葉)で限定的に利用します。

CCを使用するべきケース:

①React Hooksを使う場合 ...例: useState, useEffect, useRefなど

②イベントハンドラを使用する場合 ...例: onClick, onChangeなど

③ブラウザAPIを使用する場合 ...例: localStorage, sessionStorage, window, documentなど

④CCの方がパフォーマンス向上につながる場合 ...例: フォーム入力、検索機能、タブ切り替えなど、頻繁にインタラクションが発生するUI


1.2.2 Server Component(SC)

定義方法: 明示的な宣言なしでデフォルトでSCとして扱われます(必要に応じて"use server"と明記可能)。

特徴: サーバー側でレンダリングされるコンポーネントです。

基本的な利用方針: 基本的にはSCを使用することを推奨します。

SCを使用すべき理由:

①効率的なデータフェッチ
SC内でデータフェッチを行うことで、APIに近い位置(サーバーサイド)から直接リクエストを送信できます。これにより、不要なクライアントサーバー間の通信を削減し、効率化が図れます。

②パフォーマンス向上
クライアント側でのレンダリングを不要にできるため、初期表示速度が向上します。

③セキュリティの向上
SCはトークンやAPIキーなどの機密情報をサーバー内で安全に処理でき、セキュリティリスクを軽減します。

④キャッシュの活用
サーバーでレンダリングされたコンポーネントは自動的にキャッシュされるため、再利用時にパフォーマンスが向上します。たとえば、変更頻度の低い静的コンテンツなどに適しています。


1.3 データフェッチ

Next.jsでは、基本的に Server Component(SC) 内でデータフェッチを行うことが推奨されています。以下では、データフェッチにおけるベストプラクティスと具体例を紹介します。


1.3.1. APIを利用する場合

a. fetch

推奨理由:

  • デフォルトでServer-Side Rendering(SSR)をサポートしている。
  • Request Memorization機能が組み込まれている。
    SC内で同じエンドポイントに対して複数回リクエストを送った場合、差分がないと判断されるとリクエストを集約してくれる仕組みです。これにより、無駄な通信を防ぎ、効率的なデータ取得が可能です。
  • 他のレンダリング方式(ISR, CSRなど)をオプトインで選択できる柔軟性がある

b. Route Handlerを利用したAPI設計

概要:Next.jsでは、/app/apiディレクトリを利用してAPIを設計できます。これにより、フロントエンドから直接APIを構築・利用することが可能です。

利点:

  • ORMをAPI内部で使用し、データベースと通信。
  • API層を介すことでロジックを分離でき、以下のメリットが得られる:
    - 認証・認可のロジックを統一的に管理。
    - テストや保守が容易になる。
  • クライアント側から直接データベースにアクセスするのを防ぎ、セキュリティが向上。

検討事項:

  • API層を挟むことで通信回数が増えます
  • 保守性とスケーラビリティの観点から使用するのも手です(僕は実務で使ったことがあります)

1.3.2 ORMを利用する場合

概要:ORM(Object-Relational Mapping)は、サーバー側でデータベースとのやり取りを効率化するためのツールです。

主要な選択肢:

  1. Prisma: 最も広く使われており、豊富な機能とサポートがある。
  2. Drizzle: 学習コストが低く、SQLをそのまま活用できる新しい選択肢。

1.3.3 Server Actionsを利用する場合

概要:Server Actionsを使うと、APIを介さずにデータフェッチが可能です。例: フォーム送信やタスク実行のシンプルな実装。

個人的には非推奨:

a. フロントエンドとバックエンドを切り分けない設計は、保守性やスケーラビリティに欠ける。

b. API層を挟まないことで、ロジックの分離が難しくなる。


おまけ: サードパーティーライブラリの使用

概要:原則として、サードパーティーライブラリは Client Component(CC) でしか動作しないため、使用を避けるのが理想です。しかし、場合によってはデータフェッチや状態管理の簡略化のために利用することがありますが、これらを使うならそもそもReact+Viteで良いと思います。

主要なライブラリ:

  1. useSWR: データフェッチとキャッシュの自動管理に優れたライブラリ。
  2. Tanstack Query: より高度な状態管理とデータキャッシュが可能。
  3. useEffect(非推奨):
  • 親から子への「バケツリレー」が増える。
  • エラーハンドリングやローディング状態の管理が複雑化する。

1.4 キャッシュ

Next.jsでは、さまざまなキャッシュ機構が提供されており、効率的なデータ管理とパフォーマンス向上を実現できます。以下、それぞれのキャッシュについて解説します。


1.4.1 Request Memorization

概要: 前述のfetch機能に組み込まれているキャッシュ機能です。同一エンドポイントへのリクエストを効率化します

利点:

a. 重複排除

Server Component(SC)内で同じエンドポイントに複数回リクエストが送信されても、差分がなければ1回に集約されます。キャッシュの寿命はfetchリクエストの設定に依存し、自動的に削除されます。

b. 動的メタデータにも対応

例えば、ブログページごとにtitle(ブラウザタブの名前)を動的に設定する場合にも有効です。

注意点: データフェッチ層の分離

複数コンポーネントで共有するデータフェッチ処理は分離して管理することで、同一エンドポイントを保証し、キャッシュの恩恵を最大化できます。

c. fetch以外の利用時

fetch以外の方法(例: 直接DBクエリなど)では適用されないため、Reactのcache関数を使用してキャッシュ機能を追加する必要があります。

import { cache } from 'react';

const getUser = cache(async (userId) => {
  return await db.user.query(userId);
});

1.4.2 Data Cache

概要: Request Memorizationがユーザー単位のキャッシュであるのに対し、Data Cacheはアプリ全体で共有されるキャッシュです。他のユーザーも同じキャッシュを利用します

特徴: キャッシュの持続期間が永続的(再ビルドや再デプロイまで保持)であるため、取り扱いには注意が必要です。長さはfetch関数によって変わります

参考:キャッシュの流れ

設定:

  • デフォルト設定: { cache: "no-store" }(キャッシュなし)
  • キャッシュを有効化: { cache: "force-cache" }
  • 再検証オプション:

時間指定

fetch('https://...', { next: { revalidate: 3600 } });

タグトリガー

fetch('https://...', { next: { tags: ['a'] } });
revalidateTag('a'); // 特定のキャッシュを無効化

1.4.3 Full Route Cache

概要: 静的なページ全体(RSCツリーのバイナリ表現やHTML)をキャッシュします。静的レンダリング(SSG / ISR)の際に活用されます

特徴:

a. レンダリング結果(HTMLやRSCツリー)をキャッシュするため、初期表示の高速化が可能です。

b. キャッシュの持続期間はData Cacheと同様に永続的です。

c. App Routerの推奨方針: 静的レンダリングを推奨しており、パフォーマンス向上の観点からも静的ページにはこの手法を採用するのが望ましいです。


1.4.4 Router Cache

概要: ページ遷移時にクライアント側のメモリ内でキャッシュされます。例えば、戻る・進む操作の際にスムーズな遷移を実現します

特徴:

a. Linkコンポーネントでprefetch可能:

  • デフォルトでprefetch: true設定が有効。リンクが画面内に表示されると、静的データを事前に読み込みます。

b. キャッシュの有効期間:

  • 静的ページ: デフォルトで5分。
  • 動的ページ: デフォルトで30秒。

c. 無効化の条件:

  • server actionscookies.deleteなどの処理が実行された場合。
  • 明示的にrouter.refreshを呼び出した場合。

まとめ

  • キャッシュの理解が重要

静的サイト生成(SSG)やISRを使用する場合、意図せず古いデータが表示されることを防ぐため、キャッシュの仕組みを正しく理解する必要があります。

  • キャッシュ活用のメリット

リクエスト回数が多いAPIや負荷の高いページでキャッシュを適切に活用することで、効率的なデータ管理とパフォーマンス向上が期待できます。

  • 発展的な選択肢

さらに詳細なキャッシュ制御が必要な場合、Remixなどのフレームワークを併用することも検討できます。


1.5 レンダリング

React Server Components(RSC)とページ単位のレンダリング戦略(CSR, SSR, SSG, ISR)を混同してしまうことはありませんか?

RSCは コンポーネント単位のレンダリング戦略 にフォーカスしているのに対し、こちらは ページ単位でのHTML生成のタイミングと場所 に関するレンダリング戦略です。

結論として、各方式の主な違いは「HTMLを いつどこで 生成するか」にあります。それぞれの特徴を以下で解説します。


1.5.1 動的レンダリング

a. CSR(Client-Side Rendering): 遅い

概要: クライアント側でJavaScriptを実行してHTMLを動的に生成します。

特徴:

  • ファイル冒頭に"use client"を宣言したコンポーネントは、この方式でレンダリングされます。
  • 初期ロード時にJavaScriptが実行されるまでコンテンツが表示されないため、パフォーマンスが低下します。

ユースケース:

  • 単純なインタラクティブアプリケーションや、サーバーリソースを極力節約したい場合。

b. SSR(Server-Side Rendering): 中速

概要: リクエスト時にサーバー側でHTMLを生成します。

特徴:

  • App Routerを使用する場合、データフェッチのあるページはデフォルトでSSRになります。

注意点:

  1. HTMLが都度生成されるため、キャッシュが効きません。
  2. APIリクエストが頻発し、サーバー負荷が増大する可能性があります。

ユースケース:

  • 頻繁に更新されるコンテンツが必要なサービス(例: ダッシュボード、リアルタイムデータを扱うアプリ)。

1.5.2 静的レンダリング

a. SSG(Static Site Generation): 最速

概要: ビルド時に静的HTMLを生成します。初期表示が非常に速いのが特徴です。

特徴:

  • データフェッチのないページはデフォルトでこの方式が採用されます。
  • 静的なHTMLが生成されるため、ランタイムでサーバーリソースを消費しません。

ユースケース:

  • 更新頻度が低いページ(例: 商品紹介、会社概要ページ)。

b. ISR(Incremental Static Regeneration): 高速

概要: SSGと同様に静的HTMLを生成しますが、特定の条件や時間間隔でHTMLを再生成します。

特徴: 再生成プロセスがバックグラウンドで行われるため、静的ページの高速性と動的更新の柔軟性を両立できます。

ユースケース:

  • 更新頻度が中程度のサービス(例: ブログメディア、ニュースサイト、ECサイトの商品リスト)。

1.5.3 最新の進化: PPR(Progressive Rendering)

概要: ページやコンポーネント単位で異なるレンダリング方式を適用できる手法です。

特徴: 柔軟性が高く、特定のUI部分だけCSRやSSR、SSGを使い分けることが可能です。

ユースケース:

  • 高度にインタラクティブなサービスや、部分的なパフォーマンス最適化が求められるアプリケーション。

大事なポイント: サービスに応じた最適なレンダリング方式を選択する

  • 頻繁に更新されるサービスではSSRやISR。
  • 更新頻度が低く、初期表示速度が重要な場合はSSG。
  • PPRは新時代のベストプラクティスになるかも?
  • これらを適切に使い分けることで、パフォーマンスやユーザー体験を最大化できます。

1.6 メタデータ

メタデータとは、SEOやSNSでのシェアを最適化するための設定です。具体的には、以下のような情報を定義できます。

  • タブのタイトル(検索結果に表示されるタイトル)
  • OGP画像(SNSでシェアされた際に表示される画像)
  • ページの説明文(検索結果やSNSでのプレビューに表示されるテキスト)

Next.jsでは、以下の2種類のメタデータを設定できます。


1.6.1 静的メタデータ

概要:固定された内容をメタデータとして設定するものです。

記述方法:page.tsxまたはlayout.tsx内に以下のように記述します。

export const metadata = {
  title: 'My Page Title',
  description: 'This is a description for SEO and OGP.',
};

1.6.2 動的メタデータ

概要: 動的ルーティングのページなどで、内容を動的に生成する必要がある場合に使用します。

記述方法:page.tsxまたはlayout.tsx内に以下のように記述します。

export async function generateMetadata({ params }) {
  return {
    title: `Page for ${params.slug}`,
    description: `Details about ${params.slug}`,
  };
}

検討事項

a. メタデータの優先順位

複数のファイルでメタデータが定義された場合、次の優先順位に従って上書きされます。

  1. ファイル規約によるメタデータ(例: favicon.icoなど)
  2. 最下層のページやレイアウトで定義されたメタデータ
  3. 最上位のレイアウトで定義された共通メタデータ

b. ファイル名での自動認識

Next.jsは、特定の名前のファイルを配置するだけで自動的にメタデータとして認識します。例えば、以下のファイルを/appまたは/publicディレクトリ内に配置してください。

  • favicon.ico: ページアイコン
  • icon.svg: サイトアイコン(SVG形式)
  • apple-icon.png: Appleデバイス用アイコン
  • manifest.json: Webアプリマニフェスト
  • opengraph-image.png: OGP用画像
  • twitter-image.png: Twitter用画像
  • robots.txt: 検索エンジン向けのクロール指示
  • sitemap.xml: サイトマップ

これらのファイル名はNext.jsによって自動的に解析され、適切に処理されます。


まとめ

メタデータの基本設定を抑えて、まずは静的メタデータを設定できるようになることを目標にしましょう

2. 参考文献


PlayGround-AdventCalendar2024、明日はAzumaさんです!頼んだ!