Dev Study
← 解説「ブレークポイント — console.log を卒業する」に戻る

サンプルコードで身につける: ブレークポイント — console.log を卒業する

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1console.log の限界とブレークポイント

log で確認しようとすると、見たい変数が増えるたびにコードを書き換えて保存し直すことになります。ブレークポイントなら一度止めるだけで、その時点のすべての変数を書き換えなしで観察できます。

// バグ: 合計金額が NaN になる

recalc() {
  let sum = 0;
  for (const item of this.items()) {
    // console.log 方式だと確認のたびに書き換え→保存→再現が必要
    // console.log(item);          // 1回目: item を見る
    // console.log(item.price);    // 2回目: price を見る…キリがない
    sum += item.price * item.qty;
  }
  this.total.set(sum);
}

// ブレークポイント方式:
// 1. DevTools → Sources → Ctrl+P で .ts ファイルを開く
// 2. sum += の行番号をクリック(青い印が付く)
// 3. 操作を再現 → その行で停止し、Scope 欄に全変数が並ぶ
//    item: { price: '300', qty: 2 }
//                  ^^^^^ 文字列! APIが数値でなく文字列を返していた
// → 原因判明: '300' * 2 は動くが '300' + 数値の連鎖で NaN に
// → 直し方: 受け取り時に Number(item.price) で数値化する

2debugger 文で確実に止める

Sources パネルで目的の行を探すのが難しいときは、コードに debugger 文を書けば到達した瞬間に必ず止まります。手軽ですがコードの書き換えなので、確認が終わったら必ず削除します。

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-search',
  template: '<input (input)="onInput($event)" />',
})
export class Search {
  keyword = signal('');

  onInput(e: Event) {
    // DevTools を開いた状態でこの行に到達すると必ず停止する
    debugger;
    const value = (e.target as HTMLInputElement).value;
    this.keyword.set(value.trim());
  }
}

// 注意点:
//   ・DevTools を閉じていると debugger 文は素通りする
//   ・消し忘れるとそのまま本番に混入し、利用者の画面でも止まりうる
//   ・使い終わったら必ず削除。恒久的に使う場所には向かない

3ステップ実行で値の変化を追う

ブレークポイントで止めた後は、ステップオーバーで1行ずつ進めながら Scope 欄の値を見ます。どの行を境に値がおかしくなるかが分かれば、その行が原因です。この例では割引計算の順序ミスを特定します。

// バグ: 割引後の金額が想定より大きい

applyDiscount(price: number, rate: number): number {
  // ← ここにブレークポイントを置き、1行ずつステップオーバーする
  let result = price;          // 停止1: result = 1000(正しい)
  result = result - rate;      // 停止2: result = 999.9(あれ?)
  //       ^^^^^^^^^^^^^
  // 原因: rate は 0.1(割合)なのに金額として引いていた
  //       1000 - 0.1 = 999.9 になってしまう
  return Math.round(result);
}

// ステップ実行の見方:
//   ステップオーバーを押すたびに1行進み、Scope 欄の値が更新される
//   「停止2」で値が想定(900)とズレた → この行が原因と確定
//
// 直し方: 割合として掛けてから引く
//   result = result - result * rate;  // 1000 - 100 = 900

4止まらないブレークポイントも手がかり

印を付けたのに止まらないのは「その行が実行されていない」証拠で、それ自体が重要な情報です。この例ではイベント名の書き間違いでメソッド自体が呼ばれていないことを突き止めます。

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-like-button',
  // バグ: ボタンを押しても件数が増えない
  // 原因: イベント名を (clik) と書き間違えている(存在しないイベント)
  template: '<button (clik)="increment()">いいね {{ count() }}</button>',
})
export class LikeButton {
  count = signal(0);

  increment() {
    // ← ここにブレークポイントを置いてもまったく止まらない
    //   「止まらない = この関数がそもそも呼ばれていない」
    //   → 疑いを関数の中身からテンプレートの結線へ移せる
    this.count.update((c) => c + 1);
  }
}

// 直し方: (clik) を (click) に直す
// 補足: テンプレートのタイプミスはコンパイルを通ることがあるので、
// 「止まらない」という観察が最短の発見ルートになる

5停止中にコンソールでシグナルを覗く

ブレークポイントで停止している間は、コンソールがその場所の変数にアクセスできる電卓になります。シグナルは関数なので、count ではなく count() と呼び出して中身を見るのがつまずきやすいポイントです。

// ブレークポイントで停止中、Console タブに切り替えて式を試せる

// 停止中のコンソールでの入力例:
//
//   > this.count
//   f signal()          ← シグナル本体(関数)が返るだけで中身は見えない
//
//   > this.count()
//   5                   ← 関数として呼べば現在値が見える
//
//   > this.items().filter(i => i.qty > 1)
//   [{ price: 300, qty: 2 }]   ← その場で加工を試して仮説を検証できる
//
//   > this.count.set(100)
//   ← 値を書き換えて「この値なら直るか」を実験することも可能
//
// よくあるつまずき:
//   シグナルを () なしで見て「値が表示されない」と混乱する
//   シグナルは「値の入れ物を返す関数」なので必ず () を付けて呼ぶ