■ 1. 記事の概要
- バグ修正のプロセスをReact/React Routerのスクロールバグを実例として体系的に解説した記事
- AIも人間のエンジニアも再現手順なしで修正を試みるという同様の誤りを犯す傾向がある
■ 2. Step 0: 再現手順なしでの修正の失敗
- ClaudeにスクロールバグのFix指示を出したが繰り返し誤った修正を提案した
- useEffectの条件変更やスムーススクロールからインスタントスクロールへの変更などを試みたが効果はなかった
- バグが修正されたかを検証する手段がないことが根本的な失敗の原因
■ 3. Step 1: 再現手順(Repro)の確立
- Reproとはバグが確実に再現できる操作手順であり「何をするか」「期待動作」「実際の動作」の3要素で構成される
- 毎回確実にバグが再現できるReproは信頼性の高い検証を可能にする
- 再現率が低い場合はネットワークのモック化など不確実性の排除が必要
- Claudeはスクリーンを視覚的に確認できないため視覚的なRepro(スクロールのジッター)は有効に機能しない
- 元のReproが使用できない場合は別の形式のReproへの変換で対処可能
■ 4. Step 2: 再現手順の絞り込み
- 別のReproへの変換には新しいReproが元のバグと無関係である可能性というリスクが伴う
- 視覚的なジッターの代わりにスクロール位置の数値変化を測定するReproに変換した
- ボタンクリック前後のdocumentのスクロール位置を計測
- 位置に変化がない状態を「バグあり」として定義
- 新しいReproの有効性を「バグが解消した状態でも正しく機能するか」で必ず検証する
- ネットワークコールをコメントアウトするとスクロール位置に変化が生じることで確認
- コメントイン・コメントアウトを繰り返して因果関係を検証することが望ましい
- 新しいReproが元の問題と完全に一致しなくても関連性が認められれば継続する価値がある
■ 5. Step 3: コードの段階的削除による問題の絞り込み
- 新ブランチを作成し以下のワークフローを繰り返す
- Reproを実行してバグの存在を確認
- 関連コードから何か一部を削除(コンポーネント・イベントハンドラ・条件・スタイル・importなど)
- Reproを再実行してバグの継続を確認
- バグが継続する場合はその変更をコミット
- バグが消えた場合は解消要因の仮説を記録しリセット後により小さな単位で再試行
- Claudeは途中から独自仮説を立ててその検証に特化したテストケースを作成し始めた
- 仮説を立てて検証すること自体は問題ない
- 仮説検証が失敗した場合はバグが再現するチェックポイントに戻り削除を継続することが不可欠
- 「整礎再帰(well-founded recursion)」の概念と同様に常に確実な前進が保証される必要がある
- Fibonacci関数のバグ例(fib(n)がfib(n)を呼び出す無限再帰)を整礎でない再帰の例として示す
- Lean言語ではnが小さくならない再帰をコンパイル時に型エラーとして検出できる
- Reproの削減も同様に常にReproが「小さくなっている」ことを確認しながら進める必要がある
- このプロセスを繰り返すことで最終的には削除できるものがなくなり根本原因が浮かび上がる
■ 6. Step 4: 根本原因の特定
- Step 3のプロセスにより問題が単一ファイルに絞り込まれた
- ファイルをルーターの外に移動すると正常に動作しルーター内に戻すと再び破損した
- トップレベルルートでは動作しルートレイアウト内のネストで破損することが判明した
- 根本原因はReact RouterのScrollRestorationコンポーネントのバグ
- 本来はルート変更時のみ起動すべきところ全てのrevalidation時に起動するバグが存在していた
- アクション経由のネットワークコールがrevalidationを発生させScrollRestorationが起動した
- ScrollRestorationがscrollIntoView実行中に干渉しジッターを引き起こしていた
- 古いバージョンのReact Routerを使用していたことが問題の前提条件
- この手法はFacebookのReactツリーの半分を削除してバグを50行のコードまで絞り込んだ事例でも有効だった