こんにちは、ギフティでエンジニアをしている(@yashi848484)です。先日、ふと思い立って持っている積みゲーの数を数えたら 2 ケタあることが判明し、数えなければよかったと後悔しています。やっていきます。
フロントエンドのコンポーネント分割でよく陥る問題点
特定の技術スタックに寄らず、フロントエンドのコンポーネント分割に関して、以下のような課題に出くわします。
- (a) 多くのページで使われる共通の UI パーツがたくさんあり、それらをどう整理するかが悩ましい
- (b) 特定のページでしか使われなさそうに思えるが、同じ表示の繰り返しがあるためコンポーネント化しておきたくなる
- (c) ある部分をコンポーネント化しておいたら、それが内包する一部分のみ変化した亜種が後から必要になった
これらの事象を、現在用いている技術スタックを考慮しながらうまくコンポーネントを整理して組み立てるのはとても難しいと感じています。銀の弾丸は無いと思いますが、それぞれの課題にどう向き合うか、個人的な考えを簡単にまとめてみたいと思います。
(a) 共通コンポーネント群の整理について
それなりに大きな規模のプロダクトにならなければ、共通コンポーネントを 1 ディレクトリにまとめておくだけで、意外と困らないことも多いなとここ最近感じています。つまり、実は先送りにしてもよい課題である可能性があります。規模が大きいプロダクトか、大きくなることが想定されるプロダクトであると分かって初めて検討の必要性が出てくる課題かと思います。
HTML element に似通ったパーツが多く存在するような場合には、Material UI 等の UI コンポーネントライブラリを参考にディレクトリを切って整理する手立てがありそうです。
(b) 一つのページ内における同じ表示の繰り返しについて
デザインの 4 原則「近接・整列・反復・対比」というものがあります。https://docodoor.co.jp/staffblog/design-4general-rule/
これに則ったデザインは、意図的に似たような表示があって整理されているため、エンジニア目線ではコンポーネント化したくなりがちです。分割したコンポーネントが結局特定のページでしか使われず、開発に必要な手数だけが増え、過度な DRY になってしまうことも少なくありません。一方、 YAGNI を唱え分割しないままにしておく手立てもありそうですが、分割しない場合の課題は、見た目とセットになっているロジックも肥大化してしまうことだと思います。
いきなりプロダクト全体における共通化までは考えなくとも、ひとまず「そのページ専用のパーツ」としてコンポーネント化しておくことから始めると、過度な DRY にならずに済むことが多いと感じています。
(c) 既存コンポーネントの亜種が必要になったときについて
ほとんどの場合、コンポーネントのプロパティを増やすか、子コンポーネントを作るかで対応できるかと思います。もしくは今までコンポーネント化してこなかったパーツを新たにコンポーネント化する必要が見えてきた、と言える状況もありそうです。
また、ページのデザインを見て、似通っているものの微妙に異なるパーツを見つけたときには、デザインの意図をまず汲み取り、よりはっきり差別化したほうが良いパーツなのか、共通化したほうが良いのかをデザインチームと一緒に考えられるとベストだと思います。
全体的な方針を考える
よく挙げられるのは Atomic Design に則る方法です。しかし、Atomic Design は UI デザインの方法論であり、ロジックの分割に関しては語ってくれない点に注意が必要だと思います。エンジニアが言う「コンポーネント」とは「見た目とロジックが合わさった一つの部品」を指すことが多く、デザインだけを考慮しても片手落ちになってしまいます。他社の事例を調べてみると様々な苦労が伺えます。
- Atomic Design を採用しうまくいかなかった例 https://tech.connehito.com/entry/learn-and-failure-atomic-design
- Atomic Design をベースに拡張した結果必要なくなった例 https://note.com/tabelog_frontend/n/n07b4077f5cf3
過去に私が関わったプロダクトでは Atomic Design などに則らず、独自で考えて構築しました。しかし、プロダクト立ち上げ後のエンハンスでそれまで考慮できていなかったパターンが発生した結果、フロントエンド全体に影響の出るディレクトリ変更を行う必要に迫られ、方針を変更しづらかったり、変更後の方針に則っている部分とそうでない部分が混在する中途半端な状態になってしまったりという経験があります。
Atomic Design などに則る場合、新規参入者が構造を理解するハードルを軽減できることや、開発者同士で足並みを揃えやすいなどといったメリットがあると思います。もちろん前述の通り、Atomic Design がカバーできていないところを考慮しながら、必要に応じてアレンジしていくことが求められます。アレンジの方法としては、5 段階の分割を 3 段階に減らしたり、新たな粒度を設けたり、サブディレクトリを切ったりと様々なアプローチがあるようです(上で挙げた記事にも構成が載っていますね)。
SvelteKit を使う場合の方針を考えてみる
開発中のプロダクトで、Svelte を使ってフロントエンドを構築することにチャレンジしています。このケースにおけるコンポーネント分割について考えてみました。
ディレクトリ構成
コンポーネント分割の方針は、採用する技術スタックに応じて判断が変わってくると思います。特に最近のフロントエンドフレームワークはディレクトリベースでルーティングを行うものも多く、自分たちでは変えられない部分が出てくることも多いからです。
今回対象となるプロダクトでは SvelteKit を用いる予定なため、変更できない部分もありますが大枠として以下のような構成を考えました。
src ├── app.html ├── lib │ └── components │ └── Button.svelte └── routes ├── (contextA) │ ├── (authed) │ │ ├── +layout.svelte │ │ ├── +layout.server.ts │ │ └── hoge │ │ └── +page.svelte │ └── (unauthed) │ └── +page.svelte └── (contextB) ├── (authed) └── (unauthed)
今回のプロダクトでは、利用コンテキストの異なる複数のページ群を作成する可能性があるため、layout group を使ってそれぞれでレイアウトを変更可能にしつつ、各コンテキストを別ディレクトリに分離して管理できるようにしようと目論んでいます。また、認証前(unauthed)と認証後(authed)とでさらに layout group を作って分割しています。これは、認証前後でレイアウトが異なる可能性だけでなく、認証状態のチェックを各ページ共通で行う層を設けておきたいからです。+layout.svelte
や +layout.server.ts
がそれらを担うファイルです。
SvelteKit を用いる場合、Atomic Design における Pages に相当するコンポーネントに関してはフレームワーク側で決定され、自分たちで考える余地がありません。そのためページコンポーネント以外のコンポーネント分割については、以下の点に絞って考えてみたいと思います。
- (A) 共通コンポーネント群の整理
- (B) 一つのページ内における同じ表示の繰り返し
(A) 共通コンポーネント群の整理について
awesome-sveltekit の例を見てみると、 Svelte の公式ドキュメントを始めとしていくつかは $lib/components
にコンポーネントをまとめる構成になっていました。まずは同じディレクトリ構成を真似するところから始めてみようと思いました。ちなみに $lib は SvelteKit で予め用意されているエイリアスです。
さらに、$lib/components
内における整理手法として以下のようなパターンを考えました。
- 特にサブディレクトリを切らず、全てフラットに置く
- 特定コンポーネントの子コンポーネントはサブディレクトリに置く
- Atomic Design に則る
今回対象のプロダクトはそこまで規模が大きくなる想定ではないことから、共通コンポーネントに関してはまずはフラットに置くことにしようとしています。開発が進むにつれてコンポーネントの数が増え、管理しづらくなってきたら再度検討する予定です。
(B) 一つのページ内における同じ表示の繰り返しについて
SvelteKit では src/routes
配下のディレクトリ構成がそのままルーティングになります。以下のようなパターンを考えました。
- ページコンポーネントの一部分を分割しない
- layout group を使う
hoge/+page.svelte
に対して、(hoge)/Fuga.svelte
もしくはhoge/(components)/Fuga.svelte
のような構成をとる
- 共通コンポーネントと同じディレクトリに置く
$lib/components/routes
を作り、その中にsrc/routes
配下と同等のディレクトリツリーを作り、各ページに対応するディレクトリにページ特有のコンポーネントを置いていく- 例えば
src/routes/hoge/+page.svelte
に対して、$lib/components/routes/hoge/Fuga.svelte
を作る
- Atomic Design に則る
- molecules ないしは organisms ぐらいの粒度で、実際にはある 1 ページでしか使われていなかったとしても、他のページでも流用可能なコンポーネントとして作り切る
今回は、こちらも共通コンポーネントと同じディレクトリに置く方針としました。しかし、ルーティングと同等のディレクトリツリーを作るとコンポーネントの一覧性が下がるという意見がありました。例えば hoge/+page.svelte
で Fuga.svelte
が作られており、後から piyo/+page.svelte
を作るときに Fuga.svelte
が流用できることに気づきにくい、といったことが考えられます。結果、共通コンポーネントと同じディレクトリに置く方針を取りつつ、その中に src/routes
配下と同等のディレクトリツリーを作ることまではしないことにして最初は進めようとしています。
おわりに
コンポーネント分割に関する過去の経験や一般的な方法論をベースに、現時点での私なりの考えを書いてみました。今回は Svelte 利用時のケースを挙げましたが、今後も様々な技術やプロダクトに関わりながら設計力を磨いていきたいです。実はこの記事で挙げたこと以外にも、弊社では各チームがそれぞれフロントエンドの設計と日々向き合っています。
ギフティでは、フロントエンドのコンポーネント分割を一緒に考えていきたい人を 絶賛募集中 です!