⚙️

【Next.js】App RouterとRSCによるアーキテクチャ設計

2024/08/19に公開

はじめに

Next.jsにApp Routerが導入されたことで、開発者にとって便利な機能が増えた一方、App Routerを前提とした設計のベストプラクティスがまだ確立されておらず、設計時にさまざまな検討が必要になっています。
特に大規模プロジェクトでは、開発が進むにつれてコードが肥大化し、メンテナンス性が損なわれる可能性があります。そのため、コードを細分化して管理し、将来的な保守や拡張を容易にするアプリケーション設計が求められます。

今回の記事の項目

本記事では、App Routerの機能を最大限に活用するための設計について考察します。
特に以下の項目に焦点を当て、長期にわたってメンテナンス性の高いプロジェクトを構築するための方法を探ります。

  1. App Routerを活用した効率的なディレクトリ設計戦略
  2. レイアウトコンポーネントの効果的な利用
  3. RSCの活用 サーバーコンポーネントとクライアントコンポーネントの適切な配置
  4. ページ遷移の最適化とプリフェッチ

今回対象のプロジェクトの規模感

今回対象とするプロジェクトは中〜大規模で、具体的には以下のような規模感を想定しています。

  • ページ数:約20〜50ページ
  • コンポーネント数:約50〜200個
  • API エンドポイント:約20〜50個
  • 開発チーム:5〜20人

1. App Routerを活用した効率的なディレクトリ設計戦略

App Routerを導入したものの、コンポーネントや機能ファイルをどこに置くべきか、どのようなディレクトリ構成が最適なのか悩んでいませんか?
App Routerは、ファイルシステムベースのルーティングによって、開発者が直感的にアプリケーションの構造を定義できるようになりました。しかし、layout.tsxpage.tsxといった意味を持つファイルの登場により、app ディレクトリが複雑化し、ルーティング構造の可読性が低下するという課題が生じています。

大規模プロジェクトでは、ルーティング構造の可読性が低下すると、開発者がアプリケーション全体の構造を把握しづらくなり、保守性が損なわれる可能性があります。そのため、大規模プロジェクトにおけるディレクトリ構成はルーティング構造を明確にすることが重要です。

1.1 ディレクトリ構造の2つの案

私はディレクトリ構造について以下の2つの案を考えました。

  • 案1: コンポーネントや機能ファイルをappの外側に置く
  • 案2: コンポーネントや機能ファイルをappの内側に置く

結論を先にお伝えするとルーティング構造の明確化の観点から「案1: コンポーネントや機能ファイルをappの外側に置く」構成を推奨します。

本項ではそれぞれのメリットとデメリットを挙げ、最適なディレクトリ構成について考察します。

案1: コンポーネントや機能ファイルをappの外側に置く

このディレクトリ構成では全てのページのコンポーネントや機能をappと同階層の共有フォルダに置き、appディレクトリをルーティング目的のみにします。

src/
├── app/
│   ├── dashboard/
│   │   ├── page.tsx
│   │   └── layout.tsx
│   ├── profile/
│   │   ├── page.tsx
│   │   └── layout.tsx
│   ├── page.tsx
│   └── layout.tsx
├── components/
│   ├── Header.tsx
│   ├── Footer.tsx
│   ├── ProfileDetails.tsx
│   └── login.tsx
├── lib/

メリット

  • ルーティングに集中できる:app配下をルーティング関連のファイルに限定することで、ルーティング構造が明確になり、見通しが良くなります。
  • ファイル構造の管理が簡単: UIコンポーネントや機能コンポーネントを別のディレクトリに分けることで、役割ごとにファイルが整理され、管理が容易になります。
  • 再利用性の向上:appディレクトリ外に配置されたコンポーネントは、複数ページ間や別のプロジェクトで簡単に再利用することができます。

デメリット

  • ページに関するコンポーネントが管理しにくい: appディレクトリ外に配置されたコンポーネントは、appディレクトリ内のページファイルからインポートする必要があります。

案2: コンポーネントや機能ファイルをappの内側に置く

