♻️

ReactのSPAのリファクタリングでやったこと

はじめに

シェルフィー株式会社でGreenfile.workの開発をしているtaroと申します。
弊社では2022年11月頃からGreefile.work入退場の機能刷新プロジェクトを進めており、1月下旬にリリースしました。
同プロジェクトでは並行して設計段階からのリファクタリングを進めており、この記事ではその概要をご紹介し、記事を読まれた方にコード品質の現状が伝わればと思っております。

リファクタリングの背景

Greenfile.work入退場は2020年12月頃に開発を開始し、機能の追加や改善などを進めながら運用してきました。
弊社では今後、Greenfile.work入退場の機能追加や改善にこれまでよりももっと力を入れたいと考えており、その準備としてリリース時から溜まっていた技術的負債の解消を機能開発と並行して進めています。
改善前の主な技術スタックはこちらです。
  • React v16.13(CSRのみ)
  • JavaScript
  • webpack v4
  • styled-components
  • redux-saga
具体的な課題についてはやったことでご紹介します。

設計思想

具体的にやったことの前に、フロントエンド全体での設計思想です。
設計を整える目的は「ユーザーへの提供価値向上を加速させる」としました。
提供価値とは各機能の品質や機能数などを指していて、設計を整えることで開発効率を高め、ユーザーへ提供価値を届ける速度を高めたいと考えています。

トータルで見てユーザーへの提供価値向上が加速しているか?

「そりゃそうだろ!」と思われるかもしれませんが、大切にしたいのは加速です。
例えば、変更容易性を高めるために複雑な設計を導入した結果、各実装者がその設計に合わせるのに多くの時間をかけてしまうと、トータルで見た開発効率は下がってしまうかもしれません。
特に弊社の場合は、フロントエンドとバックエンドでエンジニアは分かれておらずチームメンバーも変化する環境の中で長期的にプロダクトを運用していく必要があり、設計の複雑さが開発効率に及ぼす影響は大きくなります。
そのためディレクトリ構成やComponentなどのそれぞれで共通して「設計を整えた結果トータルで見て加速しているか?」を考えて可能な限りシンプルな設計にしています。
そしてその結果、各実装者の設計を理解するための思考リソースを最小化し「よりUXが高いUIは何か?」などのユーザーへの提供価値向上に割ける思考リソースの最大化を目指しています。

やったこと

では具体的な改善内容です。
※一部の改善内容は、後日詳細をまとめた内容を別記事として公開できればと思います。

JavaScriptからTypeScriptへの移行

全てのJavaScriptファイルをTypeScriptに書き換えました。
移行中は拡張子を変えて一時的にで型を付けていましたが、現在は全てのに適切な型をつけ終え移行作業も完了しています。

機能を関心事としたディレクトリ構成への移行

リファクタリング前は「どこに何のファイルを置くのか」が決まっておらず、ファイルを作る時や探す時に迷う時間が多かったため、機能を関心事としたシンプルなディレクトリ構成に移行しました。
詳細は以下の記事をご覧ください。

外部ライブラリの整理

使用している外部ライブラリは開発が開始された2020年12月頃にインストールされたバージョンのままであり、その結果
  • 外部ライブラリの新機能が使えない
  • 新しいライブラリが使えない
  • ライブラリのバグがそのままになっている
  • 情報が古いため技術的な調査に時間がかかる
  • 脆弱性が発覚した際の対応に時間がかかる
などの課題があり、開発速度の低下につながっていました。
結果的には
  • React16からReact18へのアップデート
  • webpack4からviteへの移行
  • ReduxをswrとuseContextに移行
  • 不要なライブラリの削除
  • その他のライブラリのバージョンアップ
などを行い、直近のリリース時点では47/51の外部ライブラリのバージョンが最新になっています。
詳細は以下の記事をご覧ください。

State管理手法の見直し

リファクタリング前は、Local State以外のstateはredux, redux-sagaで管理しており
  • 新しいstateを定義する時の手間が多い
  • キャッシュがされていない
  • 関連ライブラリが多くメンテナンスコストがかかる
