はじめに
私が遭遇したバグを例にどのように解消していったかを画像なども含め丁寧に解説していきたいと思います。のちの開発者の役に立てればと思います。
私が開発しているアプリの詳細は、以下のURLを参照してください。
遭遇した超重大バグ
リストを並び替える機能をreact-beautiful-dndを使って実装してしばらく経った頃に判明しました。
↑重要
Application error: a client-side exception has occurred (see the browser console for more information).
と表示されて、Webアプリが停止してしまいます。
しかも、これは本番環境です。速やかに復旧しなければ、いけません。
原因特定のための4つのアプローチ
すべてがあなたのバグ解消に当てはまるかどうか、分かりません。どれか一つでもバグの原因を特定し対処することができます。
①見当を付ける
冒頭でも述べた通り、最近react-beautiful-dndを使って、リストをドラッグアンドドロップで並び替える機能を実装しました。
この時に書いたコードで何か不具合があると考えました。
見当をつけてから取り組まないと、調べる範囲が膨大になって大変です。
②エラーメッセージを読む
エラーメッセージを読んでみましょう。
Application error: a client-side exception has occurred (see the browser console for more information).
クライアント側で例外が発生したことに起因するエラーだそうです。ブラウザコンソールを見ろとのことです。
コンソールには次のようなエラーが表示されていました。
"TypeError: 'id' のプロパティを未定義の値から読み取ることはできません"
と書いてあります。
Cannot read properties of undefined (reading 'id')というエラーメッセージは、何かが undefined の状態で、その id プロパティにアクセスしようとしたときに発生します。これは配列内の項目が null や undefined であるとき、またはオブジェクト自体が undefined であるときに起こり得ます。
特に青で囲った部分に注目してほしいです。
at Array.reduce ()
は、エラーが起こった処理が分かります。配列をreduce()で処理したときに発生したようです。
at home-1d24f46a72d85634.js:1:2674
は、エラーが起こった場所を示します。
home-1d24f46a72d85634.jsという名前のファイルの2674行目です。
リンクになっているのでクリックするとエラーが起こった場所に飛べます。
リンクをクリックすると、エラーが起きた場所が一瞬光ります。
このコードは、ブラウザが解釈しやすい形にトランスパイル(変換)され、さらにファイルサイズを小さくするためにミニフィケーション(圧縮)されたものです。この結果、元のソースコードと出力されたコードは大きく異なることがあります。したがって、エラーが発生した行番号やファイル名がビルド後のものである場合、それが元のソースコードのどの部分に対応するかを直接的に特定するのは難しい場合があります。
ただし、ソースマップ(Source Map)という技術を使用することで、ビルド後のコードと元のソースコードの間のマッピングを作成し、エラーが発生した場所を元のソースコードで特定することが可能になります。
今回はソースマップを使用していないので、開発環境から検索で「reduce」と検索をかけて特定しました。(ソースマップの仕様は、次回開発の課題にします)
私の場合、リストのドラッグアンドドロップの機能を追加後しばらくして発覚したバグだったので、特定は割と用意でした。
以上から、配列であるlistsに null や undefinded が混入したことによるエラーだと判断しました。
③エラーが起きた状況を特定する
これが一番大事だと思います。たまたま適当な操作でエラーが起きた状況を自分で再現してみるのです。
この作業でどの関数でバグが起きているか判断しやすくなります。
といっても、エラーの発生を特定することは容易ではないです。なので全画面録画を使って、操作とデベロッパーツールを録画しながらやると、素早くさまざまな操作をして試行しても、後からじっくり見返せるので便利でした。
録画しながら、エラーの発生状況を調査していると
- 名前変更→新規作成→新規リストをそのまま移動→アプリ停止&移動したリストが移動したローカルストレージのnoneになる。
- 名前変更→新規作成→リロード→新規リストをそのまま移動→SAFE
- 名前変更→新規作成→新規リストを名前を変更して移動→SAFE
- 名前変更→新規作成→新規リスト以外を移動→アプリは停止しないが、新規リスト消える(cookieは消えていない)
が分かりました。この時、エラーが起きた時の状況だけでなく、エラーが起きなかった状況を把握し整理すると、原因特定しやすいです。
ローカルストレージを見てみると
ローカルストレージに入るはずのないnoneが出現!
このWebアプリでは、リストの名前変更も、リストの新規作成も、リストの移動、すべての関数において、ローカルストレージの読み書きが発生します。
これらのことから、ローカルストレージの読み書きのタイミングで競合が起きていることが予想されます。
ドラッグアンドドロップでリストを移動した直後にWebアプリがエラーで停止することから、ドラッグアンドドロップの機能を追加したときにバグが混ざったのでしょう。
④エラーが発生した関数を特定する
どのタイミングで混入したバグなのか?
これを確かめるためには、
GithubとVercelのコンビネーションが役に立ちました。
このwebアプリはNext.jsで作られ、Vercel でデプロイしています。
Gitでリポジトリにpushするたびに自動でデプロイしてくれるのです。
なんと過去のバージョンに遡ってWebアプリにアクセスすることができるのです。
これを利用して、現行のWebアプリで発生しているバグが、どのタイミングで混入してしまったか検証しました。
今回のケースの場合、ドラッグアンドドロップの機能を導入中にバグが混じったと見当がついてるので、その前後から調べていきました。すると、あるコミット以降で重大バグが発生していることに気づきました。
これは別のバグを修正するためのコミットでした。詳しい解説は割愛しますが、ドラッグアンドドロップの終了時に実行されるonDragEndと、リストの名前変更の機能が、それぞれローカルストレージのリストデータの読み書き時に、うまく連携が取れず、名前の変更が上書きされなかったのです。
バグ修正完了!と安心しきっていたのですが、まさかこの変更が別の重大バグを生んでいるとは…
原因が分かれば話は簡単です!
setGlobalList();を使うにではなく、もとからあったsetLists();を使えばよかったのです。
後から考えれば、なんともお粗末なミスですが、ローカルストレージにアクセスする二つの関数の競合によって今回のバグが発生したのでした。
終わりに
初めてWebアプリ開発をして、たくさんのバグに遭遇しました。
それらを乗り越えて思ったことを以下に示します。
-
プログラミングは、素直で正直である。
対策:バグが起こるのは人間のミス。夜遅くに複雑なことをやらない。 -
新機能を実装する際、今までの機能との競合を考える必要がある。
対策:設計を意識する必要がある。ER図のようなものを使うと全体の構成流れが把握できる? -
Gitのcommitはマメにすべし、コメントも丁寧に書くべし
よく言われていることですが、身をもって体感しました。
この記事がどこかの誰かの役に立てばうれしいです。
この記事に対するいいねや、誤りの指摘、コメント等お待ちしております。