このディレクトリ構成ではページごとに使用するコンポーネントや機能をルートセグメントに分割します。

src/
├── app/
│   ├── dashboard/
│   │   ├── page.tsx
│   │   ├── layout.tsx
│   │   ├── components/
│   │   └── lib/
│   ├── profile/
│   │   ├── page.tsx
│   │   ├── layout.tsx
│   │   ├── components/
│   │   └── lib/
│   ├── page.tsx
│   ├── layout.tsx
│   ├── components/
│   └── lib/

メリット

  • ページごとのUIコンポーネントや機能が管理しやすい:各ページに関連するすべてのファイルを同一のディレクトリ内にまとめることで、そのページに関連するコンポーネントや機能が一目でわかりやすくなります。
  • 依存関係がシンプル: コンポーネントや機能ファイルが同じディレクトリ階層にあるため、他のコンポーネントやモジュールとの依存関係が明確になります。また、同じページ内でのみ使用されるコンポーネントは、他のページに影響を与えないため、依存関係がシンプルになります。

デメリット

  • ルーティングの可読性が低下: appディレクトリ内にコンポーネントや機能ファイルを配置すると、ルーティング構造の見通しが悪くなる可能性があります。特に、大量のコンポーネントが存在する場合、ルーティングの定義が複雑になり、どのページがどのルートに対応しているのかが分かりにくくなります。
  • 再利用性の低下: ページごとにコンポーネントを配置すると、特定のページに依存し、他のページや別のプロジェクトで再利用するのが難しくなります。再利用可能なコンポーネントが少なくなると、同じような機能やデザインを別の場所で再度作成する必要が出てきて、開発効率が下がる可能性があります。

【推奨】 案1:コンポーネントや機能ファイルをappの外側に置く 理由

メリットとデメリットを踏まえた上で、私は前者の機能やコンポーネントファイルはappの外側に置くことを推奨します。
中でも大きな理由はルーティング構造が明確化されるからです。
Next.jsのApp Routerが導入されてから、layout.tsxpage.tsxなど、特定の役割を持つファイルが増加しました。これら以外のコンポーネントや機能ファイルがappディレクトリ内に存在すると、ルーティングに関連するファイルが他のファイルと混在し、複雑になりがちです。

コンポーネントや機能ファイルをappの外側に分離することで、appディレクトリをルーティングに必要な最低限の構成要素に集中させることができます。これにより、ルーティング構造がシンプルかつ明確になり、プロジェクト全体の見通しが良くなります。特に大規模なアプリケーションでは、ルーティングを中心にディレクトリ構成を整理することで、開発者がプロジェクトに素早く慣れることができ、メンテナンス性が向上します。

1.2 ルーティング構造の可読性をさらに上げる方法

コンポーネントや機能ファイルをappの外側に置く他にルーティング構造の可読性をさらに上げる方法があります。
それはRoute GroupsPrivate Foldersを活用することです。

①Route Groups

フォルダ名を()で括ると、app 配下であっても括弧内のフォルダはルーティングから除外されます。
https://nextjs.org/docs/app/building-your-application/routing/route-groups

これはURLに影響を与えずにルーティングを整理するのに有効です。
たとえば、誰でも見れるページと認証済みのユーザーしか見れないページをグルーピングする場合の例です。(それぞれのディレクトリ内にpage.tsxが存在することが前提です。)

src/
├── app/ #/
│   ├── (auth)/ #/auth❌ ルーティング除外
│   │   ├── signin #/signin
│   │   └── signup #/signup
│   ├── (loggedIn) #/loggedIn❌ ルーティング除外
│   │   ├── dashboard #/dashboard
│   │   └── mypage #/mypage
│   ├── page.tsx
│   └── layout.tsx
├── components/
├── lib/

この例では、(loggedIn)配下のフォルダがログイン済みのユーザーしかアクセスできないページであることが一目でわかります。

②Private Folders

フォルダーの先頭に_アンダースコアを付けることで、そのフォルダーおよび全てのサブフォルダーがルーティングから除外されます。
Route Groupsと異なるのは、サブフォルダまで含めて除外される点です。
Private Foldersはapp内に配置されたUIやロジックをルーティングから完全に分離したい場合に有効です。