などの課題がありました。
そのためよしこさんの記事を参考に、stateを3種類に分け管理手法を以下のように決めてreduxの全てのstateを移行しました。
  • Server Cache(API で取得したデータのstate): swr
  • Global State(複数のページ間で共有するstate): useContext
  • Local State(1 つのページ内に収まるstate): useState
swrのcacheのmutationはv2.0.0で新しく登場したを採用しており、ここらへんの設計話もいずれ別の記事にてご紹介できればと思います。

Global State管理ライブラリを使わずにuseContextを採用した理由

サービスの性質上stateのほとんどはServer CacheかLocal Stateであり、現在もGlobal Stateはほとんどありません。
そのため今はuseContextを使い、今後機能数が増えGlobal Stateの種類が増えてきたタイミングで、用途に合った適切なGlobal State管理ライブラリに乗り換えようと考えています。

Component設計の見直し

リファクタリング前は特にComponent設計が決まっておらず、Componentの粒度がばらばらで1つのComponentが肥大化していたり、共通の概念が複数箇所で実装されていたりと秩序がない状態でした。
そして一番の課題は、基準がないため各実装者がどうやって書くのが良いかが分からなくなっていることです。
そのためリファクタリングを進めながらComponent設計の指針を少しずつ決めていきました。
本記事では概要だけをご紹介し、詳細は後日別の記事でご紹介できればと思います。

Reactの思想に則った実装

全体を通して基本はReactの思想に則った公式ドキュメント通りの実装をしています。
これはReactという大きな技術の上で開発をしており、その技術の思想に則った実装が高い開発効率につながると考えているためです。
Reactに限らず思想に則って正しい使い方で実装して開発効率が出ない場合は、目的に合っていない技術を採用している兆候かもしれません。
Reactの場合は宣言性とComponent指向が最上流の思想のため、その2つを抑えて実装し「Lifting State UP」や「不要なuseEffectを避ける」などの具体的な部分に関しても公式ドキュメントに則った実装をしています。

propsとchildrenを適切に使い分ける

これまでchildrenが使われる機会が少なく、親Componentから孫Componentにデータを渡したい時に、子Componentがそのデータに依存していなくても親→子→孫とpropsをバケツリレーさせてしまい、子Componentが不要な依存を持ってしまうことがよくありました。
そのため
  • 子Componentが親Componentに依存しているのであればpropsを使う
  • 依存していないのであればchildrenを使う
と適切に使い分ける基準を決めることで、Componentが不要な依存を持たない設計をしています。

Custom Hook

これまではCustom Hookを使ってこなかったのですが、リファクタリング後は積極的にCustom Hookで責任ごとにStateとLogicを切り出しています。

Presentational/Containerパターン(PCパターン)

PCパターンは1つのComponentのViewとLogicを分けて実装する設計パターンです。
Custom Hookが実装されてからは採用されるケースが減ってきましたが、ViewとLogicを分けることで適切な責任範囲でComponentを作りやすいと考え、複数のLogicやStateを持つComponentで採用しています。

全体感の見えるComponent設計

Componentを単一責任に分けるのは大切という前提の上で、不必要にComponentを分けてネストさせるのではなく、Componentファイルを見た時の全体感の掴みやすさも重要視しています。

Hygenを使った雛形ファイルの作成

これらの設計のキャッチアップコストを下げるため、Hygenを導入しComponentやCustomHookの雛形をCLIから生成できるようにしました。
HygenはCLIの柔軟性が高いため、コマンドの数を増やさず質問文を工夫して色んなパターンの雛形が作れる点が気に入っています。
Hygenを導入した結果副次的な効果として、特に規約として決めなくても自然とComponentやHookの命名や書き方が揃っていったのもよかったです。

ESLintの最適化

リファクタリング以前はextendsにが設定されているのみだったので、主要なextendsを追加しrulesを適切に設定しました。
リンターを細かく設定すると、コードの書き方に関するレビューの指摘がなくなりレビュワーとレビュイーの両方の負担が解消されるため、レビューのコメントでリンターで解決できそうなものがあれば積極的に調べて導入するようにしています。
以下は使ってみて便利だったRuleの一部です。

便利なRule