コンテンツにスキップ

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 (ネスト可能)

移行手順

  1. pages/と並行してapp/ディレクトリを作成する
  2. ルートを段階的に移行する
  3. データ取得をServer Componentsに変換する
  4. API routesをRoute Handlersに置き換える
  5. importsと依存関係を更新する
  6. 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のような強力なプリミティブを提供することで、より高速で保守性が高く、優れたユーザーエクスペリエンスを持つアプリケーションを構築することができます。

成功の鍵は、サーバーコンポーネントとクライアントコンポーネントをいつ使用するかを理解し、キャッシング戦略を適切に実装し、最適なパフォーマンスを得るためにフレームワークの規約に従うことです。


参考資料