🐭

【React/Vue.js】コンポーネント設計の(個人的)ベストプラクティス | Offers Tech Blog

2022/05/23に公開
1

概要

こんにちは、Offers を運営している株式会社 overflow の Software Engineer(主戦場はフロントエンド)の Kazuya です。今回は、ReactVue.js などの SPA フレームワークにおけるコンポーネント設計について紹介します。

昨今のフロントエンド開発では、コンポーネント指向での開発がスタンダート化しつつありますが、コンポーネント設計には厳格なルールが無く、どのように設計すればいいか悩む方も多いのではないでしょうか?(筆者は沼にはまりました)

コンポーネントの単位はどの程度に分割すべきなのか、状態管理はどうすればいいのか、API 通信はどこですべきなのかなど、一言にコンポーネント設計と言っても考えるべき項目が多いです。チーム開発では、認識があっていないとコードが魔境になることもしばしばあると思います。(筆者の経験談より)

そこで今回は、数々の新規プロダクト開発を経験して得た現段階でのベストプラクティスをベースに、設計例やディレクトリ構造なども紹介していますので、ぜひ参考にしてもらえればと思います。

[AD] Offers MGR(オファーズマネージャー)

本記事で紹介する方法は、筆者が担当しているプロダクトである「Offers MGR(オファーズマネージャー) 」で活用されています。「Four Keys分析」や「サイクルタイム分析」など開発組織の生産性を最大化するために必要となる指標を可視化させることができます。開発組織の健全性・生産性を中長期的に改善していきたい方はぜひお問い合わせください!

https://offers-mgr.com/
https://zenn.dev/offersmgr

はじめに

前述した通り、本記事では新規開発におけるコンポーネント設計の個人的ベストプラクティスをベースに紹介します。チームメンバーのスキルアセット、要件定義など様々な要因で本記事で紹介する内容とマッチしない場合があります。今回は設計の一例であることをご理解の上、参考にしていただけると幸いです。

コンポーネント指向とは?

コンポーネント指向とは、「アプリケーション開発において、分割した部品(コンポーネント)を組み合わせて開発する考え方」です。イメージ的には、LEGO とかを思い浮かべてもらうと分かりやすいと思います。様々なパーツを組み合わせて大きなものを作っていく感じです。また、コンポーネント指向は、後述する「分割統治法」と「単一責任の原則」の考え方がベースに含まれています。

分割統治法

分割統治法は、「大きな課題を小さく分割することで課題解決をしていくという考え方」で、開発的な表現にすると「責務毎に分割したコードの組み合わせでアプリケーション開発する」と言ったところでしょうか。これ自体は、コンポーネント指向ではない言語でも取り入れらている考え方なので、理解しやすいと思います。

単一責任の原則

次に単一責任の原則ですが、関数やクラスなどの「モジュールが持つ責務は一つにすべきという考え方」で、責務を限定化させることでメンテナンス性を担保できます。

React では、「ひとつのコンポーネントは理想的にはひとつのことだけをするべきだということ」と定義されています。できるだけコンポーネントを分割して、肥大化は避けましょうという考え方です。これはコンポーネント設計では、かなり大事な考え方になるので、常に頭の片隅に置いておくと良いと思います。

コンポーネント設計において意識しておくべきこと

コンポーネント毎の責務を明確にする

1 つのコンポーネントが、通信、ロジック、UI など複数の責務を持っていると、再利用性がないコンポーネントになってしまうことに加え、機能追加等の拡張もしにくい状態になってしまいます。
コンポーネント毎の責務を明確にして、前述した「単一責任の原則」に基づいた設計にすることで、影響範囲を限定化させることができるため、保守性と再利用性を担保できます。

設計はデザイナーと協業で行う

コンポーネント設計は、デザイナーがモックを作る段階で始まっていると言っても過言ではありません。コンポーネントの単位やデザインルールなどコンポーネントを設計にするにあたって決めておくべきことがあり、これらはモックを作成する段階でデザイナーとエンジニアで認識を合わせておく必要があります。認識合わせができていないと、再利用性できないコンポーネントが多く爆誕したりするので、早い段階でデザイナーとの協業できるようにしておくと良いと思います。

