Documentation Next.js

はじめに

Next.jsでの開発中、ビルドエラーに遭遇することは避けられません。この記事では、よくあるビルドエラーとその解決方法を、具体的なエラーメッセージと対処法のコード例を交えて解説します。

ビルドエラーの基本的な調査方法

まず、ビルドエラーを調査する際の基本的なアプローチを紹介します。

# 詳細なログを表示してビルド
npm run build -- --debug

# キャッシュをクリアしてビルド
rm -rf .next && npm run build

# 依存関係を再インストール
rm -rf node_modules package-lock.json && npm install

モジュールインポートエラー

Module not found エラー

最も頻繁に発生するエラーの一つです。

エラーメッセージ例:

Module not found: Can't resolve 'fs'

原因と解決方法:

Node.js専用モジュールをクライアントサイドでインポートしている場合に発生します。

// ❌ 悪い例:クライアントコンポーネントでfsを使用
'use client';
import fs from 'fs'; // エラー!

// ✅ 良い例:サーバーサイドでのみ使用
// app/api/files/route.ts
import fs from 'fs';
import { NextResponse } from 'next/server';

export async function GET() {
  const files = fs.readdirSync('./public');
  return NextResponse.json({ files });
}

dynamic importを使った解決方法:

// 条件付きでモジュールをインポート
export async function getServerSideProps() {
  // サーバーサイドでのみ実行される
  const fs = await import('fs');
  const content = fs.readFileSync('./data.json', 'utf-8');

  return {
    props: { data: JSON.parse(content) },
  };
}

パス解決エラー

エラーメッセージ例:

Module not found: Can't resolve '@/components/Button'

解決方法:

tsconfig.jsonでパスエイリアスを正しく設定します。

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/lib/*": ["./src/lib/*"]
    }
  }
}

動的ルーティングエラー

getStaticPaths関連エラー

エラーメッセージ例:

Error: getStaticPaths is required for dynamic SSG pages

解決方法:

動的ルートページには必ずgetStaticPathsを実装します。

// pages/posts/[slug].tsx または app/posts/[slug]/page.tsx

// Pages Router の場合
export async function getStaticPaths() {
  try {
    const res = await fetch('https://api.example.com/posts');

    if (!res.ok) {
      // APIエラー時は空のパスを返す
      return { paths: [], fallback: 'blocking' };
    }

    const posts = await res.json();

    const paths = posts.map((post: { slug: string }) => ({
      params: { slug: post.slug },
    }));

    return {
      paths,
      // fallback の選択
      // false: 未定義パスは404
      // true: 未定義パスはクライアントでフェッチ
      // 'blocking': 未定義パスはサーバーでレンダリング
      fallback: 'blocking',
    };
  } catch (error) {
    console.error('Failed to fetch posts:', error);
    return { paths: [], fallback: 'blocking' };
  }
}

export async function getStaticProps({ params }: { params: { slug: string } }) {
  try {
    const res = await fetch(`https://api.example.com/posts/${params.slug}`);

    if (!res.ok) {
      return { notFound: true };
    }

    const post = await res.json();

    return {
      props: { post },
      revalidate: 60, // ISRを有効化
    };
  } catch (error) {
    return { notFound: true };
  }
}
// App Router の場合
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }));
}

export default async function PostPage({
  params,
}: {
  params: { slug: string };
}) {
  const res = await fetch(`https://api.example.com/posts/${params.slug}`);

  if (!res.ok) {
    notFound();
  }

  const post = await res.json();

  return <article>{/* ... */}</article>;
}

型エラー(TypeScript)

Props型の不一致

エラーメッセージ例:

Type 'string | undefined' is not assignable to type 'string'

解決方法:

// ❌ 悪い例:型が不明確
interface Props {
  title: string;
}

function Component({ title }: Props) {
  return <h1>{title}</h1>;
}

// 使用時
<Component title={data?.title} /> // エラー!

// ✅ 良い例:オプショナルプロパティを使用
interface Props {
  title?: string;
}

function Component({ title = 'デフォルトタイトル' }: Props) {
  return <h1>{title}</h1>;
}

// または型ガードを使用
<Component title={data?.title ?? 'デフォルト'} />

サーバーコンポーネントでのクライアント専用APIの使用

エラーメッセージ例:

Error: useState only works in Client Components

解決方法:

// ❌ 悪い例:サーバーコンポーネントでuseStateを使用
// app/page.tsx
import { useState } from 'react';

export default function Page() {
  const [count, setCount] = useState(0); // エラー!
  return <div>{count}</div>;
}

// ✅ 良い例:クライアントコンポーネントに分離
// components/Counter.tsx
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

// app/page.tsx
import { Counter } from '@/components/Counter';

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

環境変数エラー

環境変数が読み込まれない

エラーメッセージ例:

Error: Missing required env var: DATABASE_URL

解決方法:

// lib/env.ts - 環境変数の検証ユーティリティ
function getEnvVar(key: string, required = true): string {
  const value = process.env[key];

  if (required && !value) {
    throw new Error(`Missing required environment variable: ${key}`);
  }

  return value || '';
}

// サーバーサイド専用の環境変数
export const serverEnv = {
  databaseUrl: getEnvVar('DATABASE_URL'),
  apiSecret: getEnvVar('API_SECRET'),
};

