Next.js 15 App Router への移行で学んだこと

Pages Router から App Router へ

Next.js 15のApp Routerは、従来のPages Routerと比べて大きく設計思想が変わった。単なる「新しいルーティング方式」ではなく、React Server Componentsを前提とした新しいアーキテクチャだ。

求人AIシステム(job-posting-brushup-tool)をPages RouterからApp Routerに移行した際の学びをまとめる。

移行の主な変更点

1. ファイル構成の変化

Pages Router:

pages/
├── index.tsx         # /
├── dashboard.tsx     # /dashboard
└── api/
    └── analyze.ts    # /api/analyze

App Router:

app/
├── page.tsx          # /
├── layout.tsx        # 共通レイアウト
├── dashboard/
│   └── page.tsx      # /dashboard
└── api/
    └── analyze/
        └── route.ts  # /api/analyze

page.tsxがルートの実体、layout.tsxが共通レイアウト。この分離により、レイアウトの再レンダリングが最小化される。

2. Server Components がデフォルト

App Routerでは、すべてのコンポーネントがServer Componentとして扱われる。クライアント側で動かしたいコンポーネントには'use client'が必要。

移行前(Pages Router):

// pages/dashboard.tsx
export default function Dashboard() {
  const [data, setData] = useState(null);
  // ...
}

移行後(App Router):

// app/dashboard/page.tsx
'use client';  // これがないとuseStateがエラーになる

export default function Dashboard() {
  const [data, setData] = useState(null);
  // ...
}

3. データフェッチングの変更

Pages RouterのgetServerSidePropsgetStaticPropsは廃止。代わりに、Server Componentで直接async/awaitを使う。

移行前:

export async function getServerSideProps() {
  const data = await fetchData();
  return { props: { data } };
}

export default function Page({ data }) {
  return <div>{data}</div>;
}

移行後:

async function fetchData() {
  const res = await fetch('...');
  return res.json();
}

export default async function Page() {
  const data = await fetchData();
  return <div>{data}</div>;
}

コンポーネント自体がasyncになる。これにより、データフェッチングとレンダリングが統合され、コードがシンプルになった。

実装で詰まったポイント

1. 'use client'の配置

最初、ルート全体に'use client'を付けてしまった。これではServer Componentsの恩恵がゼロ

改善後:
– データフェッチング部分: Server Component('use client'なし)
– インタラクティブなUI部分: Client Component('use client'あり)

// app/dashboard/page.tsx (Server Component)
import ClientDashboard from './ClientDashboard';

async function getData() {
  const res = await fetch('...');
  return res.json();
}

export default async function Page() {
  const data = await getData();
  return <ClientDashboard initialData={data} />;
}

// app/dashboard/ClientDashboard.tsx (Client Component)
'use client';

export default function ClientDashboard({ initialData }) {
  const [data, setData] = useState(initialData);
  // インタラクティブな処理
}

2. API Routesの変更

Pages Routerのpages/api/analyze.tsは、App Routerではapp/api/analyze/route.tsになる。

移行前:

// pages/api/analyze.ts
export default async function handler(req, res) {
  if (req.method === 'POST') {
    const result = await analyze(req.body);
    res.status(200).json(result);
  }
}

移行後:

// app/api/analyze/route.ts
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const body = await request.json();
  const result = await analyze(body);
  return NextResponse.json(result);
}

NextResponseを使った新しいAPI設計。req.methodでの分岐が不要になり、HTTPメソッドごとに関数を分ける。

3. キャッシュの挙動

App Routerは積極的にキャッシュする。開発中、「コード変更したのに反映されない」が頻発。

解決策:

// キャッシュを無効化
export const revalidate = 0;

// または個別のfetchで制御
fetch('...', { cache: 'no-store' })

移行して良かったこと

1. パフォーマンス向上

Server Componentsにより、クライアント側のJavaScriptバンドルサイズが約40%削減。初期ロードが速くなった。

2. コードの整理

データフェッチングとレンダリングが統合され、getServerSidePropsの煩雑さが消えた。

3. Streaming SSR

loading.tsxを使った段階的レンダリングで、ユーザー体験が向上。

// app/dashboard/loading.tsx
export default function Loading() {
  return <div>読み込み中...</div>;
}

移行時の注意点

  1. 既存のライブラリ: Client Componentでしか動かないものがある(react-chartjsなど)
  2. 状態管理: Zustand/Reduxの使い方が変わる可能性
  3. 学習コスト: チーム全員がServer Componentsを理解する必要

まとめ

Next.js 15 App Routerは、最初は戸惑うが、慣れるとより直感的でパフォーマンスの高いコードが書ける。

特にSEO記事生成システムのような「サーバー側で処理が完結する部分が多いアプリ」には最適。


関連記事:
– TypeScript strict mode で型安全性を高めた
– Supabase + RLS でユーザー管理を実装した


この記事は「中野のAI開発部屋」で公開中の開発実績シリーズです。