🌳

全フロントエンドエンジニアに告ぐ。ブラウザコンソールのwarningを軽視するな

2022/10/13に公開

はじめに

さっそく質問である。あなたのフロントエンドプロジェクトでは、どれほどコンソールにwarningが出力されているだろうか。
一切warningを残さないという方もいれば、ブラウザでアプリを操作しているとちらほら出てきてしまうという方もいるだろう

今回の対象読者に以下のような方を想定している

  • 開発中のブラウザのコンソールにwarningがちらほら出てしまっている方
  • よりプロダクトの品質を上げたい方
  • なんとなくTypescriptの型をつけてそれで終わっている方

もちろんこれはサーバーサイドやインフラエンジニアにも言えることかもしれないが今回はフロント領域にフォーカスして話をしたい

そして今回私からはwarningを絶対に残さないでほしいという主張をさせてほしい

もっと言おう。開発環境にwarningがたくさん残っているプロジェクトで、開発安定したプロダクトは見たことがない

ブラウザコンソールにログが出力される意義

ブラウザのコンソールへのログ(以降単に「ログ」と表現する)は基本的にJavascriptから出力される。それはJavascriptの仕様だったり、エラーコードだったり、ライブラリ開発者の注意喚起だったり様々な理由がある

その中でもブラウザのコンソールに出力されるログの目的は以下3つだと考えている

  • 問題が発生したときに調査するため
  • 問題の発生を防ぐため
  • デバッグのため

このログは開発者をサポートするためにある。あなたの書いたコードが実行されたときに、何が起こったのか、なぜその事象が起こったのか原因を特定することもできる

問題が発生したときに調査するため

フロントのアプリで意図したUIの表示がされない。そんなときにはまず最初にブラウザのコンソールを確認するだろう

コンソールに出力されたログから問題が発生したコンポーネントや関数を特定し、データの状態を確認して、処理を修正する。これが一般的なコード修正の流れだ。

コンソールのログの中には問題を解決するためのヒントが隠れているはずだ

問題の発生を防ぐため

2つ目に問題の発生を防ぐためという理由がある。

多くのフレームワークやライブラリは意図しない使われ方を検知したときにwarningを出力するように作られている。これは開発者が正しくツールを使い、バグやトラブルが出ないように開発者をサポートしてくれる

フレームワークやライブラリの製作者というのは、ユーザーによって間違った使われ方をしないようにwarningを仕込むことで生産的な開発を促進できると考えている。warningはツール製作者から利用者に向けたメッセージとも言える

そういったツール製作者側の意図を無視すれば当然プロダクトの品質は下がることになるだろう。そのためにもwarningは絶対に無視すべきではない

デバッグのため

最後にデバッグのためという理由がある

デバッグとはプログラムの誤りを見つけて修正することだ。デバッグをするためにプログラム内部の変数の値を調べたり、console.log('fire')等で処理が本当に実行されているか、またUIの表示速度やデータの計算速度のパフォーマンスの計測をするためにログを仕込むこともあるだろう

そのように開発者が意図してコンソールのログへ何かを出力することもある。デバッグは開発において非常に重要な作業だ

なぜwarningを残すべきでないのか

それではなぜwarningを残すべきでないのか考えてみよう

プロダクト品質の低下

まずフロントにおいてはwarningが残っていても意図通りに動くから問題ないであろうという考えがあるようだ。これは非常に危ない考えだ

それでは具体的なコードを見てみよう

今回は私が普段使っているVueのコードを例として取り上げるが、他のUIフレームワークやライブラリを使っている方でもなんとなく読めるだろう

propsの型が実態とずれている例

Message.vue
<script lang="ts" setup>
interface Props {
  message: string
}

const props = defineProps<Props>()
</script>

<template>
  <p>{{ props.message }}</p>  
</template>

今回Messageコンポーネントを作成した。テキストを子コンポーネントに流して表示するためのシンプルなコンポーネントだ

実際に親コンポーネントからデータを流し込んでみよう

