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のgetServerSidePropsやgetStaticPropsは廃止。代わりに、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>;
}
移行時の注意点
- 既存のライブラリ: Client Componentでしか動かないものがある(react-chartjsなど)
- 状態管理: Zustand/Reduxの使い方が変わる可能性
- 学習コスト: チーム全員がServer Componentsを理解する必要
まとめ
Next.js 15 App Routerは、最初は戸惑うが、慣れるとより直感的でパフォーマンスの高いコードが書ける。
特にSEO記事生成システムのような「サーバー側で処理が完結する部分が多いアプリ」には最適。
関連記事:
– TypeScript strict mode で型安全性を高めた
– Supabase + RLS でユーザー管理を実装した
この記事は「中野のAI開発部屋」で公開中の開発実績シリーズです。