Kuboyage

・ フロントエンド / 設計

 Next.js Page RouterとApp Router - アーキテクチャ思想の違いによる選定基準🚀

はじめに

こんにちは!H×H のセンリツ大好きエンジニアです!✌️

最近、Next.js の Page Router から App Router への移行を検討している方も多いのではないでしょうか?しかし、単なる「新しいバージョンだから移行する」では、本質的な違いを理解できていないかもしれません 💭

実は先日、Zenn で akfm_sato さんが書かれた「React チームが見てる世界、React ユーザーが見てる世界」という記事を読んで、非常に腑に落ちることがありました!この記事では、自律分散的アーキテクチャ vs 中央集権的アーキテクチャ という観点で React の思想を解説されており、これがまさに Next.js の Page Router と App Router の根本的な違いを説明する鍵だったのです 🔑

この記事では、その洞察を活かしながら、Next.js の 2 つのアプローチがどのように違うのか、ディレクトリ構造、レンダリング戦略、状態管理の 3 つの観点から深掘りしていきます!

React チームビジョンからみえた Next.js の思想変遷

React の歴史的文脈

まず理解しておきたいのは、React が Meta の大規模開発を支えるために生まれたフレームワークであるということです。Meta では Facebook の複雑な UI を管理する必要があり、その解決策として シンプルでスケーラブルなアーキテクチャ が求められていました。

この背景から、React は以下の特徴を持つように設計されました:

  • コンポーネント指向: 各部品が自律的に動作
  • 関数型指向: 予測可能で副作用の少ない設計
  • 再レンダリング: 状態変化に応じた UI 更新

Next.js と React の関係性

Next.js は、この React の思想を ウェブアプリケーション開発により適用しやすい形 にしたフレームワークです。しかし、Page Router と App Router では、この React の思想をどう解釈するかが大きく異なります 😮

💡 重要な洞察: Page Router は React の 実用性 を重視し、App Router は React の 本来の思想 により忠実に従っています

Page Router の中央集権的アーキテクチャ

設計思想

Page Router は、従来の Web アプリケーション開発に慣れた開発者にとって 直感的で理解しやすい 中央集権的なアプローチを採用しています。

中央集権的な特徴

pages/
├── _app.tsx          # アプリケーション全体の設定
├── _document.tsx     # HTMLドキュメント全体の制御
├── index.tsx         # ホームページ
├── about.tsx         # 単一ページ
└── blog/
    ├── index.tsx     # ブログ一覧
    └── [slug].tsx    # 動的ルート

特徴:

  • ファイル = ページ という 1 対 1 の明確な関係
  • _app.tsx で全体的な設定を一元管理
  • ページ単位でのレンダリング戦略決定

メリット

  1. 学習コストが低い 📚

    • 従来の MVC アーキテクチャに近い
    • ファイル構造が直感的
  2. 開発スピードが早い ⚡

    • 小〜中規模プロジェクトで威力を発揮
    • プロトタイピングに最適
  3. エコシステムの充実 🌟

    • 長い歴史による豊富な情報
    • ライブラリの対応状況が良い

デメリット

  1. スケーラビリティの課題 📈

    • 大規模になるとファイル管理が複雑
    • グローバル状態の管理が困難
  2. 柔軟性の制限 🔒

    • ページ全体でのレンダリング戦略
    • 細かい最適化が困難

App Router の自律分散的アーキテクチャ

設計思想

App Router は、React の 本来の思想により忠実 で、各コンポーネントが自律的に動作する分散型アーキテクチャを採用しています。

自律分散的な特徴

app/
├── layout.tsx        # ルートレイアウト
├── page.tsx          # ホームページ
├── about/
│   └── page.tsx      # About ページ
└── blog/
    ├── layout.tsx    # ブログ用レイアウト
    ├── page.tsx      # ブログ一覧
    └── [slug]/
        └── page.tsx  # 記事詳細

特徴:

  • セグメント指向 でのルート構成
  • 各セグメントが独立したレイアウトを持てる
  • コンポーネント単位での細かい制御

React Server Components との統合

App Router の真骨頂は、React Server Components (RSC) との深い統合です:

// Server Component (デフォルト)
async function BlogPage() {
  // サーバーで実行される
  const posts = await fetch("/api/posts");

  return (
    <div>
      <PostList posts={posts} />
      <ClientComponent /> {/* 必要な部分のみクライアント */}
    </div>
  );
}

// Client Component
("use client");
function ClientComponent() {
  const [state, setState] = useState();
  // クライアントサイドの処理
}

