はじめに VSCodeでのデバッグ設定 launch.json設定 package.jsonのデバッグスクリプト tasks.json設定 Server Componentsのデバッグ console.logでのデバッグ Server Actionsのデバッグ データフェッチのデバッグ Client Componentsのデバッグ React DevToolsの活用 useEffectのデバッグ エラーハンドリングのデバッグ error.tsxでのエラーキャッチ global-error.tsxでのルートエラー ハイドレーションエラーのデバッグ 原因の特定 suppressHydrationWarningの使用 ネットワークリクエストのデバッグ カスタムロガー APIリクエストのトレース パフォーマンスデバッグ React Profilerの使用 Server Timingの計測 よくある問題と解決策 デバッグユーティリティ まとめ 参考文献 はじめに
Next.js App Routerでは、Server ComponentsとClient Componentsの両方をデバッグする必要があります。この記事では、VSCode、Chrome DevTools、React DevToolsを使った効果的なデバッグ方法を解説します。
VSCodeでのデバッグ設定
launch.json設定
// .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" : "- Local:.+(https?://.+)" ,
"uriFormat" : "%s" ,
"action" : "debugWithChrome"
}
},
{
"name" : "Next.js: debug server-side (attach)" ,
"type" : "node" ,
"request" : "attach" ,
"port" : 9229 ,
"skipFiles" : [ "<node_internals>/**" ]
}
]
}
package.jsonのデバッグスクリプト
{
"scripts" : {
"dev" : "next dev" ,
"dev:debug" : "NODE_OPTIONS='--inspect' next dev" ,
"dev:turbo" : "next dev --turbo" ,
"dev:turbo:debug" : "NODE_OPTIONS='--inspect' next dev --turbo"
}
}
tasks.json設定
// .vscode/tasks.json
{
"version" : "2.0.0" ,
"tasks" : [
{
"label" : "Next.js: build" ,
"type" : "npm" ,
"script" : "build" ,
"problemMatcher" : [ "$tsc" ],
"group" : "build"
},
{
"label" : "Next.js: lint" ,
"type" : "npm" ,
"script" : "lint" ,
"problemMatcher" : [ "$eslint-stylish" ],
"group" : "test"
}
]
}
Server Componentsのデバッグ
console.logでのデバッグ
// app/users/page.tsx
async function getUsers () {
console. log ( '[Server] Fetching users...' );
const res = await fetch ( 'https://api.example.com/users' , {
next: { revalidate: 60 },
});
console. log ( '[Server] Response status:' , res.status);
const data = await res. json ();
console. log ( '[Server] Users count:' , data. length );
return data;
}
export default async function UsersPage () {
const users = await getUsers ();
// サーバーサイドのログはターミナルに出力される
console. log ( '[Server] Rendering UsersPage' );
return (
< div >
< h1 >Users</ h1 >
{ /* ... */ }
</ div >
);
}
Server Actionsのデバッグ
// app/actions.ts
'use server' ;
export async function createUser ( formData : FormData ) {
console. log ( '[Action] createUser called' );
console. log ( '[Action] FormData:' , Object. fromEntries (formData));
try {
const name = formData. get ( 'name' ) as string ;
const email = formData. get ( 'email' ) as string ;
console. log ( '[Action] Parsed data:' , { name, email });
// ブレークポイントをここに設定可能
debugger ;
const result = await db.user. create ({
data: { name, email },
});
console. log ( '[Action] Created user:' , result);
return { success: true , user: result };
} catch (error) {
console. error ( '[Action] Error:' , error);
throw error;
}
}
データフェッチのデバッグ
// lib/debug-fetch.ts
export async function debugFetch (
url : string ,
options ?: RequestInit
) : Promise < Response > {
const startTime = performance. now ();
console. log ( `[Fetch] ${ options ?. method || 'GET'} ${ url }` );
if (options?.body) {
console. log ( '[Fetch] Body:' , options.body);
}
try {
const response = await fetch (url, options);
const duration = performance. now () - startTime;
console. log ( `[Fetch] ${ response . status } ${ response . statusText } (${ duration . toFixed ( 2 ) }ms)` );
// レスポンスヘッダーのログ
console. log ( '[Fetch] Headers:' , Object. fromEntries (response.headers));
return response;
} catch (error) {
const duration = performance. now () - startTime;
console. error ( `[Fetch] Error after ${ duration . toFixed ( 2 ) }ms:` , error);
throw error;
}
}
Client Componentsのデバッグ
// components/DebugComponent.tsx
'use client' ;
import { useState, useEffect, useDebugValue } from 'react' ;
// カスタムフックのデバッグ
function useDebugState < T >( initialValue : T , label : string ) {
const [ value , setValue ] = useState (initialValue);
// React DevToolsで表示されるデバッグ値
useDebugValue ( `${ label }: ${ JSON . stringify ( value ) }` );
useEffect (() => {
console. log ( `[${ label }] State changed:` , value);
}, [value, label]);
return [value, setValue] as const ;
}
export function DebugComponent () {
const [ count , setCount ] = useDebugState ( 0 , 'Counter' );
const [ items , setItems ] = useDebugState < string []>([], 'Items' );
return (
< div >
< p >Count: { count } </ p >
< button onClick ={ () => setCount (count + 1 ) } >Increment</ button >
</ div >
);
}
useEffectのデバッグ
'use client' ;
import { useEffect, useRef } from 'react' ;
function useEffectDebugger (
effect : React . EffectCallback ,
deps : React . DependencyList ,
name : string
) {
const previousDeps = useRef < React . DependencyList >();
useEffect (() => {
if (previousDeps.current) {
const changedDeps = deps. reduce < Record < number , { from : unknown ; to : unknown }>>(
( acc , dep , index ) => {
if (dep !== previousDeps.current?.[index]) {
acc[index] = {
from: previousDeps.current?.[index],
to: dep,
};
}
return acc;
},
{}
);
if (Object. keys (changedDeps). length > 0 ) {
console. log ( `[${ name }] Dependencies changed:` , changedDeps);
}
} else {
console. log ( `[${ name }] Initial render` );
}
previousDeps.current = deps;
return effect ();
}, deps);
}
// 使用例
function MyComponent ({ userId } : { userId : string }) {
const [ user , setUser ] = useState ( null );
useEffectDebugger (
() => {
fetchUser (userId). then (setUser);
},
[userId],
'FetchUser'
);
return < div > { /* ... */ } </ div >;
}
エラーハンドリングのデバッグ
error.tsxでのエラーキャッチ
// app/error.tsx
'use client' ;
import { useEffect } from 'react' ;
export default function Error ({
error,
reset,
} : {
error : Error & { digest ?: string };
reset : () => void ;
}) {
useEffect (() => {
// エラーログサービスに送信
console. error ( '[Error Boundary]' , {
message: error.message,
stack: error.stack,
digest: error.digest,
});
}, [error]);
return (
< div className = "p-4 bg-red-50 border border-red-200 rounded" >
< h2 className = "text-red-800 font-bold" >エラーが発生しました</ h2 >
{ process.env. NODE_ENV === 'development' && (
< details className = "mt-2" >
< summary className = "cursor-pointer text-red-600" >
エラー詳細
</ summary >
< pre className = "mt-2 p-2 bg-red-100 rounded text-sm overflow-auto" >
{ error.stack }
</ pre >
</ details >
) }
< button
onClick ={ reset }
className = "mt-4 px-4 py-2 bg-red-600 text-white rounded"
>
再試行
</ 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 = "min-h-screen flex items-center justify-center" >
< div className = "text-center" >
< h2 className = "text-2xl font-bold text-red-600" >
重大なエラーが発生しました
</ h2 >
< p className = "mt-2 text-gray-600" >
ページを再読み込みしてください
</ p >
< button
onClick ={ reset }
className = "mt-4 px-4 py-2 bg-blue-600 text-white rounded"
>
再試行
</ button >
</ div >
</ div >
</ body >
</ html >
);
}
ハイドレーションエラーのデバッグ
原因の特定
// components/HydrationDebug.tsx
'use client' ;
import { useEffect, useState } from 'react' ;
// ハイドレーション不一致の検出
export function HydrationDebug ({ children } : { children : React . ReactNode }) {
const [ isClient , setIsClient ] = useState ( false );
useEffect (() => {
setIsClient ( true );
}, []);
if (process.env. NODE_ENV === 'development' ) {
console. log ( '[Hydration] isClient:' , isClient);
}
return <> { children } </>;
}
// 問題のあるコード例
function ProblematicComponent () {
// ❌ サーバーとクライアントで異なる値
const now = new Date (). toLocaleString ();
return < p >現在時刻: { now } </ p >;
}
// 修正版
function FixedComponent () {
const [ now , setNow ] = useState < string >( '' );
useEffect (() => {
setNow ( new Date (). toLocaleString ());
}, []);
return < p suppressHydrationWarning >現在時刻: { now || '読み込み中...' } </ p >;
}
suppressHydrationWarningの使用
// components/ClientDate.tsx
'use client' ;
import { useState, useEffect } from 'react' ;
export function ClientDate ({ date } : { date : string }) {
const [ formatted , setFormatted ] = useState (date);
useEffect (() => {
setFormatted ( new Date (date). toLocaleDateString ( 'ja-JP' ));
}, [date]);
return (
< time dateTime ={ date } suppressHydrationWarning >
{ formatted }
</ time >
);
}
ネットワークリクエストのデバッグ
カスタムロガー
// lib/logger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error' ;
const LOG_COLORS = {
debug: ' \x1b [36m' , // Cyan
info: ' \x1b [32m' , // Green
warn: ' \x1b [33m' , // Yellow
error: ' \x1b [31m' , // Red
reset: ' \x1b [0m' ,
};
class Logger {
private prefix : string ;
constructor ( prefix : string ) {
this .prefix = prefix;
}
private log ( level : LogLevel , ... args : unknown []) {
if (process.env. NODE_ENV === 'production' && level === 'debug' ) {
return ;
}
const color = LOG_COLORS [level];
const reset = LOG_COLORS .reset;
const timestamp = new Date (). toISOString ();
console[level](
`${ color }[${ timestamp }] [${ this . prefix }] [${ level . toUpperCase () }]${ reset }` ,
... args
);
}
debug ( ... args : unknown []) {
this . log ( 'debug' , ... args);
}
info ( ... args : unknown []) {
this . log ( 'info' , ... args);
}
warn ( ... args : unknown []) {
this . log ( 'warn' , ... args);
}
error ( ... args : unknown []) {
this . log ( 'error' , ... args);
}
}
export const logger = new Logger ( 'App' );
export const apiLogger = new Logger ( 'API' );
export const dbLogger = new Logger ( 'DB' );
APIリクエストのトレース
// lib/api-client.ts
import { apiLogger } from './logger' ;
export async function apiClient < T >(
endpoint : string ,
options ?: RequestInit
) : Promise < T > {
const requestId = Math. random (). toString ( 36 ). substring ( 7 );
const url = `${ process . env . API_URL }${ endpoint }` ;
apiLogger. debug ( `[${ requestId }] Request: ${ options ?. method || 'GET'} ${ url }` );
const startTime = performance. now ();
try {
const response = await fetch (url, {
... options,
headers: {
'Content-Type' : 'application/json' ,
'X-Request-ID' : requestId,
... options?.headers,
},
});
const duration = performance. now () - startTime;
apiLogger. info (
`[${ requestId }] Response: ${ response . status } (${ duration . toFixed ( 2 ) }ms)`
);
if ( ! response.ok) {
const error = await response. text ();
apiLogger. error ( `[${ requestId }] Error response:` , error);
throw new Error ( `API Error: ${ response . status }` );
}
const data = await response. json ();
apiLogger. debug ( `[${ requestId }] Response data:` , data);
return data;
} catch (error) {
const duration = performance. now () - startTime;
apiLogger. error ( `[${ requestId }] Failed after ${ duration . toFixed ( 2 ) }ms:` , error);
throw error;
}
}
パフォーマンスデバッグ
React Profilerの使用
// components/ProfiledComponent.tsx
'use client' ;
import { Profiler, ProfilerOnRenderCallback } from 'react' ;
const onRenderCallback : ProfilerOnRenderCallback = (
id ,
phase ,
actualDuration ,
baseDuration ,
startTime ,
commitTime
) => {
console. log ( `[Profiler] ${ id }:` , {
phase,
actualDuration: `${ actualDuration . toFixed ( 2 ) }ms` ,
baseDuration: `${ baseDuration . toFixed ( 2 ) }ms` ,
startTime,
commitTime,
});
};
export function ProfiledComponent ({ children } : { children : React . ReactNode }) {
if (process.env. NODE_ENV === 'production' ) {
return <> { children } </>;
}
return (
< Profiler id = "App" onRender ={ onRenderCallback } >
{ children }
</ Profiler >
);
}
Server Timingの計測
// middleware.ts
import { NextResponse } from 'next/server' ;
import type { NextRequest } from 'next/server' ;
export function middleware ( request : NextRequest ) {
const startTime = Date. now ();
const response = NextResponse. next ();
// Server-Timingヘッダーを追加
const duration = Date. now () - startTime;
response.headers. set ( 'Server-Timing' , `middleware;dur=${ duration }` );
return response;
}
よくある問題と解決策
問題 原因 解決策 ハイドレーションエラー サーバー/クライアントの不一致 useEffect、suppressHydrationWarning Server Actionsが動かない ’use server’の欠落 ファイル先頭に追加 環境変数が未定義 NEXT_PUBLIC_の欠落 クライアント用は接頭辞追加 キャッシュが更新されない revalidateの欠落 revalidatePath/revalidateTag ブレークポイントが効かない ソースマップの問題 —inspect オプション確認
デバッグユーティリティ
// lib/debug-utils.ts
export function inspect < T >( value : T , label ?: string ) : T {
if (process.env. NODE_ENV === 'development' ) {
console. log (label || 'Inspect:' , value);
}
return value;
}
export function time < T >( fn : () => T , label : string ) : T {
const start = performance. now ();
const result = fn ();
const duration = performance. now () - start;
console. log ( `[Timer] ${ label }: ${ duration . toFixed ( 2 ) }ms` );
return result;
}
export async function timeAsync < T >(
fn : () => Promise < T >,
label : string
) : Promise < T > {
const start = performance. now ();
const result = await fn ();
const duration = performance. now () - start;
console. log ( `[Timer] ${ label }: ${ duration . toFixed ( 2 ) }ms` );
return result;
}
// 使用例
const users = inspect ( await getUsers (), 'Fetched users' );
const data = await timeAsync (() => fetchData (), 'Data fetch' );
まとめ
デバッグ対象 ツール 方法 Server Components VSCode + Node.js —inspect、console.log Client Components Chrome DevTools React DevTools、ブレークポイント Server Actions VSCode debugger文、ログ出力 ハイドレーション React DevTools エラーメッセージ確認 パフォーマンス Profiler React Profiler、Server-Timing
参考文献