// クライアントでも使える環境変数(NEXT_PUBLIC_プレフィックス必須)
export const publicEnv = {
  apiUrl: getEnvVar('NEXT_PUBLIC_API_URL'),
  siteUrl: getEnvVar('NEXT_PUBLIC_SITE_URL'),
};
# .env.local
DATABASE_URL=postgresql://localhost:5432/mydb
API_SECRET=your-secret-key

# クライアントで使用する変数にはNEXT_PUBLIC_プレフィックスが必要
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_SITE_URL=https://example.com

APIルート関連エラー

CORSエラー

エラーメッセージ例:

Access to fetch at 'http://localhost:3000/api/data' from origin 'http://localhost:3001' has been blocked by CORS policy

解決方法:

// middleware.ts - グローバルCORS設定
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // プリフライトリクエストの処理
  if (request.method === 'OPTIONS') {
    return new NextResponse(null, {
      status: 200,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        'Access-Control-Max-Age': '86400',
      },
    });
  }

  const response = NextResponse.next();

  // CORSヘッダーを追加
  response.headers.set('Access-Control-Allow-Origin', '*');
  response.headers.set(
    'Access-Control-Allow-Methods',
    'GET, POST, PUT, DELETE, OPTIONS'
  );

  return response;
}

export const config = {
  matcher: '/api/:path*',
};
// app/api/data/route.ts - 個別のAPIルートでのCORS設定
import { NextResponse } from 'next/server';

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type',
};

export async function OPTIONS() {
  return NextResponse.json({}, { headers: corsHeaders });
}

export async function GET() {
  const data = { message: 'Hello World' };

  return NextResponse.json(data, { headers: corsHeaders });
}

リクエストボディのパースエラー

エラーメッセージ例:

SyntaxError: Unexpected token < in JSON at position 0

解決方法:

// app/api/submit/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  try {
    // Content-Typeヘッダーを確認
    const contentType = request.headers.get('content-type');

    if (!contentType?.includes('application/json')) {
      return NextResponse.json(
        { error: 'Content-Type must be application/json' },
        { status: 400 }
      );
    }

    // ボディをパース
    const body = await request.json();

    // バリデーション
    if (!body.name || !body.email) {
      return NextResponse.json(
        { error: 'name and email are required' },
        { status: 400 }
      );
    }

    // 処理...
    return NextResponse.json({ success: true });
  } catch (error) {
    if (error instanceof SyntaxError) {
      return NextResponse.json(
        { error: 'Invalid JSON in request body' },
        { status: 400 }
      );
    }

    console.error('API error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

ビルド時のメモリ不足

エラーメッセージ例:

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

解決方法:

// package.json
{
  "scripts": {
    "build": "NODE_OPTIONS='--max-old-space-size=4096' next build",
    "build:analyze": "ANALYZE=true next build"
  }
}
// next.config.js - バンドルサイズの分析
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // 大きなページの分割
  experimental: {
    largePageDataBytes: 128 * 1024, // 128KB
  },
});

画像最適化エラー

エラーメッセージ例:

Error: Invalid src prop on `next/image`, hostname "example.com" is not configured under images in your `next.config.js`

解決方法:

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
        pathname: '/images/**',
      },
      {
        protocol: 'https',
        hostname: '**.cloudinary.com',
      },
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
    ],
    // 古い設定方法(非推奨だが互換性のため)
    // domains: ['example.com', 'images.unsplash.com'],
  },
};

ビルドキャッシュの問題

ビルドキャッシュが原因で古いエラーが残ることがあります。

# キャッシュを完全にクリア
rm -rf .next
rm -rf node_modules/.cache

# Turbopackのキャッシュもクリア
rm -rf .turbo

# 全てクリアして再ビルド
npm run clean && npm install && npm run build
// package.json
{
  "scripts": {
    "clean": "rm -rf .next node_modules/.cache .turbo",
    "build:fresh": "npm run clean && npm run build"
  }
}

デバッグのベストプラクティス

エラーログの詳細化

// lib/logger.ts
export function logBuildError(error: unknown, context: string) {
  console.error(`
=====================================
Build Error in: ${context}
Time: ${new Date().toISOString()}
=====================================
`);

  if (error instanceof Error) {
    console.error('Message:', error.message);
    console.error('Stack:', error.stack);
  } else {
    console.error('Error:', error);
  }
}

// 使用例
export async function getStaticProps() {
  try {
    const data = await fetchData();
    return { props: { data } };
  } catch (error) {
    logBuildError(error, 'getStaticProps in pages/index.tsx');
    return { props: { data: null } };
  }
}

まとめ

Next.jsのビルドエラーを解決するためのポイントをまとめます。

  • エラーメッセージを正確に読む: エラーの原因となるファイルと行番号を確認
  • キャッシュをクリア: 不明なエラーはまずキャッシュクリアを試す
  • 段階的にデバッグ: 問題のあるコードを特定するため、コメントアウトで切り分け
  • 型チェックを活用: TypeScriptの型エラーはビルド前に解決
  • 環境変数を確認: 本番とローカルで異なる設定がないか確認

参考文献

円