はじめに
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の型エラーはビルド前に解決
- 環境変数を確認: 本番とローカルで異なる設定がないか確認