ディレクトリ構造

 L components/
   L parts/
   L templates/
   L views/
   L pages/

各レイヤーの説明

各レイヤーのサンプル画像

Parts

parts ディレクトリに格納するのは「ロジックが無い UI コンポーネント」です。この構成における最小粒度のコンポーネントで他のコンポーネントには依存していません。スタイルを伴う汎用的なコンポーネントなので、ロジック等は内包しません。

Templates

templates ディレクトリに格納するのは「コンポーネントを組み合わせることを目的としたコンポーネント」です。parts レイヤーと templates レイヤーに依存しており、部分的に切り出した範囲内のレイアウトを決めて、コンポーネント毎に依存するロジックもこちらで処理します。

Views

views ディレクトリに格納するのは「テンプレートにデータを渡すことを目的としたコンポーネント」です。templates レイヤーに依存しており、API 通信などから取得したデータをそれぞれのテンプレートに割り振ることを責務にしています。

Pages

views ディレクトリに格納するのは「ページの表示を目的にしたコンポーネント」です。views レイヤーに依存しており、ページのタイトルやディスクリプションなどのメタ情報の設定も責務に含まれています。

各レイヤーにおける責務の早見表

API 通信 グローバル State Style 依存関係
Parts × × parts
Templates parts, templates
Views ○(client) parts, templates
Pages ○(ssr) × × views

Atomic Design を採用を見送っている理由

コンポーネント設計を調べていると必ずと言ってもいいぐらい登場する「Atomic Design」ですが、いずれも運用面で課題がでてきてしまい、なかなか上手くいきませんでした。そこで、Atomic Design の考え方をインスパイアして個人的な解釈で取り扱いやすいものにしました。Atomic Design の考え方はコンポーネント指向に準拠している感じなのですが、思想通りの設計は茨の道だと思います。(理論上は最強)

状態管理と通信

状態管理について

ReactVue.js では、状態管理の概念が存在しており、これもコンポーネント設計をする際に考慮すべき要素になります。状態管理と急に言われてもなんのことって?なると思うので、もう少し簡単に表すと「コンポーネントが動作する際に変化する値」と言ったところでしょうか。例にあげると Input タグにおける Value などが挙げられます。

コンポーネント設計では、「コンポーネントがどのような状態を持つべきか」が重要になってきます。どのようにデータを管理して使い回していくか考えるのですが、データの流れを一方向に限定にするか、ReduxVuex などを活用してどこからでもアクセスできるようにするのか選択していくことになると思います。

個人的には、データの流れは限定化するほうを推奨しています。その理由ですが、データの流れを限定化させることで、保守性に優れた状態管理を実現できるためです。確かに Redux や Vuex などは便利ですが、データの動きが見えづらいため、デバックがしずらく依存度の高さから引き剥がしが難しいという問題があります。

通信について

アプリケーション開発では、API からデータを取得して描写すると思います。コンポーネント設計では、どこで API と通信してデータを取得するのか検討する必要があります。なにも考えずにコンポーネント内に組み込むというのは、設計思想的に良くありません。前述で解説していますが、「単一責任の原則」に基づくのであれば、通信をするためのコンポーネントを定義すべきということになります。
また、コンポーネントの分離以外にも通信メソッドを共通化させるなどの工夫をすることで、より堅牢なアプリケーションの構築を実現できると思います。

具体的な設計例

各レイヤーの説明画像

【赤】Parts

赤枠の部分が、Parts レイヤーのコンポーネントです。全部やると見づらくなってしまうため、一部のみ表示させています。ボタンやヘッダーなどコンポーネントとしての粒度は比較的小さめです。ヘッダーとバナーはテンプレートと悩ましいところですが、個人的には Parts レイヤーにしています。判断基準ですが、コンポーネントが過度に UI の表示以外の責務を持っていないかで判断しています。(言語化が難しい、、、)