メリット

  1. 高いスケーラビリティ 🚀

    • 大規模アプリケーションに適している
    • コンポーネント単位での最適化
  2. 優れたパフォーマンス ⚡

    • Server Components による初期ロード高速化
    • ストリーミングによる段階的レンダリング
  3. モダンな開発体験 ✨

    • TypeScript との高い親和性
    • React 18 の最新機能を活用

デメリット

  1. 学習コストが高い 📚

    • 新しい概念の理解が必要
    • Server/Client Components の使い分け
  2. エコシステムの発展途上 🌱

    • 一部ライブラリの対応待ち
    • ベストプラクティスが確立中

ディレクトリ構造の設計思想比較

Page Router: ファイルベースルーティング

📁 pages/
├── 📄 _app.tsx              # グローバル設定
├── 📄 _document.tsx         # HTMLドキュメント
├── 📄 index.tsx             # / ルート
├── 📄 about.tsx             # /about ルート
├── 📁 blog/
│   ├── 📄 index.tsx         # /blog ルート
│   └── 📄 [slug].tsx        # /blog/[slug] ルート
└── 📁 api/
    └── 📄 posts.ts          # /api/posts エンドポイント

思想: 「ファイルがそのままルートになる」シンプルさを重視

App Router: セグメントベースルーティング

📁 app/
├── 📄 layout.tsx            # ルートレイアウト
├── 📄 page.tsx              # / ルート
├── 📄 loading.tsx           # ローディングUI
├── 📄 error.tsx             # エラーUI
├── 📁 about/
│   └── 📄 page.tsx          # /about ルート
├── 📁 blog/
│   ├── 📄 layout.tsx        # /blog/* 共通レイアウト
│   ├── 📄 page.tsx          # /blog ルート
│   ├── 📄 loading.tsx       # /blog/* ローディング
│   └── 📁 [slug]/
│       ├── 📄 page.tsx      # /blog/[slug] ルート
│       └── 📄 loading.tsx   # /blog/[slug] ローディング
└── 📁 api/
    └── 📄 posts/
        └── 📄 route.ts      # /api/posts エンドポイント

思想: 「各セグメントが責任を持つ」階層的な設計

特殊ファイルの意味

Page RouterApp Router意味
_app.tsxlayout.tsxアプリケーション・セクション全体の設定
_document.tsxlayout.tsx (root)HTML ドキュメント構造
なしloading.tsxローディング状態の UI
なしerror.tsxエラー状態の UI
なしnot-found.tsx404 ページ

🎯 設計の違い: Page Router は「ページ中心」、App Router は「ユーザー体験中心」

レンダリング戦略の根本的違い

Page Router: ページ単位のレンダリング

// pages/blog/[slug].tsx
export async function getStaticProps({ params }) {
  // ビルド時に実行
  const post = await fetchPost(params.slug);
  return { props: { post } };
}

export async function getServerSideProps({ params }) {
  // リクエスト毎に実行
  const post = await fetchPost(params.slug);
  return { props: { post } };
}

function BlogPost({ post }) {
  // ページ全体が同じレンダリング戦略
  return <div>{post.title}</div>;
}

特徴:

  • ページ全体で統一されたレンダリング戦略
  • getStaticProps, getServerSideProps でページ単位制御
  • シンプルで理解しやすい

App Router: コンポーネント単位のレンダリング

// app/blog/[slug]/page.tsx
async function BlogPost({ params }) {
  // Server Component - サーバーで実行
  const post = await fetchPost(params.slug);

  return (
    <div>
      <PostHeader post={post} />
      <PostContent content={post.content} />
      <CommentSection postId={post.id} /> {/* Client Component */}
    </div>
  );
}

// app/blog/[slug]/loading.tsx
function Loading() {
  return <div>Loading...</div>;
}

特徴:

  • コンポーネント単位での細かい制御
  • Server/Client Components の組み合わせ
  • ストリーミングによる段階的レンダリング

パフォーマンス比較

項目Page RouterApp Router
初期ロードページ全体で決定コンポーネント単位で最適化
JavaScript サイズ大きくなりがち必要最小限
ストリーミング限定的フル対応
キャッシュ戦略ページレベルコンポーネント/データレベル

状態管理パラダイムの変化

Page Router: グローバル状態中心

// pages/_app.tsx
function MyApp({ Component, pageProps }) {
  return (
    <GlobalStateProvider>
      <Component {...pageProps} />
    </GlobalStateProvider>
  );
}

// Context や Redux で状態を一元管理
const GlobalStateProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);

  return (
    <StateContext.Provider value={{ user, posts, setUser, setPosts }}>
      {children}
    </StateContext.Provider>
  );
};

思想: 中央集権的な状態管理で、アプリケーション全体の状態を一箇所で管理

App Router: ローカル状態重視

