読み始めた切っ掛け
最近、業務でReactのコードを書いていたのですが、不可解なバグを起こしてしまいました。具体的には「音量を変えているはずなのに、音が鳴らない」という現象です。
内部の挙動を推測して、色々と試行錯誤してみました。console.log()
も使ってみたのですが、処理が複雑で原因となる箇所が分かりませんでした。
このバグについて、CTOのdarashiに質問したところ、あっという間に原因を特定し、解決することができました。
その際に教えて頂いた本が、今回ご紹介する『コードが動かないので帰れません! 新人プログラマーのためのエラーが怖くなくなる本』でした。
タイトルを検索したところ、この本の著者の方が、ムーザルちゃんねる*1のお二人であることに気づきました。最近YouTube自体を見ていなかったので、本を出していたことに気付いていませんでした。
「これはすぐに買うしか無い!」と思い、早速購入して中身を読みました。
以下に、自分が学びになったことを書いていきます。
スタックトレース
エラーメッセージが発生したときに、一文目はよく読むようにしていましたが、二文目以降のファイルのパス(?)については「なんでこんなにいっぱい表示されているんだろう?」とあまり注意を向けていませんでした。また、名称も知りませんでした。
これはスタックトレースといって、エラーが発生するまでの処理の流れを教えてくれていたらしいです。
スタックトレースとは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
スタックトレースがあることで、最終地点のコードを読んでも解決できないときに、その一つ前のコードを読むことができるので、効率的に原因をたどることができるそうです。知らなかった!
プリントデバッグは複数埋め込んで良い
本書を読んでいて気付いたのですが、console.log()
を埋め込むとき、色んな場所に複数埋め込んで確認しても良いことに気づきました。
自分の場合、一度のデバッグでせいぜい1個か2個くらいのconsole.log()
を埋め込んで確認していましたが、この方法だと非効率です。気になる箇所にはまとめてプリントデバッグを埋めてあげれば効率的にログを確認することができます。
関数の実行前と実行後でサンドイッチしてあげる、という方法も新たな知見でした。
コメントアウトやコードを消しながら範囲を絞り込む
二分探索法について分かったつもりになっていましたが、コメントアウトを駆使しながら自分がデバッグしていたかというと、あまりできていない気がしました。
また、コードを消してあげて、最小限のコードから確認するという手法も参考になりました。
デバッガを使う手間を惜しまない
以前はちゃんと使いこなせていた気がするんです。デバッガ。
具体的には、以前のこのコントリビュートも、OSSにdebugger
を埋め込んで、少しずつ探索しながら見つけたものでした。
人生で2回目のOSSコントリビュートをしました!🎉🥳 - LEFログ:学習記録ノート
最近はちょっと慢心していたのか、デバッガを使う機会が減っていました。なのでこれからは手間を惜しまずどんどん使えるようにしていきたいと思います。
GitHubの検索テクニック
GitHubの検索欄で正規表現が使えるなんて……知らなかった!
エラーの発生場所を疑ってみる
最近、こんなことがありました。
FreshというDenoのフレームワークで開発を進めていました。DenoにはDenoKVという機能があり、Redisのように簡単にデータを保存することができます。
公式ドキュメントを読んでいると、そこにTTL(Time to Live)の機能が追加されたことが分かりました。
しかし、公式ドキュメントに従って、自分のファイル上にTTLの値を書いてみても、なぜかエラーが出るのです。VSCodeのコードジャンプで型情報を見に行ったところ、TTLを第三引数として取れるように定義されていません。
これは不思議です。もしかすると自分のコードの書き方が間違っているのだろうか……? としばらく悩んでいました。
最終的に、darashiさんに質問して原因が分かりました。自分のDenoのパッケージが古いものだったのです。そして、Denoのパッケージをアップデートしたところ、無事にTTLが動くようになりました。
これは自分が遭遇した一例ですが、このように「コードに間違いがなくても、それ以外のところに原因があるために、エラーが出てしまっていること」を身をもって実感しました。
本書においても、エラーが別の場所で起こっていないか(見る場所が違っていないか)を確認するように書いてあって、まさにそのことだと思いました。
デバッグしやすいコードを書く
スコープを狭める
「6-2 スコープは可能な限り狭めよう」に出てくるconst data = getData();
の例を読み、なるほどな〜と思いました。何も考えずにコードを書いていると、変更前のほうが読みやすそうだと思ってしまうかもしれなかったです。
また、Reactの場合だと「Data down, events up」を意識してついつい親コンポーネントでuseStateを使ってしまいがちですが、あまり上のコンポーネントに置きすぎてしまうと、今度はスコープが無駄に広くなってしまうので、適切なコンポーネントにuseStateを置いてあげるのが大事だな〜と改めて思いました。
そもそもコードの設計や書き方が良くないかも
今までバグやエラーを生んでしまったコードのことを考えると、そもそもの設計の時点で無駄に複雑化していることが多かったです。
適切にクラスや関数を切り出すことで、読みやすく、そして理解しやすい挙動になり、エラーが生まれにくくなります。そして、デバッグの面でも助かることが分かりました。
まとめ
自分は今まで、「エラーメッセージを読めているつもり」でいました。
以前、こんな記事を書いたこともありました。
【チェックリスト】エラーが出たときのチェック項目をまとめてみました! - LEFログ:学習記録ノート
しかし、実際には「エラーメッセージを読む」という作業一つを取っても、種類やスタックトレースを意識する必要があります。デバッグにも適切な方法があり、ただ漫然とプリントデバッグを埋め込むだけではいけないことが分かりました。
最初に書いた「切っ掛け」の話に戻りますが、自分はバグを発生させてしまったとき、効果的なデバッグをおこなえず、推測ベースで試行錯誤してしまっていました。
推測ではなく、デバッグしながら実際に値を確認することが必要でした。まさに「推測するな、計測せよ」です。
本書を参考にして、効果的な方法を用いつつ、エラーやバグに向き合っていきたいと思いました。