【青】Templates

青枠の部分が、Templates レイヤーのコンポーネントです。画像だと中央のカードを囲っているコンポーネントです。カードのコンポーネントを組み合わせて表示させることを責務にしています。今回は存在していませんが、仮にこの部分にフィルターとかがあった場合は、ここでロジックも組み込みます。

【緑】Views

緑枠の部分が、viewss レイヤーのコンポーネントで、前述のコンポーネントを結合してページの構成をしています。API をクライアントサイドで呼び出す場合は、このレイヤーで通信してデータを各コンポーネントに割り振ります。

よくある質問(FAQ)

CSS フレームワークは使ったほうが良いですか?

CSS フレームワークは、工数の削減やデザインの一貫性の担保できるというメリットがありますが、デザイナーとの連携が必須になります。デザイナー側もフレームワークで表現できる UI にしないと恩恵を受けられません。また、一度フレームワークに依存したデザインを別のデザインに変更しようとすると、かなり工数が掛かるため、導入は中長期的な視点で判断すると良いと思います。

CSS 変数で定義したほうが良いものはありますか?

個人的には、margin や padding などのスペースサイト内で取り扱う色あたりは初期段階で定義しておくとよいと思います。これらは使用頻度が高いことに加えて、見た目に対する影響が大きいため、開発初期で定義しておくとコンポーネントに一貫性を持たせることができます。

デザインのフィードバックはどのようにしていますか?

ここ 1 年ぐらいは、StoryBook を用いてコンポーネント単位でデザインフィードバックをもらっています。詳しくは別記事でまとめる予定のため、割愛しますが、コンポーネントの Props を自由に変更できるため、想定外のデザイン崩れなどもこの段階で確認できます。UI とロジックがしっかりと分離できていれば、ページを表示させることも可能です。

設計に関する認識合わせはどのようにしていますか?

筆者の経験上、新規開発は携わるメンバーが限られるため、認識合わせの座談会的なものをおこなって認識を合わせています。また、その場でメンバーのフロントエンドに関するスキルがどのくらいなのか見極めて、実際に採用するコンポーネント設計を検討しています。(スキルやメンバーに応じて無理のない感じに調整するイメージ)

まとめ

筆者はデカルトの「困難は分割せよ」という名言がコンポーネント設計においても重要だと考えています。これはコンポーネント指向における「単一責任の原則」に通じる考えであり、設計/開発をしていく中で運用や更新が困難になりそうであれば、分割していくことで保守的且つ可読性の高いコードになります。

コンポーネント設計における正解は、なかなか見つけづらいと思います。
ただ、ルールを設けずにハードコーディングしてしまうと逆に複雑になってしまい、負の遺産になってしまいます。(筆者への戒め)

本記事で紹介した内容が、少しでも参考になれば幸いです。少々長くなりましたが、本記事を最後まで読んで頂き、ありがとうございました。

更新履歴

  • コンポーネント設計のデータソースについて追記(2022/05/23)

関連記事

https://zenn.dev/offers/articles/20220418-what-is-bff-architecture
https://zenn.dev/offers/articles/20220404-motivation-and-impressions-changing-jobs
https://zenn.dev/offers/articles/20220322-hello-offers-tech-blog

Offers Tech Blog

Discussion

菊谷 知真菊谷 知真

失礼します。こちらの記事、設計の参考にさせていただこうと思いました。

質問なのですが
サムネイルとタイトルが表示されるpartsのコンポーネントがあったとして、
サムネのクリックイベント、タイトルのクリックイベントそれぞれが必要な場合、イベント処理はTemplatesに書くということでしょうか?
その場合、セレクタでpartsコンポーネントの「サムネイル」「タイトル」の要素を指定してあげる必要があると思いますが
idやclassの文字列をベタ書きするしかないでしょうか?partsと密結合になってしまうと思うのですが、適切な記述はどのようにしたらいいのかご教示頂けますと幸いです。