Documentation Next.js

はじめに

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アプリケーションでよく発生するエラーの原因となります。特にuseEffectuseStateの使用時に、依存関係が正しく指定されていないと、無限ループや予期せぬ動作が発生することがあります。

無限ループの防止

// 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ルートの問題など、様々なケースに対して適切な対応を行うことで、ユーザー体験を向上させることができます。定期的なログ監視やエラートラッキングツールを使用して、リアルタイムでエラーを把握し、迅速に解決することが推奨されます。

参考文献

円