ReactとTypeScriptで半年間サービスを走らせてみてよかった点を振り返って見る

自分はCODEPREPというオンラインプログラミング学習サービスをやっているのですが、今年の2月にReactとTypeScriptを使ってフロントエンドを再構築し、半年間サービスを走らせてみた結果について振り返ってみたいと思います。

はじめに

CODEPREPは月間で50万PV以上あるWebサービスです。

そのため、それなりの事態は発生するだろうと思い、フロントエンドにはエラー監視を導入して、ユーザーのブラウザ上で何かエラーが発生したら、直ちにSlackに通知が来て対応できるような万全の準備をしていました。

しかし、自分が担当して来たWebサービスの中では、もっともユーザーに頻繁に使われているにも関わらず、稀に見る安定稼働のサービスとなっています。

今回は、選定したフロントエンドの技術スタックのどの辺りが良かったのか、少し振り返りたいと思います。

TypeScriptの型チェックが有効だった

事前に予想していた通り、 TypeScriptの型チェックはかなり有効に働いています。

基本的には、APIのレスポンスを一度フロントエンドのデータモデルクラスに変換して、これをアプリケーションで利用する形を取るのですが、データモデルクラスの段階で型安全が保証されるので、ベースとなるデータモデルの品質が普通のJavaScriptと比べて格段に違います。

例えば、APIのデータモデルの変更に関連するリファクタであっても、何かデータ型で矛盾している点があればコンパイルエラーで事前に検知できる。当然、必要なプロパティがない、タイポしている、これらもコンパイルエラーで検知できます。

とにかく、リファクタに対する心理的・肉体的な負担が減りました。
これの何か良かったかというと、リリース後に自信を持ってコードの改善が継続的にでき、技術負債がたまりにくくなったことです。

今のところ一番目にするエラーは、オブジェクトがnullundefinedでプロパティや関数を参照するときに実行時エラーになることがあることですね。(特に初期ロード時)

これは、データモデルクラスのライフサイクルを設計し直してnullundefinedの状態がないようにしたあとで、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+TypeScriptもいいそ

ただ結局のところ、Reactを使い試行錯誤の末、Reactの流儀に沿って正しくできたことが大きかったかと思います。
おそらく別のフレームワークを使ったとしたら、戦略レベルでは考えることは一緒であっても、戦術レベルではもう少し違うアプローチをしていたと思います。

技術は正しく使ってこそ価値がある。

時間がある時にフロントエンドのコンサルなどもしているので、もし興味がある方がいたらtwitterなどでメッセください。