// app/blog/page.tsx - Server Component
async function BlogPage() {
  // サーバーで直接データを取得
  const posts = await fetchPosts();

  return (
    <div>
      <PostList posts={posts} />
      <SearchForm /> {/* 必要に応じてClient Component */}
    </div>
  );
}

// app/components/SearchForm.tsx - Client Component
("use client");
function SearchForm() {
  // ローカルな状態管理
  const [searchTerm, setSearchTerm] = useState("");
  const [results, setResults] = useState([]);

  return (
    <form>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
    </form>
  );
}

思想: 各コンポーネントが必要な状態のみを管理し、グローバル状態は最小限に

状態管理の比較表

項目Page RouterApp Router
主なアプローチグローバル状態管理ローカル状態 + サーバー状態
人気ライブラリRedux, ZustandReact Query, SWR, Server State
データフェッチクライアント中心サーバー中心
状態の所在中央集権的分散的

実際の開発体験での比較

小規模プロジェクト (ブログサイト)

Page Router の場合:

// pages/blog/[slug].tsx
export async function getStaticProps({ params }) {
  const post = await cms.getPost(params.slug);
  return { props: { post } };
}

function BlogPost({ post }) {
  return (
    <Layout>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </Layout>
  );
}

App Router の場合:

// app/blog/[slug]/page.tsx
async function BlogPost({ params }) {
  const post = await cms.getPost(params.slug);

  return (
    <div>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </div>
  );
}

🤔 小規模での印象: Page Router の方がシンプルで分かりやすいです

大規模プロジェクト (ダッシュボードアプリ)

Page Router の問題:

  • ページ毎にデータフェッチが必要
  • レイアウトの共通化が困難
  • 状態管理が複雑化

App Router の利点:

  • 階層的なレイアウト設計
  • コンポーネント単位でのデータフェッチ
  • ストリーミングによる UX 向上
// app/(dashboard)/layout.tsx
async function DashboardLayout({ children }) {
  const user = await getUser();

  return (
    <div className="dashboard-layout">
      <Sidebar user={user} />
      <main>{children}</main>
    </div>
  );
}

// app/(dashboard)/analytics/page.tsx
async function AnalyticsPage() {
  // この部分だけでデータを取得
  const analytics = await getAnalytics();

  return <AnalyticsChart data={analytics} />;
}

どちらを選ぶべきか?

Page Router を選ぶべき場合 🎯

  1. 小〜中規模のプロジェクト

    • 個人ブログ、コーポレートサイト
    • ランディングページ
  2. チームの学習コストを重視したい

    • React 初心者が多い
    • 短期間での開発が必要
  3. 安定性を重視したい

    • 成熟したエコシステム
    • 豊富な情報とコミュニティ

App Router を選ぶべき場合 🚀

  1. 大規模なアプリケーション

    • 複雑な UI 構造
    • 高いパフォーマンス要求
  2. モダンな開発体験を重視したい

    • 最新の React 機能を活用
    • TypeScript との高い親和性
  3. 長期的な保守性を考慮したい

    • Next.js の今後の方向性
    • React の思想との整合性

移行のタイミング 🕐

// 段階的移行の例
// Step 1: 新機能はApp Routerで
app/
├── new-feature/
│   └── page.tsx

// Step 2: 既存ページを徐々に移行
pages/      // 既存のPage Router
├── old-page.tsx
app/        // 新しいApp Router
├── migrated-page/
│   └── page.tsx

⚠️ 注意: 一つのプロジェクトで Page Router と App Router は併用可能です

まとめ

Next.js の Page Router と App Router の違いは、単なる機能の差ではなく、根本的な設計思想の違いです 🎯

🔑 重要なポイント

  1. Page Router = 中央集権的アーキテクチャ

    • 理解しやすく、小〜中規模に適している
    • 従来の Web 開発の延長線上
  2. App Router = 自律分散的アーキテクチャ

    • React の本来の思想に忠実
    • 大規模アプリケーションに適している
  3. 選択の指針

    • プロジェクトの規模と複雑性
    • チームの学習コスト
    • 長期的な保守性

🚀 今後の展望

React チームの方向性を見ると、自律分散的アーキテクチャがより推進されていくと考えられます。一方で、中央集権的なアプローチが不要になるわけではなく、用途に応じた 適切な選択 が重要です。

App Router は確かに学習コストが高いですが、一度理解すれば より柔軟で拡張性の高い アプリケーションを構築できます。逆に、Page Router は シンプルで直感的 で、多くの用途では十分以上の機能を提供します。

💡 最終的なメッセージ: どちらも素晴らしいアプローチです。プロジェクトの要件とチームの状況に応じて、適切な選択をしましょう!


参考文献:

この記事が、Next.js の選択で迷っている皆さんの参考になれば嬉しいです!

ブログの記事一覧へ