src/
├── app/ #/
│   ├── _common/ #/common❌ ルーティング除外
│   │   ├── header #/header❌ ルーティング除外
│   │   └── footer #/footer❌ ルーティング除外
│   ├── home #/home
│   │   ├── dashboard #/home/dashboard
│   │   └── mypage #/home/mypage
│   ├── page.tsx
│   └── layout.tsx
├── components/
├── lib/

このようにRoute GroupsPrivate Foldersを活用することで、ルーティング構造の可読性を上げることができます。

2. レイアウトコンポーネントの効果的な利用

本項ではレイアウトコンポーネントの効果的な活用方法について解説します。
Next.jsのファイル規約の一つであるレイアウトコンポーネント(layout.tsx)は、ページ間で共通のUIを作成するために用いられます。しかし、その役割は単なる共通UI作成にとどまらず、共通のロジックやデータフロー、パフォーマンスに影響を与える重要な要素です。

2.1 レイアウトコンポーネントを使うと何がいいか

レイアウトコンポーネントの最も大きな利点の一つは、部分レンダリングを可能にすることです。
部分レンダリングとは、ページ間のナビゲーション時に共通のレイアウトコンポーネントはその状態を保持し、一部のページコンポーネントのみが再レンダリングされることです。
https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering

部分レンダリングを行わない場合、ナビゲーションごとにクライアント上でページ全体が再レンダリングされます。一方、レイアウトコンポーネントを使用すると、変更される部分のみをレンダリングするため、転送されるデータ量と実行時間が削減され、結果としてパフォーマンスが大幅に向上します。
したがって、共通UIの実装には積極的にレイアウトコンポーネントの利用を検討すべきです。

2.2 ルートレイアウトに何を置く?

app 直下のlayout.tsxはルートレイアウトと呼ばれ、アプリケーション内のすべてのページで共有されます。では、ルートレイアウトに何を置くのが適切でしょうか?
ルートレイアウトに何を置くかは慎重に考える必要があります。以下のような要素を配置するのが良いでしょう。

  • html、head、body タグの構造
  • 静的なメタデータ(titleやmetaタグなど)
  • 全ページ共通の設定(Google Analyticsの初期化スクリプトなど)
  • グローバルなCSSやフォントのインポート

一方で、すべてのページで必須でない要素、たとえばヘッダー、フッターコンポーネントについては、各ページのlayout.tsxpage.tsxで個別にimportするのが賢明です。
その方が、特定のページでヘッダー、フッターが不要になった場合などに柔軟に対応できます。

2.3 共通UIだけにとどまらないレイアウトの使用例

レイアウトコンポーネントは共通UIだけでなくさまざまな活用方法があります。

  • 共通UIの効率的な作成:ヘッダー、フッター、サイドバーなどの共通UI要素を定義することで、コードの重複を減らし、アプリケーションの統一性を保ちます。これは一般的な使用方法です。
  • データ共有:レイアウトコンポーネントは、ページ間で共通のデータを渡すための便利な手段です。ユーザー情報や商品情報など、複数のページで必要となるデータをレイアウトコンポーネントでフェッチして共有することで、コードの可読性と保守性を向上させることができます。
  • 認証状態に基づくリダイレクト:レイアウトコンポーネントはデフォルトではサーバーコンポーネントとして機能するため、サーバーサイドでAPIを呼び出すことが可能です。これにより、ユーザーの認証状態を確認し、必要に応じてリダイレクトを実行するなど、通常はmiddlewareで行われる処理を実装できます。
8.20追記

https://www.ericburel.tech/blog/static-paid-content-app-router

  • エラーハンドリングとリダイレクト:レイアウトコンポーネントにエラーハンドリングを共通処理として実装することで、エラー発生時に一部のページで共通の特定の処理を行うことができます。
  • 動的なメタデータの生成:レイアウトコンポーネントを使って、ページごとに異なる動的なメタデータを生成することができ、SEOにも効果的です。

