Next.js App Router アーキテクチャ概要¶
2025年版 - モダンアーキテクチャ設計ガイド
はじめに¶
本ドキュメントは、React 18/19 Server Componentsとモダンフロントエンド設計パターンを統合した、Next.js App Routerアーキテクチャ(Next.js 13-15)の包括的なガイドを提供します。
目的¶
- 仕様とアーキテクチャの議論を統合する
- 設計思想とその根拠を文書化する
- コード生成とチームコラボレーションのための再利用可能な知識を作成する
対象読者¶
- Next.js 14-15開発者
- React Server ComponentsとServer Actionsを学習するエンジニア
- フロントエンドでDomain-Driven Design (DDD)を実装するチーム
- 設計哲学と用語を整理する技術リード
App Router の哲学¶
核となる概念¶
App Routerは、Next.jsアーキテクチャにおける根本的な変化を表しています:
- ディレクトリ構造:
pages/からapp/ディレクトリへの移行 - デフォルトでServer Components: React Server Components (RSC)がデフォルト
- Client Components: 明示的な
"use client"ディレクティブが必要 - 自動キャッシング: サーバーサイドのfetchが自動的にキャッシュされる
- ISR統合:
revalidateオプションによりIncremental Static Regenerationが有効化
主要機能¶
| 概念 | 説明 |
|---|---|
| React Server Components (RSC) | サーバー上で直接データを取得し、クライアントにストリーミング |
| Server Actions | "use server"で宣言された関数をクライアントから呼び出し可能 |
| Streaming SSR | <Suspense>と組み合わせて部分的なHTML配信を実現 |
| Edge Runtime | CDNノード上での低レイテンシ実行 |
| Route Handlers | app/api/route.tsで定義されるAPIルート |
アーキテクチャの原則¶
1. Server/Client 責務の分離¶
| レイヤー | 主な責務 |
|---|---|
| Server Component | データ取得、ドメインロジック、キャッシュ制御 |
| Client Component | ユーザー入力、UIイベント、状態管理 |
原則: "サーバー上で可能な限り全てを実行する"
これによりバンドルサイズを最小化し、再レンダリングのオーバーヘッドを削減します。
2. App Router ディレクトリ構造¶
app/
├── (marketing)/ # Route groups (URLに影響しない)
│ ├── layout.tsx
│ └── page.tsx
├── blog/
│ ├── [slug]/
│ │ └── page.tsx # 動的ルート
│ ├── loading.tsx # Loading UI
│ ├── error.tsx # エラーバウンダリ
│ └── page.tsx
├── api/
│ └── posts/
│ └── route.ts # API Route Handler
├── layout.tsx # ルートレイアウト
└── page.tsx # ホームページ
3. Server vs Client Components¶
Server Components (デフォルト)を使用すべき場合: - データベースやAPIからデータを取得する - バックエンドリソースにアクセスする - 機密情報をサーバーサイドに保持する(APIキー、トークン) - クライアントサイドのJavaScriptを削減する
Client Components ("use client")を使用すべき場合:
- インタラクティビティを追加する(onClick、onChange)
- hooksを使用する(useState、useEffect、useContext)
- ブラウザ専用APIを使用する
- クラスコンポーネントを使用する
データ取得戦略¶
Fetch API とキャッシング¶
// デフォルトでキャッシュされる (force-cacheと同等)
const data = await fetch('https://api.example.com/posts')
// 60秒ごとに再検証 (ISR)
const data = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }
})
// キャッシュしない (no-storeと同等)
const data = await fetch('https://api.example.com/posts', {
cache: 'no-store'
})
React.cache によるデータ重複排除¶
import { cache } from 'react'
export const getUser = cache(async (id: string) => {
const user = await fetch(`/api/users/${id}`)
return user.json()
})
// 異なるコンポーネントで複数回呼び出すことができる
// リクエストごとに1回だけ実行される
use() Hook による Promise のアンラップ¶
// Server Component
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
function Component() {
const promise = getData()
return <DataDisplay promise={promise} />
}
// 別のコンポーネント内
function DataDisplay({ promise }) {
const data = use(promise) // promiseをアンラップ
return <div>{data.title}</div>
}
レンダリングパターン¶
静的レンダリング (デフォルト)¶
ルートはビルド時、またはデータ再検証後にバックグラウンドでレンダリングされます。
// app/blog/page.tsx
export const dynamic = 'force-static' // オプション、これがデフォルト
export default async function BlogPage() {
const posts = await getPosts()
return <PostList posts={posts} />
}
動的レンダリング¶
ルートはリクエスト時にレンダリングされます。
// app/profile/page.tsx
export const dynamic = 'force-dynamic'
export default async function ProfilePage() {
const user = await getCurrentUser()
return <UserProfile user={user} />
}
Suspense を使用したストリーミング¶
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<LoadingSkeleton />}>
<UserStats />
</Suspense>
<Suspense fallback={<LoadingSkeleton />}>
<RecentActivity />
</Suspense>
</div>
)
}
// 各コンポーネントは独立してロードできる
async function UserStats() {
const stats = await fetchUserStats() // 遅い可能性がある
return <StatsDisplay stats={stats} />
}
Edge Runtime¶
Edge Runtime の有効化¶
// app/api/hello/route.ts
export const runtime = 'edge' // Edge Runtimeを有効化
export async function GET() {
return Response.json({ message: 'Hello from Edge!' })
}
Edge Runtime のメリット¶
- 低レイテンシ: ユーザーに近いCDNノードで実行
- グローバル分散: 自動的な地理的分散
- 軽量: Node.jsよりも小さいランタイム
- 高速なコールドスタート: 迅速な初期化
制限事項¶
- Node.js APIs (fs、cryptoなど)が使用不可
- npmパッケージの互換性が限定的
- 実行時間の制限が短い
ISR (Incremental Static Regeneration)¶
ページレベルの再検証¶
// app/posts/page.tsx
export const revalidate = 60 // 60秒ごとに再検証
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts')
return <PostList posts={posts} />
}
オンデマンド再検証¶
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const { path, tag } = await request.json()
if (path) {
revalidatePath(path)
}
if (tag) {
revalidateTag(tag)
}
return Response.json({ revalidated: true })
}
タグ付きキャッシング¶
// タグ付きfetch
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
// タグによる再検証
revalidateTag('posts')
パフォーマンス最適化¶
コード分割¶
// コード分割のための動的インポート
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'), {
loading: () => <LoadingSpinner />,
ssr: false // オプション: このコンポーネントのSSRを無効化
})
画像最適化¶
import Image from 'next/image'
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // ファーストビュー画像は即座にロード
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
)
}
フォント最適化¶
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
})
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
)
}
ベストプラクティス¶
1. データ取得¶
- 可能な限りServer Componentsでデータを取得する
- React.cacheを使用してリクエストを重複排除する
- Promise.allで並列データ取得を活用する
- ローディング状態にはSuspenseバウンダリを使用する
2. 状態管理¶
- 状態を最小化する: local → context → server state
- ミューテーションにはServer Actionsを使用する
- クライアント状態を最小限に保つ
- 共有可能なUI状態にはURL状態を使用する
3. キャッシング戦略¶
- ISRには明示的な
revalidate時間を設定する - 動的データには
no-storeを使用する - きめ細かい無効化のためにキャッシュにタグを付ける
- キャッシュヒット率を監視する
4. コード構成¶
- コンポーネントをルートと同じ場所に配置する
- Server ComponentsとClient Componentsを明確に分離する
- 型安全性のためにTypeScriptを使用する
- 一貫した命名規則に従う
5. パフォーマンス¶
- より速い知覚ロード時間のためにStreaming SSRを使用する
- 適切なローディング状態を実装する
- next/imageで画像を最適化する
- クライアントサイドのJavaScriptを最小化する
Pages Router からの移行¶
主な違い¶
| 機能 | Pages Router | App Router |
|---|---|---|
| ルーティング | ファイルベース (pages/) |
フォルダーベース (app/) |
| データ取得 | getServerSideProps, getStaticProps | Server Components, fetch |
| API Routes | pages/api/ |
app/api/route.ts |
| レイアウト | _app.tsx | layout.tsx (ネスト可能) |
| ローディング状態 | 手動実装 | loading.tsx |
| エラーハンドリング | _error.tsx | error.tsx (ネスト可能) |
移行手順¶
pages/と並行してapp/ディレクトリを作成する- ルートを段階的に移行する
- データ取得をServer Componentsに変換する
- API routesをRoute Handlersに置き換える
- importsと依存関係を更新する
- Pages Routerを削除する前に徹底的にテストする
用語集¶
| 用語 | 説明 |
|---|---|
| RSC | React Server Components - サーバー上でのみ実行されるコンポーネント |
| RCC | React Client Components - ブラウザで実行されるコンポーネント |
| Hydration | サーバーレンダリングされたHTMLにJavaScriptを追加するプロセス |
| Suspense | 非同期ローディング状態を処理するReactコンポーネント |
| Server Actions | クライアントから呼び出し可能な"use server"で宣言された関数 |
| Edge Runtime | CDNエッジノード上で実行される軽量JavaScriptランタイム |
| ISR | Incremental Static Regeneration - ビルド後に静的ページを更新 |
| Revalidate | キャッシュされたコンテンツを再生成する時間間隔またはトリガー |
| Route Handlers | app/api/route.tsファイルで定義されるAPIエンドポイント |
| React.cache | Server Componentsでデータ取得を重複排除する関数 |
| use() | コンポーネント内でpromiseをアンラップするReact hook |
まとめ¶
Next.js App Routerは、Reactアプリケーションの構築方法におけるパラダイムシフトを表しています。Server Componentsをデフォルトとして採用し、Streaming SSRを活用し、Server Actionsのような強力なプリミティブを提供することで、より高速で保守性が高く、優れたユーザーエクスペリエンスを持つアプリケーションを構築することができます。
成功の鍵は、サーバーコンポーネントとクライアントコンポーネントをいつ使用するかを理解し、キャッシング戦略を適切に実装し、最適なパフォーマンスを得るためにフレームワークの規約に従うことです。