Parent.vue
<template>
  <Message message="メッセージです" />
</template>

これで見事に子コンポーネントがレンダリングされるはずだ。では流し込むデータを変えてみよう

Parent.vue
<template>
  <Message :message="null" />
</template>

Web APIから取得したデータを流し込んだがそのデータがたまたまnullだったときを想像してほしい。実はこの場合も特にエラーは出ず、Messageコンポーネントでは単純に空タグが描画される

しかし私はscriptタグの中で以下のように定義したはずだ。messageはstring型であると。

Message.vue
<script lang="ts" setup>
interface Props {
  message: string
}

const props = defineProps<Props>()
</script>

それではコンソールを見てみよう。以下のようなwarningが出力されているはずだ

[Vue warn]: Invalid prop: type check failed for prop "message". Expected String with value "message", got Null

このwarningはVueが自動で出力してくれている。これは私が定義した型と実際に渡されたデータの型が一致していないことを指す。messageはstring型を期待していたが、実際にはnullを受け取ったというメッセージである

これは私が意図した挙動ではない。もし対応するとしたら以下のような方法があるだろう

  • messageがnullの場合はMessageコンポーネントを描画しない
  • messageがnullの場合は何も表示されていないことを明示する

まず前者の場合は以下のように書ける。v-ifを使ってMessageコンポーネントをレンダリングしないように指定した

Parent.vue
<template>
  <Message v-if="message" :message="message" />
</template>

続いて後者の対応である

Message.vue
<script lang="ts" setup>
interface Props {
  message?: string // messageをoptionalに変更
}

const props = defineProps<Props>()
</script>

<template>
  <p>{{ props.message || '-' }}</p> // messageがnullの場合は'-'を表示する
</template>

まず型に注目してほしい。messageはstring型かもしくはnullやundefinedの可能性があるということを明示している。こうすることで他の開発者が後々コンポーネントを改修する場合には、messageが空になる可能性があることを知ることができる

続いてtemplateでは||という表現を使っている。これは左側の値が空なら右側の値を適用するという意味だ。つまりmessageがnullやundefinedの場合は-を表示するということになる

このようにすることでwarningを防ぎつつ適切なUIを表現することができた

そして問題はここからだ。先程のMessageコンポーネントに次のような処理を入れるとどうなるだろうか

Message.vue
<script lang="ts" setup>
interface Props {
  message: string // messageはoptionalでない
}

const props = defineProps<Props>()

