Next.js App Router と Pages Router の進化におけるナビゲーション
Grace Collins
Solutions Engineer · Leapcell

はじめに
ウェブ開発の状況は常に変化しており、パフォーマンス、保守性、開発者体験に対する高まる要求に対応するために、新しいパラダイムやツールが登場しています。近年最も影響力のある進歩の 1 つは、Next.js フレームワーク内での進化、特に確立された Pages Router の後継としての App Router の導入です。この決定的なアーキテクチャの変更は、データ取得、レンダリング、ルーティングのための強化された機能を提供する、React アプリケーションの構築方法における重要な一歩を表しています。これらの 2 つのルーティングメカニズム間のニュアンスを理解することは、もはや単なる学術的な興味の問題ではなく、堅牢でスケーラブルで効率的なウェブ体験を構築することを目指す開発者にとって実践的な必要性となっています。この記事では、App Router と Pages Router を徹底的に調査し、それらの根本的なアーキテクチャを解剖し、それぞれの長所と短所を比較検討し、既存のアプリケーションを移行したり、新しいアプリケーションを開始したりするための明確な道筋を提供します。
Next.js のルーティングパラダイムの理解
あらゆるウェブアプリケーションの中心にあるのは、そのルーティングシステムであり、ユーザーがアプリケーションのさまざまな部分をどのようにナビゲートするか、そしてコンテンツがどのように配信されるかを決定します。Next.js はこれに取り組むための 2 つの主要なアプローチを提供しています。Pages Router と App Router です。それらの違いを把握するには、それらの基本的な原則と、それらが開発パターンにどのように影響するかを理解することが重要です。
Pages Router: ファイルシステムベースの基盤
Pages Router は、Next.js の元のルーティングシステムであり、ファイルシステムベースのルーティングの原則に基づいて動作します。pages
ディレクトリ内に配置された JavaScript、TypeScript、または JSX ファイルはすべて、自動的にルートになります。たとえば、pages/index.js
はルートパス (/
) に対応し、pages/about.js
は /about
にマッピングされます。
Pages Router の主な概念:
- ファイルシステムベースのルーティング: ルートは、
pages
ディレクトリ内のファイル構造によって定義されます。 - データ取得: データ取得は、主に
getServerSideProps
、getStaticProps
、getStaticPaths
を使用してページレベルで処理されます。これらの関数は、コンポーネントがレンダリングされる前にサーバーで実行され、サーバーサイドレンダリング (SSR) や静的サイト生成 (SSG) のような事前レンダリング戦略を可能にします。// pages/posts/[id].js export async function getServerSideProps(context) { const { id } = context.params; const res = await fetch(`https://api.example.com/posts/${id}`); const post = await res.json(); return { props: { post } }; } function Post({ post }) { return ( <div> <h1>{post.title}</h1> <p>{post.content}</p> </div> ); } export default Post;
- クライアントサイドナビゲーション:
next/link
は、ページ間のクライアントサイド遷移に使用され、高速なシングルページアプリケーション (SPA) のような体験を保証します。// pages/index.js import Link from 'next/link'; function HomePage() { return ( <div> <Link href="/about"> <a>私たちについて</a> </Link> </div> ); } export default HomePage;
Pages Router の利点:
- シンプルさと親しみやすさ: 従来のファイルベースのルーティングシステムから来た開発者にも理解しやすいです。
- 成熟したエコシステム: 長く、よくサポートされており、膨大な量のドキュメントとコミュニティリソースがあります。
- 関心の明確な分離: 各ページは通常、明確なルートとその関連データ取得ロジックを表します。
Pages Router の欠点:
- データ取得と UI の結合: データ取得ロジックは、多くの場合、ページコンポーネントに直接結び付けられており、ネストされたコンポーネント間でデータ取得を共有したり再利用したりすることが困難になります。
- 限定的なレイアウトネスト: 複雑なレイアウトの管理は、高階コンポーネントやプロパティドリリングを必要とすることが多く、煩雑になる可能性があります。
- 深いツリーでのパフォーマンスの課題: ディープにネストされたルートや複雑なデータ依存関係を持つアプリケーションのパフォーマンスを最適化することは、ルート変更時にページ全体が再レンダリングされることが多いため、困難な場合があります。
App Router: React Server Components パラダイム
App Router は、React Server Components (RSC) を基盤として構築されており、パラダイムシフトを表します。Pages Router をルーティングとデータ取得の主要な単位として扱うのではなく、App Router はよりきめ細かなコンポーネントレベルのアプローチを導入します。app
ディレクトリ内のファイルとフォルダはルートを定義しますが、レイアウト、ローディング状態、エラー境界、API ルートのための追加の規約があります。
App Router の主な概念:
- フォルダベースのルーティング: Pages Router と同様ですが、フォルダがルートを定義し、
page.js
ファイルがそのルートセグメントの UI を定義します。app/ ├── layout.js // ルートレイアウト、すべてのルートに適用 ├── page.js // ホームページ ├── dashboard/ │ ├── layout.js // ダッシュボードレイアウト、ダッシュボードルートに適用 │ ├── page.js // ダッシュボードホーム │ ├── analytics/ │ │ └── page.js // ダッシュボード分析 │ └── settings/ │ └── page.js // ダッシュボード設定
- React Server Components (RSC): これは基本的な構成要素です。RSC を使用すると、コンポーネントをバンドルしてクライアントに送信することなくサーバーでレンダリングできます。これにより、クライアントサイド JavaScript バンドルサイズが大幅に削減され、初期ページロードパフォーマンスが向上します。
app
ディレクトリでは、コンポーネントはデフォルトで Server Component になります。コンポーネントを Client Component にするには、"use client"
ディレクティブを使用します。// app/dashboard/page.js (デフォルトで Server Component) import { Suspense } from 'react'; import DashboardCharts from './DashboardCharts'; // これは Client Component になる可能性があります async function getAnalyticsData() { // サーバーサイドデータ取得 const res = await fetch('https://api.example.com/analytics'); if (!res.ok) throw new Error('Failed to fetch analytics'); return res.json(); } export default async function DashboardPage() { const data = await getAnalyticsData(); // Server Component で直接データ取得 return ( <div> <h1>ダッシュボード概要</h1> {/* DashboardCharts はインタラクティブ性が必要な場合はクライアントコンポーネントになる可能性があります */} <Suspense fallback={<p>チャートを読み込み中...</p>}> <DashboardCharts analyticsData={data} /> </Suspense> </div> ); } // app/dashboard/DashboardCharts.js (Client Component) // インタラクティビティが必要、例: useState、useEffect またはイベントリスナーの使用 "use client"; import { useState, useEffect } from 'react'; export default function DashboardCharts({ analyticsData }) { // インタラクティブなチャートのクライアントサイドロジック const [chartData, setChartData] = useState(analyticsData); useEffect(() => { // クライアントサイドのデータ更新やチャートライブラリの初期化など }, [analyticsData]); return ( <div> <h2>チャート</h2> {/* クライアントサイドライブラリを使用してチャートをレンダリング */} <pre>{JSON.stringify(chartData, null, 2)}</pre> </div> ); }
- ネストされたレイアウト: App Router は、UI コンポーネントが子ルートをラップすることを可能にする、深くネストされたレイアウトを自然にサポートします。どのフォルダレベルにも
layout.js
ファイルがあると、その子ルートをラップします。これにより、一般的な UI パターンにおけるコードの再利用性と保守性が大幅に向上します。// app/dashboard/layout.js export default function DashboardLayout({ children }) { return ( <section> <nav> <ul> <li>ダッシュボードナビゲーションリンク 1</li> <li>ダッシュボードナビゲーションリンク 2</li> </ul> </nav> {children} {/* ここに子ルート/ページがレンダリングされます */} </section> ); }
- ロジックのコロケーション:
loading.js
(ローディング状態用)、error.js
(エラー境界用)、route.js
(API ルート用) をそれぞれのルートセグメント内にコロケイトでき、モジュール性を向上させます。// app/dashboard/loading.js export default function Loading() { return <p>ダッシュボードデータを読み込み中...</p>; } // app/dashboard/error.js "use client"; // エラー境界はクライアントコンポーネントである必要があります export default function ErrorBoundary({ error, reset }) { return ( <div> <h2>エラーが発生しました!</h2> <button onClick={() => reset()}>再試行</button> </div> ); } // app/api/users/route.js (GET リクエストを /api/users に送信するための API ルート) import { NextResponse } from 'next/server'; export async function GET() { const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]; return NextResponse.json(users); }
- 高度なデータ取得: データ取得は、Server Component で
async
/await
を使用して直接行うことができ、getServerSideProps
やgetStaticProps
を必要としない、より直接的で効率的なデータ取得方法を提供します。Next.js は、fetch リクエストを自動的に重複排除およびキャッシュします。 - ストリーミングと Suspense: UI の部分を準備ができ次第クライアントにストリーミングするために React の Suspense を活用し、知覚されるロード時間を改善します。
App Router の利点:
- パフォーマンスの向上 (バンドルサイズ縮小、初期ロード高速化): コンポーネントをサーバーでレンダリングし、必要なクライアント JavaScript のみを送信することで、初期ロード時間が大幅に短縮されます。
- 強化されたデータ取得戦略: Server Component 内で直接データ取得を可能にし、コロケーションを促進し、ボイラープレートを削減します。自動リクエストメモ化とキャッシュにより、ネットワークリクエストが最適化されます。
- ネストされたレイアウトとコロケーション: 複雑な UI 構造を簡素化し、レイアウトと関連ロジック (ローディング、エラー) を対応するルートとともに配置できるようにすることで、保守性を向上させます。
- UI のストリーミング: React Suspense を活用して UI をクライアントに段階的にストリーミングし、知覚されるパフォーマンスを向上させます。
- 将来性: React Server Components や最新のウェブ開発ベストプラクティスへの方向性と一致しています。
App Router の欠点:
- 学習曲線がきつい: Server Components と Client Components のメンタルモデル、および新しいデータ取得ルールを理解するには、より深い理解が必要です。
- 成熟度: 本番環境対応ですが、Pages Router よりも新しいため、一部のエッジケースやコミュニティリソースはまだ開発中である可能性があります。
- デバッグの複雑さ: サーバーサイドとクライアントサイドの実行を区別することが、デバッグを困難にする場合があります。
- 古いライブラリとの統合: クライアントサイド専用の React ライブラリの中には、App Router パラダイム内でシームレスに動作させるために特定の調整が必要になる場合があります。
アーキテクチャの進化とハイブリッドアプローチ
Pages Router から App Router への移行は、よりきめ細かな制御、より良いパフォーマンス最適化、そして React の将来とのより強力な整合性への進化を表しています。Pages Router は、サーバーレンダリングされた React の良い出発点を提供しましたが、複雑な UI やデータ取得の最適化におけるその制限は明らかになりました。
Next.js は、同じプロジェクト内で pages
と app
ディレクトリの両方を共存させるハイブリッドアプローチを可能にするように設計されています。これは、段階的な移行戦略に非常に役立ちます。
- 競合するパスに対して、
pages
ディレクトリのルートがapp
ディレクトリのルートよりも優先されます。 - App Router を使用して、既存のアプリケーションの新しい機能やまったく新しいセクションを導入できます。
your-next-app/
├── app/ // 新機能、App Router
│ ├── dashboard/
│ │ └── page.js
│ └── products/
│ └── [id]/
│ └── page.js
├── pages/ // 既存機能、Pages Router
│ ├── index.js
│ ├── about.js
│ └── api/
│ └── hello.js
├── components/
├── public/
└── next.config.js
このハイブリッドモデルにより、開発者はアプリケーション全体を破壊的な「ビッグバン」書き直しなしに、App Router の利点を新しい開発のために活用できます。
既存アプリケーションの移行ガイド
既存の Next.js アプリケーションを Pages Router から App Router に移行することは daunting に思えるかもしれませんが、体系的なアプローチと主要な違いの理解により、管理可能になります。
1. フェーズ移行戦略 (推奨)
大規模なアプリケーションの場合、通常、「ビッグバン」移行は推奨されません。代わりに、フェーズアプローチを採用してください。
- 新しい機能に App Router を導入: 新しい機能やアプリケーションのまったく新しいセクションを App Router を使用して構築することから始めます。これにより、チームは既存の機能に影響を与えることなく、新しいパラダイムでの経験を積むことができます。
- リーフルートから移行: 独立して移行できる、より単純で複雑でない
pages
ディレクトリ内のルートを特定します。例としては、静的ページ (/about
、/contact
) や、ディープにネストされたレイアウトがない単純な動的ルート (/blog/[slug]
) があります。 - 共有コンポーネントとレイアウトの移行: 個々のルートに慣れたら、共有コンポーネントとレイアウトを移行する方法を検討します。App Router では、レイアウトは
app
ディレクトリ内のファイルであり、子ルートをラップします。 - データ取得の処理: これは多くの場合、移行の最も重要な部分です。
getServerSideProps
/getStaticProps
から Server Component 内での直接のfetch
呼び出しへの移行。
2. 主要な移行ステップと考慮事項
-
app
ディレクトリの作成: プロジェクトのルートに新しいapp
ディレクトリを単に作成します。Next.js はそれを自動的に認識します。 -
pages
をapp
に移動 (選択的に):pages/about.js
のような単純なページの場合:app/about/page.js
を作成します。pages/blog/[slug].js
のような動的なページの場合:app/blog/[slug]/page.js
を作成します。
-
layout.js
の更新:app/layout.js
: このファイルは、アプリケーションのルートレイアウトを定義します。<html>
と<body>
タグを定義する必要があります。Server Component がデフォルトなので、クライアントサイドプロバイダー (例: Redux、Context API、Chakra UI Provider) がある場合、これらは"use client"
コンポーネントでラップする必要があります。
// app/layout.js import './globals.css'; // グローバルスタイル // プロバイダーをここに移動し、クライアントコンポーネントでラップする可能性があります import Providers from './providers'; // providers.js がクライアントコンポーネントであることを確認してください export const metadata = { title: 'Next.js App Router', description: '移行例', }; export default function RootLayout({ children }) { return ( <html lang="en"> <body> <Providers> {/* Providers がクライアントサイドフックを使用する場合、"use client" が必要です */} {children} </Providers> </body> </html> ); }
app/providers.js
(クライアントサイドプロバイダーの例):
// app/providers.js "use client"; // このコンポーネントは Client Component である必要があります import { ThemeProvider } from 'next-themes'; // import { ReduxProvider } from '../redux/provider'; // 例 export default function Providers({ children }) { return ( <ThemeProvider attribute="class" defaultTheme="system" enableSystem> {/* <ReduxProvider> */} {children} {/* </ReduxProvider> */} </ThemeProvider> ); }
-
データ取得の更新:
getServerSideProps
/getStaticProps
をasync
Server Component に:export async function getServerSideProps(...)
またはgetStaticProps(...)
を削除します。page.js
コンポーネントをasync
関数にします。- コンポーネント内で直接
await fetch()
呼び出しを行います。 - Next.js は
fetch
リクエストを自動的にキャッシュし、getStaticProps
と同様にrevalidate
を処理します。
// 以前 (Pages Router の例) // pages/blog/[slug].js export async function getServerSideProps(context) { const { slug } = context.params; const res = await fetch(`https://api.example.com/blog/${slug}`); const post = await res.json(); return { props: { post } }; } function BlogDetails({ post }) { ... } // 後 (App Router の例) // app/blog/[slug]/page.js async function getPost(slug) { const res = await fetch(`https://api.example.com/blog/${slug}`, { next: { revalidate: 3600 } }); // 1時間ごとに再検証 if (!res.ok) throw new Error('Failed to fetch post'); return res.json(); } export default async function BlogDetailsPage({ params }) { const post = await getPost(params.slug); return ( <div> <h1>{post.title}</h1> <p>{post.content}</p> </div> ); }
getStaticPaths
の置き換え: App Router では、p.js
がgetStaticPaths
を置き換えます。// app/products/[id]/page.js export async function generateStaticParams() { const products = await fetch('https://api.example.com/products').then((res) => res.json()); return products.map((product) => ({ id: product.id.toString(), })); } export default async function ProductPage({ params }) { const product = await fetch(`https://api.example.com/products/${params.id}`).then((res) => res.json()); return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> </div> ); }
-
Client と Server Component の管理:
- クライアントサイドのインタラクティビティ (状態、エフェクト、イベントリスナー) を必要とするコンポーネントを特定します。これらのファイルを
"use client"
でプレフィックスを付けます。 "use client"
コンポーネントをインポートするコンポーネントも、クライアント固有のフックや機能を使用する場合はクライアントで実行されるようにします。- Server Component から Client Component にデータプロパティを渡します。
- クライアントサイドのインタラクティビティ (状態、エフェクト、イベントリスナー) を必要とするコンポーネントを特定します。これらのファイルを
-
loading.js
とerror.js
の実装: カスタムローディングスピナーとエラーハンドリングを Next.js の組み込み規約に置き換えます。- ルートセグメントに
loading.js
(Server Component) を作成して、ローディング UI を表示します。 - ルートセグメントに
error.js
(Client Component with"use client"
) を作成して、エラー境界に使用します。
- ルートセグメントに
-
API ルートの調整:
pages/api
ルートは、API ルートのためにapp
ディレクトリ内のroute.js
ファイルに置き換えられました。// app/api/my-endpoint/route.js import { NextResponse } from 'next/server'; export async function GET(request) { // request ヘッダー、クエリパラメータなどにアクセスできます return NextResponse.json({ message: 'Hello from App Router API!' }); } export async function POST(request) { const body = await request.json(); return NextResponse.json({ received: body }, { status: 200 }); }
-
テスト: 各移行されたルートとコンポーネントの機能、パフォーマンス、および正しいクライアント/サーバーの区別を徹底的にテストします。
3. 一般的な移行の落とし穴
- 意図しない Client Component: クライアントサイドの動作が必要なコンポーネントに
"use client"
を忘れる、または必要のないコンポーネントに誤って追加する。 - Client Component でのサーバーサイド専用機能: Client Component で Node.js 固有の API (例:
fs
、process.env
NEXT_PUBLIC_プレフィックスなしで直接) を使用しようとすること。 - データ再検証: App Router での
fetch
リクエストの新しいデータキャッシュおよび再検証モデルを理解することが、最新のデータを確保するために重要です。 - 状態管理: グローバル状態管理 (例: Redux、Zustand) やコンテキスト API を使用している場合、プロバイダーが
app/layout.js
または特定のクライアント境界で Client Component として正しく設定されていることを確認してください。 - CSS バンドリング: グローバル CSS は
app/layout.js
でインポートする必要があります。モジュール CSS (例:component.module.css
) や Tailwind CSS は同様に機能します。
段階的なアプローチに従い、これらの主要な変更を理解することで、App Router への移行はスムーズでやりがいのあるプロセスとなり、大幅なパフォーマンスと開発者体験のメリットを解き放つことができます。
結論
Next.js Pages Router から App Router への進化は、特に React Server Components を採用したことで、ウェブアプリケーション开发へのアプローチにおいて重要な飛躍となります。Pages Router は、ファイルシステムベースのルーティングと事前レンダリングソリューションを提供しましたが、App Router は、クライアントサイド JavaScript の削減、強化されたデータ取得、ネイティブストリーミング機能によるパフォーマンスを最適化する、よりきめ細かなコンポーネントレベルのパラダイムを導入します。Pages Router のシンプルさを小規模プロジェクトに選択するか、スケーラブルで高性能なアプリケーションのために App Router の高度な機能を利用するかを問わず、それぞれのアーキテクチャモデルを理解することが最も重要です。既存のプロジェクトでは、戦略的なフェーズ移行により、開発者は混乱なく App Router のメリットを段階的に採用できます。これにより、Next.js は最新の Web のための最先端フレームワークとしての地位を確立します。Next.js 開発の将来は、間違いなく App Router に向かっており、高速で保守性の高い React アプリケーションを構築するための堅牢な基盤を提供します。