・ フロントエンド / 設計

はじめに
こんにちは!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で全体的な設定を一元管理- ページ単位でのレンダリング戦略決定
メリット
-
学習コストが低い 📚
- 従来の MVC アーキテクチャに近い
- ファイル構造が直感的
-
開発スピードが早い ⚡
- 小〜中規模プロジェクトで威力を発揮
- プロトタイピングに最適
-
エコシステムの充実 🌟
- 長い歴史による豊富な情報
- ライブラリの対応状況が良い
デメリット
-
スケーラビリティの課題 📈
- 大規模になるとファイル管理が複雑
- グローバル状態の管理が困難
-
柔軟性の制限 🔒
- ページ全体でのレンダリング戦略
- 細かい最適化が困難
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();
// クライアントサイドの処理
}
メリット
-
高いスケーラビリティ 🚀
- 大規模アプリケーションに適している
- コンポーネント単位での最適化
-
優れたパフォーマンス ⚡
- Server Components による初期ロード高速化
- ストリーミングによる段階的レンダリング
-
モダンな開発体験 ✨
- TypeScript との高い親和性
- React 18 の最新機能を活用
デメリット
-
学習コストが高い 📚
- 新しい概念の理解が必要
- Server/Client Components の使い分け
-
エコシステムの発展途上 🌱
- 一部ライブラリの対応待ち
- ベストプラクティスが確立中
ディレクトリ構造の設計思想比較
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 Router | App Router | 意味 |
|---|---|---|
_app.tsx | layout.tsx | アプリケーション・セクション全体の設定 |
_document.tsx | layout.tsx (root) | HTML ドキュメント構造 |
| なし | loading.tsx | ローディング状態の UI |
| なし | error.tsx | エラー状態の UI |
| なし | not-found.tsx | 404 ページ |
🎯 設計の違い: 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 Router | App 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 Router | App Router |
|---|---|---|
| 主なアプローチ | グローバル状態管理 | ローカル状態 + サーバー状態 |
| 人気ライブラリ | Redux, Zustand | React 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 を選ぶべき場合 🎯
-
小〜中規模のプロジェクト
- 個人ブログ、コーポレートサイト
- ランディングページ
-
チームの学習コストを重視したい
- React 初心者が多い
- 短期間での開発が必要
-
安定性を重視したい
- 成熟したエコシステム
- 豊富な情報とコミュニティ
App Router を選ぶべき場合 🚀
-
大規模なアプリケーション
- 複雑な UI 構造
- 高いパフォーマンス要求
-
モダンな開発体験を重視したい
- 最新の React 機能を活用
- TypeScript との高い親和性
-
長期的な保守性を考慮したい
- 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 の違いは、単なる機能の差ではなく、根本的な設計思想の違いです 🎯
🔑 重要なポイント
-
Page Router = 中央集権的アーキテクチャ
- 理解しやすく、小〜中規模に適している
- 従来の Web 開発の延長線上
-
App Router = 自律分散的アーキテクチャ
- React の本来の思想に忠実
- 大規模アプリケーションに適している
-
選択の指針
- プロジェクトの規模と複雑性
- チームの学習コスト
- 長期的な保守性
🚀 今後の展望
React チームの方向性を見ると、自律分散的アーキテクチャがより推進されていくと考えられます。一方で、中央集権的なアプローチが不要になるわけではなく、用途に応じた 適切な選択 が重要です。
App Router は確かに学習コストが高いですが、一度理解すれば より柔軟で拡張性の高い アプリケーションを構築できます。逆に、Page Router は シンプルで直感的 で、多くの用途では十分以上の機能を提供します。
💡 最終的なメッセージ: どちらも素晴らしいアプローチです。プロジェクトの要件とチームの状況に応じて、適切な選択をしましょう!
参考文献:
この記事が、Next.js の選択で迷っている皆さんの参考になれば嬉しいです!