https://zenn.dev/kiwichan101kg/articles/e8273a4ada7458

以下はレイアウトコンポーネントの効果的な利用例です。

app/
├── layout.tsx #ルートレイアウト(静的なメタデータ、共通の初期化スクリプト、グローバルCSSなど)
├── page.tsx
│
├── (authenticated)/
│   ├── layout.tsx #認証済み用レイアウト(ヘッダー、エラーハンドリングなど)
│   │
│   ├── dashboard/
│   │   ├── layout.tsx #ダッシュボード用レイアウト(サイドバー、動的メタデータなど)
│   │   └── page.tsx
│   │
│   └── mypage/
│       ├── layout.tsx #マイページ用レイアウト(ユーザー情報をフェッチしコンテキストで共有)
│       ├── page.tsx
│       │
│       ├── profile/
│       │   ├── layout.tsx #/mypage/profile用レイアウト
│       │   └── page.tsx
│       │
│       └── settings/
│           ├── layout.tsx #/mypage/settings用レイアウト
│           └── page.tsx
│
├── (public)/
│   ├── layout.tsx #公開ページ用レイアウト(ヘッダー、フッターなど)
│   │
│   ├── about/
│   │   ├── layout.tsx #about/用レイアウト
│   │   └── page.tsx
│   │
│   └── contact/
│        ├── layout.tsx #contact/用レイアウト
│       └── page.tsx

レイアウトコンポーネントは、UIの作成にとどまらず、共通処理を実装するための重要な機能です。これを意識することで、レイアウトコンポーネントの活用方法がさらに広がります。

3. RSCの活用 サーバーコンポーネントとクライアントコンポーネントの適切な配置

続いてはサーバーコンポーネントとクライアントコンポーネントの適切な配置についてのお話です。
Next.jsのApp Routerで導入されたReact Server Components(RSC)は、アプリケーションのパフォーマンスと開発効率を大幅に向上させる革新的な技術です。しかし、この新しいパラダイムは、サーバーコンポーネントとクライアントコンポーネントをどのように適切に配置するかという新たな課題をもたらしました。
サーバーコンポーネントとクライアントコンポーネントの配置戦略を深掘りします。

3.1 サーバーコンポーネントとクライアントコンポーネントのの配置戦略

基本的な原則として、まずサーバーコンポーネントでの実装を検討し、必要な場合のみクライアントコンポーネントを使用するというアプローチを採用します。なぜならサーバーコンポーネントには以下の利点があるからです。

  • 初期ページロード時間の短縮:サーバー側でレンダリングされるため、クライアント側の処理を減らし、ページの初回ロード速度を向上させます。
  • クライアントサイドのJavaScriptバンドルサイズの削減:サーバーコンポーネントでは、クライアントに送信するJavaScriptコードが減少し、クライアントの負荷を軽減できます。
  • SEO対策:サーバーであらかじめHTMLを生成するため、検索エンジンにとってページの情報がわかりやすくなり、SEOに寄与します。
  • セキュリティの強化:認証や認可、機密データの処理をサーバー側で行うことで、クライアントへの漏洩リスクを削減できます。

①サーバーコンポーネントで行うべき処理

サーバーコンポーネントは、サーバーサイドでレンダリングされるため、初期ロード時に最適なパフォーマンスを提供することができます。基本的には、以下のようなロジックをサーバーコンポーネントに含めるようにします。

  • データフェッチ:APIの呼び出しやデータベースからの情報取得はサーバーで行うことで効率的に処理できます。
  • 認証と認可: ユーザーの認証や認可処理、アクセス制御などのセキュリティ関連の処理は、サーバーサイドで処理することでセキュリティを強化できます。
  • SEO対策:検索エンジンにとって有利なHTML構造を生成することで、SEO効果を高めます。
  • 静的コンテンツの提供:頻繁に変更されないコンテンツをサーバー側で静的に生成します。

②クライアントコンポーネントに切り分けるべき処理

