デバッグテクニック – 開発者ツールを使いこなす

「console.logだけのデバッグから卒業したい…」

「非同期処理のデバッグってどうすればいい?」

「パフォーマンスの問題箇所を特定するには?」

前回はJestを使用したテスト手法について学びました。今回は、開発者ツールを活用した効率的なデバッグ手法を解説します。

基本的なデバッグ手法

console系メソッドの使い分け

JS
// 基本的なログ
console.log('通常のログ');
console.info('情報');
console.warn('警告');
console.error('エラー');

// オブジェクトの詳細表示
const user = { 
    name: 'John',
    age: 30,
    address: { city: 'Tokyo' }
};
console.log('単純な出力:', user);
console.dir(user);  // プロパティをツリー表示

// テーブル形式での出力
const users = [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
];
console.table(users);  // 表形式で表示

// 処理時間の計測
console.time('処理時間');
// 重い処理
console.timeEnd('処理時間');

使い分けのポイント:

  • console.log: 一般的なデバッグ情報
  • console.dir: オブジェクトの階層構造の確認
  • console.table: 配列やオブジェクトの一覧性の高い表示
  • console.time/timeEnd: パフォーマンス計測

ブレークポイントの活用

JS
class UserManager {
    async fetchUsers() {
        try {
            // ここにブレークポイントを設定
            const response = await fetch('/api/users');
            const users = await response.json();
            
            // 条件付きブレークポイント
            users.forEach(user => {
                if (user.id === 5) {
                    debugger;  // 特定の条件で停止
                }
                this.processUser(user);
            });
            
            return users;
        } catch (error) {
            console.error('Failed to fetch users:', error);
            throw error;
        }
    }
}

ブレークポイントの種類:

  • 通常のブレークポイント:特定の行で停止
  • 条件付きブレークポイント:条件が真の時のみ停止
  • XHRブレークポイント:特定のAPIリクエストで停止
  • イベントリスナーブレークポイント:特定のイベントで停止

非同期処理のデバッグ

JS
async function fetchUserData() {
    try {
        const startTime = performance.now();
        
        // Promiseの状態を確認
        const userPromise = fetch('/api/user');
        console.log('Promise状態:', userPromise);
        
        const response = await userPromise;
        const userData = await response.json();
        
        const endTime = performance.now();
        console.log(`処理時間: ${endTime - startTime}ms`);
        
        return userData;
    } catch (error) {
        console.error('Error stack:', error.stack);
        throw error;
    }
}

// async/awaitのデバッグ
async function debugAsyncFlow() {
    console.group('非同期処理フロー');
    try {
        console.log('処理開始');
        const result = await fetchUserData();
        console.log('処理完了:', result);
    } catch (error) {
        console.error('エラー発生:', error);
    }
    console.groupEnd();
}

非同期デバッグのポイント:

  • Promise の状態確認
  • async/await の実行フロー追跡
  • エラースタックの確認
  • 処理時間の計測

パフォーマンス計測

JS
// パフォーマンスマークの設定
performance.mark('処理開始');

// 重い処理
for (let i = 0; i < 1000000; i++) {
    // 何か処理
}

performance.mark('処理終了');

// 計測結果の取得
performance.measure('処理時間', '処理開始', '処理終了');
const measurements = performance.getEntriesByType('measure');
console.log(measurements);

// メモリ使用状況の確認
const memory = performance.memory;  // Chrome のみ
console.log('ヒープサイズ上限:', memory.jsHeapSizeLimit);
console.log('現在のヒープサイズ:', memory.usedJSHeapSize);

メモリリークの調査

JS
class EventManager {
    constructor() {
        this.handlers = new Set();
        // メモリリークの可能性がある実装
        window.addEventListener('resize', this.onResize.bind(this));
    }

    onResize() {
        this.handlers.forEach(handler => handler());
    }

    // クリーンアップが必要
    dispose() {
        window.removeEventListener('resize', this.onResize.bind(this));
        this.handlers.clear();
    }
}

// メモリリークを防ぐ実装
class BetterEventManager {
    constructor() {
        this.handlers = new Set();
        this.boundOnResize = this.onResize.bind(this);
        window.addEventListener('resize', this.boundOnResize);
    }

    dispose() {
        window.removeEventListener('resize', this.boundOnResize);
        this.handlers.clear();
    }
}

実践的なデバッグ例

イベントデリゲーションのデバッグ

JS
class ListManager {
    constructor(containerId) {
        this.container = document.getElementById(containerId);
        this.setupEventListeners();
    }

    setupEventListeners() {
        this.container.addEventListener('click', (e) => {
            // イベントの伝播を確認
            console.log('Event path:', e.composedPath());
            
            const button = e.target.closest('button');
            if (!button) return;
            
            // データ属性の確認
            console.log('Button data:', button.dataset);
            
            // 特定の条件でブレークポイント
            if (button.dataset.action === 'delete') {
                debugger;
            }
            
            this.handleButtonClick(button);
        });
    }

    handleButtonClick(button) {
        const action = button.dataset.action;
        const itemId = button.dataset.id;
        
        console.group(`Button Click: ${action}`);
        console.log('Item ID:', itemId);
        console.log('Button:', button);
        console.groupEnd();
        
        // アクションの実行
        this[action](itemId);
    }
}

ネットワークリクエストのデバッグ

JS
class APIClient {
    constructor(baseURL) {
        this.baseURL = baseURL;
    }

    async request(endpoint, options = {}) {
        const url = `${this.baseURL}${endpoint}`;
        const startTime = performance.now();
        
        console.group(`API Request: ${endpoint}`);
        console.log('URL:', url);
        console.log('Options:', options);
        
        try {
            const response = await fetch(url, options);
            const endTime = performance.now();
            
            console.log('Response status:', response.status);
            console.log('Response time:', endTime - startTime);
            
            const data = await response.json();
            console.log('Response data:', data);
            
            console.groupEnd();
            return data;
        } catch (error) {
            console.error('Request failed:', error);
            console.groupEnd();
            throw error;
        }
    }
}

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

1.段階的なデバッグ

JS
function complexOperation(data) {
    console.group('複雑な処理の実行');
    
    // 入力データの確認
    console.log('Input data:', data);
    
    // 中間処理の確認
    const intermediate = processData(data);
    console.log('Intermediate result:', intermediate);
    
    // 最終結果の確認
    const result = finalizeData(intermediate);
    console.log('Final result:', result);
    
    console.groupEnd();
    return result;
}

2.エラーハンドリングの改善

JS
class ErrorBoundary {
    static handleError(error, context = '') {
        console.group(`Error in ${context}`);
        console.error('Error message:', error.message);
        console.error('Stack trace:', error.stack);
        console.log('Error details:', error);
        
        // カスタムエラー情報の記録
        if (error.response) {
            console.log('Response:', error.response);
        }
        
        console.groupEnd();
    }
}

まとめ

効果的なデバッグのために:

  • 適切なツールの使い分け
  • 段階的なアプローチ
  • パフォーマンスの意識
  • エラー情報の充実