はじめに
Next.jsアプリケーションの開発・運用中には、データ取得エラー、ビルドエラー、APIルートのエラーなど、様々な問題が発生することがあります。本記事では、これらの一般的なエラーの原因を理解し、迅速に解決するためのトラブルシューティング手法を解説します。エラーハンドリング(プログラムの実行中に発生するエラーを検知し、適切に処理する仕組み)の実装パターンを学ぶことで、より堅牢なアプリケーションを構築できます。
よくあるエラーと解決方法一覧
| エラー | 原因 | 解決方法 |
|---|---|---|
Hydration failed | サーバーとクライアントのHTML不一致 | useEffectで動的コンテンツを制御 |
getStaticPaths is required | 動的ルートでpathsが未定義 | getStaticPaths関数を実装 |
CORS error | オリジン間リソース共有の設定不備 | APIルートでCORSヘッダーを設定 |
Module not found | パッケージ未インストールまたはパス誤り | npm installまたは相対パスを確認 |
Build optimization failed | メモリ不足またはバンドルサイズ超過 | analyzeで原因特定、動的importで分割 |
App Routerでのエラーハンドリング
App Routerでは、error.tsxファイルを使用してエラーをキャッチし、ユーザーにフレンドリーなエラー画面を表示できます。
// app/error.tsx
'use client';
import { useEffect } from 'react';
interface Props {
error: Error & { digest?: string };
reset: () => void;
}
export default function Error({ error, reset }: Props) {
useEffect(() => {
// エラーをログサービスに送信
console.error('エラーが発生しました:', error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">問題が発生しました</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
もう一度試す
</button>
</div>
);
}
グローバルエラーハンドリング
ルートレイアウトでのエラーをキャッチするには、global-error.tsxを使用します。
// app/global-error.tsx
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">
アプリケーションエラー
</h2>
<p className="text-gray-600 mb-4">
予期しないエラーが発生しました
</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
リロード
</button>
</div>
</body>
</html>
);
}
404エラーページ
存在しないページへのアクセスにはnot-found.tsxで対応します。
// app/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-4xl font-bold mb-4">404</h2>
<p className="text-gray-600 mb-4">
お探しのページが見つかりませんでした
</p>
<Link
href="/"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
ホームに戻る
</Link>
</div>
);
}
プログラムから404を発生させるにはnotFound()関数を使用します。
// app/posts/[slug]/page.tsx
import { notFound } from 'next/navigation';
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`);
if (!res.ok) return null;
return res.json();
}
export default async function PostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
if (!post) {
notFound(); // 404ページを表示
}
return <article>{post.content}</article>;
}
Hydrationエラーの対処
Hydrationエラーは、サーバーでレンダリングされたHTMLとクライアントでレンダリングされたHTMLが一致しない場合に発生します。
よくある原因と対策
// NG: サーバーとクライアントで異なる値
function BadComponent() {
return <p>現在時刻: {new Date().toLocaleString()}</p>;
}
// OK: クライアントでのみレンダリング
'use client';
import { useState, useEffect } from 'react';
function GoodComponent() {
const [currentTime, setCurrentTime] = useState<string>('');
useEffect(() => {
setCurrentTime(new Date().toLocaleString());
}, []);
return <p>現在時刻: {currentTime || 'Loading...'}</p>;
}
suppressHydrationWarningの使用
どうしても警告を抑制したい場合(日時表示など)は、suppressHydrationWarningを使用できます。
<time suppressHydrationWarning>
{new Date().toLocaleDateString()}
</time>
データ取得エラーの対処方法
Server Componentsでのエラーハンドリング
App RouterのServer Componentsでは、try-catchでエラーをハンドリングします。
// app/posts/page.tsx
async function getPosts() {
try {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }, // 60秒キャッシュ
});
if (!res.ok) {
throw new Error(`Failed to fetch: ${res.status}`);
}
return res.json();
} catch (error) {
console.error('データ取得エラー:', error);
return null;
}
}
export default async function PostsPage() {
const posts = await getPosts();
if (!posts) {
return (
<div className="p-4 bg-red-100 text-red-700 rounded">
データの取得に失敗しました。後ほど再度お試しください。
</div>
);
}
return (
<ul>
{posts.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Pages Routerでのエラー(getStaticPaths)
動的ルーティングでgetStaticPathsを使う際のエラーと対策です。
-
エラー例:
getStaticPaths is required for dynamic SSG pagesが発生する。対策:
getStaticPaths関数を追加して、動的ルーティングに必要なデータを返すようにします。
export async function getStaticPaths() {
return {
paths: [{ params: { slug: 'example' } }],
fallback: false
};
}
また、データ取得エラーのデバッグには、console.logやロギングツールを使い、エラーメッセージやスタックトレースを記録することが有効です。
ビルドエラーの対処
モジュール解決エラー
# エラー例
Module not found: Can't resolve 'package-name'
対策として、パッケージがインストールされているか確認します。
npm install package-name
# または
npm install --legacy-peer-deps
バンドルサイズの分析
ビルドが遅い、またはメモリ不足の場合は、バンドルを分析します。
npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// 他の設定
});
ANALYZE=true npm run build
動的インポートによる最適化
大きなライブラリは動的インポートで分割します。
import dynamic from 'next/dynamic';
// 重いコンポーネントを動的に読み込み
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <p>チャートを読み込み中...</p>,
ssr: false, // クライアントのみで実行
});
APIルートのエラーハンドリング
App RouterのRoute Handlersでのエラーハンドリングパターンです。
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json(
{ error: 'IDは必須です' },
{ status: 400 }
);
}
const data = await fetchData(id);
return NextResponse.json(data);
} catch (error) {
console.error('APIエラー:', error);
return NextResponse.json(
{ error: 'サーバーエラーが発生しました' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// バリデーション
if (!body.title || !body.content) {
return NextResponse.json(
{ error: 'タイトルと本文は必須です' },
{ status: 400 }
);
}
const result = await createPost(body);
return NextResponse.json(result, { status: 201 });
} catch (error) {
if (error instanceof SyntaxError) {
return NextResponse.json(
{ error: 'JSONの形式が不正です' },
{ status: 400 }
);
}
console.error('POST APIエラー:', error);
return NextResponse.json(
{ error: 'サーバーエラーが発生しました' },
{ status: 500 }
);
}
}
CORSの設定
外部からのAPIアクセスを許可する場合は、CORSヘッダーを設定します。
// app/api/public/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const data = { message: 'Hello' };
return NextResponse.json(data, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}
// OPTIONSリクエストへの対応(プリフライト)
export async function OPTIONS() {
return new NextResponse(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}
デバッグの設定
VS Codeでのデバッグ
.vscode/launch.jsonを作成してブレークポイントを使ったデバッグを有効にします。
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
"serverReadyAction": {
"pattern": "started server on .+, url: (https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
}
}
]
}
ソースマップの有効化
本番環境でのデバッグ用にソースマップを生成します。
// next.config.js
module.exports = {
productionBrowserSourceMaps: true,
};
本番環境のモニタリング
Sentryによるエラートラッキング
npm install @sentry/nextjs
npx @sentry/wizard@latest -i nextjs
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
カスタムログユーティリティ
// lib/logger.ts
type LogLevel = 'info' | 'warn' | 'error';
interface LogEntry {
level: LogLevel;
message: string;
timestamp: string;
context?: Record<string, unknown>;
}
export function log(
level: LogLevel,
message: string,
context?: Record<string, unknown>
): void {
const entry: LogEntry = {
level,
message,
timestamp: new Date().toISOString(),
context,
};
if (process.env.NODE_ENV === 'development') {
console[level](JSON.stringify(entry, null, 2));
} else {
// 本番環境では外部サービスに送信
console[level](JSON.stringify(entry));
}
}
// 使用例
log('error', 'データベース接続エラー', {
userId: 123,
operation: 'fetchUser',
});
Reactフックのエラーハンドリング
Reactフックの誤った使用も、Next.jsアプリケーションでよく発生するエラーの原因となります。特にuseEffectやuseStateの使用時に、依存関係が正しく指定されていないと、無限ループや予期せぬ動作が発生することがあります。
無限ループの防止
// NG: 無限ループが発生
function BadComponent() {
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}); // 依存配列がない
return <div>{data.length}</div>;
}
// OK: 初回のみ実行
function GoodComponent() {
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []); // 空の依存配列
return <div>{data.length}</div>;
}
非同期処理のクリーンアップ
function AsyncComponent({ id }: { id: string }) {
const [data, setData] = useState(null);
useEffect(() => {
let isMounted = true;
async function fetchData() {
try {
const res = await fetch(`/api/items/${id}`);
const json = await res.json();
// コンポーネントがアンマウントされていたら更新しない
if (isMounted) {
setData(json);
}
} catch (error) {
if (isMounted) {
console.error('Fetch error:', error);
}
}
}
fetchData();
return () => {
isMounted = false;
};
}, [id]);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
まとめ
Next.jsアプリケーションの障害対応やトラブルシューティングには、効果的なエラーハンドリングが不可欠です。サーバーサイドのエラーやデータ取得の失敗、APIルートの問題など、様々なケースに対して適切な対応を行うことで、ユーザー体験を向上させることができます。定期的なログ監視やエラートラッキングツールを使用して、リアルタイムでエラーを把握し、迅速に解決することが推奨されます。