const message = computed(() => props.message.replace(//g, '')) // 何も警告してくれない
</script>

<template>
  <p>{{ message || '-' }}</p>
</template>

今回は文字列から「、」を取り除く処理を挟んでViewに描画させてみた。例えば「私は、フィリピンに、住んでいます」という文字列をpropsに流し込んだ場合には「私はフィリピンに住んでいます」と出力されるはずだ

しかしnullを親から流し込んだ場合はどうなるだろうか。以下のようなエラーが発生し画面が真っ白になったり、Nuxtであれば500エラー画面が表示されるはずである

Uncaught (in promise) TypeError: Cannot read properties of null (reading 'replace')

これはpropsとして流し込んだmessageがnullであり、nullという値にreplaceというメソッドは定義されていないが故に起きるエラーだ

もしmessageがoptionalでなくwarningを無視して実装していた場合、messageがnullにも関わらずreplaceメソッドの追加が簡単にできてしまう。これはまさにwarningを放置していたからに他ならない

このようにwarningを無視し続けるといつ致命的なエラーにつながるか怯えながら過ごさなければならないのだ

v-forにおけるkeyの値がユニークでない例

他にも問題があるケースを見てみよう

<script lang="ts" setup>
const users = ref([
  {
    id: 1,
    name: 'John Doe',
  },
  {
    id: 1,
    name: 'James Smith',
  },
  {
    id: 1,
    name: 'Jack Brown',
  },
])
</script>

<template>
  <ul v-for="user in users" :key="user.id">
    <li>{{user.name}}</li>
  </ul>
</template>

仮想DOMに疎い方がやりがちなミスとしてDOMループ中のキーがユニークでなかったり、indexが指定されていたり、そもそもキーが指定されていないことがあったりする

今回はキーがユニークでないケースだ。仮想DOMは兄弟要素の描画をkeyによって一意に決めるという仕組みがある。もしキーが被っている場合には描画がダブったり意図しないエラーが発生するだろう

今回のケースでは以下のようなwarningが出力された

[Vue warn]: Duplicate keys found during update: 1 Make sure keys are unique

UIの1回目の描画だとこのミスに気づかないかもしれない。しかし再レンダリングされたときに初めて目視できるバグが発覚する

ログ汚染の問題

ブラウザコンソールのログは先程も述べたように以下3つの目的がある

  • 問題が発生したときに調査するため
  • 問題の発生を防ぐため
  • デバッグのため

それではもしブラウザのコンソールに大量のwarningが出力されていたらどうなるだろうか。

UIを操作したり、何かをキーボードで入力する度に次から次へとwarningが出力される。あっという間に300行を超えてしまった、、、という場面を想像してほしい

とあるUIを操作している最中に急にアプリがクラッシュして真っ白になってしまった。そこであなたはログを確認する。だがひたすらwarningが出力されているので原因が特定できる箇所までひたすらスクロールして探さなければならない。

それでは「デバッグ」を考えてみよう。ある処理で変数の中身がどうなっているかを連続で出力したい。そこでログを仕組んだはいいがwarningが大量に出力されている

warning: 1
warning: 2
warning: 3
warning: 4
.
..
...
data: hoge
waning: 1
waning: 2
waning: 3
waning: 4
.
..
...
data: fuga

こんなログを読みたいだろうか。データの連続性を解析したいだけなのにひたすらwarningが邪魔をしてくる

つまりあなたはそれだけ問題の修正に時間を取られてしまうことになるのだ

フロントのログはバックエンド側よりも軽視されやすい?

最近思うのはフロントのログはバックエンドやインフラよりも軽視されやすいのではないかということだ

理由として大きいのはwarningが出ていても意図した通りに動いていれば正常とみなされる点であろう

ユーザーがプロダクトに持つ関心の大半はUIだ。UIが正常に動いていればそのプロダクトは正常に動いているとみなされる

特に開発に詳しくないお客さんの場合はそうだろう。プロジェクトによってマネージャーはデプロイされたテスト環境やステージング環境、本番環境しか確認しないということも考えられる

だがこれは非常に危険な考えだ。少なくともフロントエンドのwarningは開発環境でしか出さないことが多い。理由としてはそのwarningがセキュリティの問題につながることがあるからだ。やたらとwarningがシステムに出ているとそこを脆弱性として攻撃者に利用される可能性がある

UIが正常に動いていいたとしてもwarningは開発時に徹底的に排除するよう意識しよう。そうしなければいつか必ず足元をすくわれることになる

また言語の特性もあるだろう。フロントは静的型付言語であるTypescriptが普及し、比較的型安全に開発できるようにはなったが先程の例のようにVueのtemplateに意図しない値を渡したとしても正常に動作するという特性がある。ReactもViewでコンパイルエラーが発生するかどうかの違いはあれど、基本的には同様だ。

言語によってはランタイムでエラーを出すこともあるだろうが、Typescriptはそうではない。これは仕方のないことだ。ただ、だからこそ人的チェックが重要であるということが筆者の考えだ。javascriptから出たエラーは基本的にはエラーだと思って解消してほしいというのが私の願いである

まとめ

今回はブラウザのコンソールに出力されるログの重要性について話した。普段バックやインフラに触れていなければログについてはそこまで細かく気にしていない方も多いかもしれない

しかしプロダクトの品質を上げるためにまずやれることはコンソールにwarningを出さないことだ。もちろんユニットテストや結合テストの自動化も品質を上げるためには重要だ

だがそれらと共に普段から開発環境でwarningを出さないようにぜひ意識してほしい

GitHubで編集を提案

Discussion