自分はCODEPREPというオンラインプログラミング学習サービスをやっているのですが、今年の 2 月に React と TypeScript を使ってフロントエンドを再構築し、半年間サービスを走らせてみた結果について振り返ってみたいと思います。
はじめに
CODEPREPは月間で 50 万 PV 以上ある Web サービスです。
そのため、それなりの事態は発生するだろうと思い、フロントエンドにはエラー監視を導入して、ユーザーのブラウザ上で何かエラーが発生したら、直ちに Slack に通知が来て対応できるような万全の準備をしていました。
しかし、自分が担当して来た Web サービスの中では、もっともユーザーに頻繁に使われているにも関わらず、稀に見る安定稼働のサービスとなっています。
今回は、選定したフロントエンドの技術スタックのどの辺りが良かったのか、少し振り返りたいと思います。
TypeScript の型チェックが有効だった
事前に予想していた通り、 TypeScript の型チェックはかなり有効に働いています。
基本的には、API のレスポンスを一度フロントエンドのデータモデルクラスに変換して、これをアプリケーションで利用する形を取るのですが、データモデルクラスの段階で型安全が保証されるので、ベースとなるデータモデルの品質が普通の JavaScript と比べて格段に違います。
例えば、API のデータモデルの変更に関連するリファクタであっても、何かデータ型で矛盾している点があればコンパイルエラーで事前に検知できる。当然、必要なプロパティがない、タイポしている、これらもコンパイルエラーで検知できます。
とにかく、リファクタに対する心理的・肉体的な負担が減りました。 これの何か良かったかというと、リリース後に自信を持ってコードの改善が継続的にでき、技術負債がたまりにくくなったことです。
今のところ一番目にするエラーは、オブジェクトがnull
やundefined
でプロパティや関数を参照するときに実行時エラーになることがあることですね。(特に初期ロード時)
これは、データモデルクラスのライフサイクルを設計し直してnull
やundefined
の状態がないようにしたあとで、TypeScript のコンパイラの--strictNullChecks
をオンにすると軽減できるかもしれない。
というか、、、そもそも自分の設計が悪い。
末端の UI コンポーネントを徹底的に Stateless にした
ここでの Stateless コンポーネントとは、Functional stateless component のことで、内部に一切状態を保持せず、ただ外部から与えられた値を元に描画することに特化したコンポーネントのことです。
ボタン・リンク・タブなどの共通的に利用できそうな UI パーツは、Stateless コンポーネントとして小分けに作成するようにしました。
これにより、Stateless コンポーネントはただ与えられた値を元に UI を描画するだけとなり、props で渡される値のパターンだけ注意すればいい状態となりました。
内部に state により状態遷移を持たないので、コンポーネントがシンプルで見通しがよくなり、ほとんど不具合を起こす要因が見当たらなくなりました。
Stateless コンポーネントからの発生する(change, click などの)イベントについては、コンポーネントの props に
onChange
などのハンドラを渡すようにして、処理自体を呼び出し元に移譲しています。
これの何が良かったかというと、末端の UI コンポーネントを信頼して使うことができることです。
仮に渡す props の値が何か間違ったとしても、TypeScript の型チェック機能が有効に作用するわけで、しっかり作り込んだ Stateless コンポーネントを作りさえすれば、あとはそれに適切な props を渡せばいいという状態になりました。
コンテナパターンを採用してフロントのアプリケーションを 3 レイヤーに分けた
3 レイヤーとは、次の 3 つです。
- コンテナ
- ページコンポーネント
- Stateless コンポーネント
大まかに「ページコンポーネント」が各画面 1 つに対応していて、「Stateless コンポーネント」は共通的に利用する小さい UI 部品のことです。そして「コンテナ」とはいくつかページコンポーネントを束ねるコンポーネントのことです。 アプリケーションで共通で利用するデータなどを保持してたり、ページへの許可されていないアクセスをブロックする門番のような役割もしています。どちらかというとGatewayと言った方がしっくりくるかもしれません。
厳密にいうと世にある React のコンテナパターンのように綺麗に責務が別れていません。かなりオリジナル色が強いです。
これの何が良かったかというと、ページコンポーネントからアプリケーション全体で必要なロジックの大部分をコンテナに移譲できたことです。 コンテナ・ページコンポーネント・Stateless コンポーネントの各役割が割としっかり分離できているので、何か問題があった場合でも、容易にどの辺りが問題ありそうかあたりをつけることができます。
アプリケーションの構造をなるべくフラットに保った
これは先の 3 レイヤーの話ともかぶるのですが、内部の状態を保ったページコンポーネントは極力ネストさせないようにしました。
これはつまり、コンテナに対してページコンポーネントは常にフラットな構造を保つということです。
Stateless コンポーネントはネストさせても OK です。(ただしあまり深くしなければ)
内部の状態があるページコンポーネントをネストした場合、props で渡って来た値を state に格納して、これをまた子コンポーネントに渡すといったことをやることもあるかと思いますが、これは React でよくやってしまうアンチパターンだと思います。
なぜかというと、props の変更を検知するためにcomponentWillReceiveProps
を実装する必要があるためです。
大抵の場合、この部分の実装は難しく、複雑になりがちで、コンポーネントのメンテナンスコストを引き上げます。
これの何が良かったかというと、このようなネストを極力減らすことで、アプリケーション全体の構造をシンプルに保ち、不要なデバックコストを削減できたとです。
普段はreact-devtoolsをデバックに使っているのですが、何かおかしい動きをした時にチェックする場所が固定できる。 コンポーネントでデータを受け渡しをしている間に、なぜか途中から値が変わっている、なんて悪夢はもうありません。
まとめ
実は、他にもいろいろ工夫している点があるのですが、大きなところはこんなとこです。
リニューアル前に React と TypeScript でプロダクトを作っているけど、なかなか良さそうだという趣旨の発表をしたのですが、正直期待以上でした。
こちらが、リニューアル前に話した内容。
ただ結局のところ、React を使い試行錯誤の末、React の流儀に沿って正しくできたことが大きかったかと思います。 おそらく別のフレームワークを使ったとしたら、戦略レベルでは考えることは一緒であっても、戦術レベルではもう少し違うアプローチをしていたと思います。
技術は正しく使ってこそ価値がある。