サーバーコンポーネントでは対応できないためクライアントコンポーネントに切り分ける必要のある処理は以下の通りです。

  • 状態管理: ユーザーのインタラクションによって変化する状態は、クライアント側で管理する必要があります。
  • インタラクティブな操作: ボタンクリックやフォーム送信、ドラッグアンドドロップなど、ユーザーのリアルタイムな操作はクライアントで処理します。(フォーム送信に関してはサーバーサイドで処理を行うServer Actionという技術が登場しました。)
  • ブラウザ固有のAPI利用: localStorage や window オブジェクトへのアクセスが必要な場合、クライアントコンポーネントで処理を行う必要があります。

サーバーコンポーネントとクライアントコンポーネントの適切な配置によって、アプリケーションのパフォーマンスとユーザー体験が大きく改善されます。
このように開発時には、それぞれの特性を理解し、適切な形で組み合わせることが重要です。

4. ページ遷移の最適化とプリフェッチ

続いてはページ遷移に関するお話です。
Next.jsでは、ページ遷移をいくつかの方法で実現できます。特に、ユーザーエクスペリエンスを向上させるために、ページ遷移の高速化やシームレスな表示は不可欠です。そのために重要なのがプリフェッチです。

プリフェッチとは、ユーザーが特定のページを訪れる前に、バックグラウンドでそのページを事前に取得することを指します。クライアントサイドレンダリング(CSR)において、遷移先のページをシームレスに表示するためには、プリフェッチが重要な役割を果たします。
https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching

4.1 Next.jsのページ遷移方法とプリフェッチ

Next.jsのページ遷移方法を、プリフェッチの観点を踏まえて解説します。

<Link>コンポーネント

<Link>コンポーネントは、Next.jsが提供する、<a>タグを拡張したコンポーネントです。
https://nextjs.org/docs/app/api-reference/components/link

  • 使用可能箇所:サーバーコンポーネント※1、クライアントコンポーネント
  • SEOに有利<Link>コンポーネントは、内部的には<a>タグ※2を拡張していいるため、クローラーがリンクを認識しやすくSEOに有利です。
  • プリフェッチ機能がある<Link>コンポーネントはビューポートに表示されると、自動的にルートをプリフェッチを行います。これにより、ユーザーがリンクをクリックした際のページ表示速度が向上します。

useRouterフック

useRouterフックはクライアントコンポーネントで利用可能なNext.jsが提供するフックです。

  • 使用可能箇所:クライアントコンポーネント
  • SEOに不利: useRouterはクライアントサイドでのナビゲーションを行うため、SEOには不利です。
  • 手動でプリフェッチする必要がある: useRouterではrouter.prefetch("url")とすることでプリフェッチができますが、特に設定がない場合は実行されません。

公式でもSEOやプリフェッチの観点からページ遷移はuseRouterではなく、<Link>コンポーネントの使用を推奨しています。
https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#link-component

条件に基づいて異なるページに遷移させたい場合や、ボタンのクリックイベントなどのプログラム的なナビゲーションが必要な場合はuseRouterを使用し、それ以外は<Link>コンポーネントの使用が適しています。

まとめ

App Routerの導入により、Next.jsでの開発はますます効率的かつ直感的になりましたが、その反面、新たな設計上の課題も生じています。本記事では、ディレクトリ構成、レイアウトコンポーネントの活用、サーバーコンポーネントとクライアントコンポーネントの最適な配置、そしてページ遷移について考察しました。
Next.jsの機能を効果的に活用していくために、今後も引き続きベストプラクティスを探求していくことが重要です。

現在、Next.jsのApp Routerを活用し、T3 Stackといったモダンな技術(Next.js、tRPC、TypeScript、Tailwind CSS、Prisma、NextAuth.js)を組み合わせて、効率的かつスケーラブルなアプリケーションの構築について執筆中です。
次回は、App RouterにtRPCを導入した際のデータフェッチに焦点を当てた内容を投稿する予定なので、ぜひご期待ください!

T3 Stackについては気になる方は以前の記事をご覧ください。
https://zenn.dev/kiwichan101kg/articles/279cc65988a39b

Discussion