🪨

Atomic Designでの失敗と、コンポーネント設計のトレンドから考える構成アイデア

2023/12/05に公開

まえがき

株式会社エアークローゼット 新卒3年目の小林です。
この記事はエアークローゼット Advent Calendar 2023 4日目の記事です。

先日、弊社からDisney FASHION CLOSET というディズニーキャラクターをモチーフとしたバウンドコーデを借りられるサービスがリリースされ、私はそのフロントエンド全体の設計・開発と、認証周りの開発を担当していました。
https://disney.air-closet.com/

今回はその開発時の失敗話を起点に、Reactエコシステムを絡めつつ、これからのコンポーネント設計アイデアについて書いていこうと思います。

失敗話: 巨大OrganismsとReact Suspense

どんな失敗だったのか

Disney FASHION CLOSET(以下DFC)のコンポーネント設計は、Atomic Designを採用しています。
理由はその概念の解説記事などがネットに多く出回っていてキャッチアップが容易なこと、何より自分が慣れていることが決め手でした。

それと同時に、リクエストキャッシュライブラリとしてTanstack Query(旧React Query)を採用。また、React Suspense機構を用いて局所Loadingやエラー境界を設け、ユーザビリティやエラーハンドリングの自由度を高めよう、と企みつつ設計していきました。

しかし、以下のような流れで各ページに大きなコンポーネントが発生。

  • UIをベースに設計を考えていった結果、見た目上の構造をコードにも持ち込んでしまい、データを流しづらいUI密結合な階層構造になってしまった
  • hooksでビジネスロジックごとに処理をまとめることにこだわった(これ自体は別に悪くない)
  • hooksでまとめた状態やロジックを一度organisms(=Atomic Design的には「状態を扱う層」)におろした結果、クソデカOrganismsが誕生(見通しの悪さからContainer/Presentationで分離した)
  • 最終的には各ページにデカいcontainer-デカいpresentation-molecules-atoms のようなクソデカ多層岩templateが1〜2つずつ爆誕

最終的に画面-ロジック間にメインで関わる部分は以下のようになりました(めっちゃ簡略化してます)。

src/
├─ components
│  ├─ templates
│  ├─ organisms
│  │  ├─ containers
│  │  │  ├─ CartXXX.tsx
│  │  │  ├─ OrderXXX.tsx
│  │  │  └─ OrderYYY.tsx
│  │  └─ presentations
│  │     ├─ CartImageCard.tsx
│  │     ├─ CartButton.tsx
│  │     ├─ OrderList.tsx
│  │     └─ OrderButton.tsx
│  ├─ molecules
│  └─ atoms
├─ service/ (←ビジネスドメインhooks層)
│  ├─ cart/
│  │  ├─ InitializeCart.ts
│  │  ├─ GetCart.ts
│  │  └─ SetCartItem.ts
│  └─ order/
│     └─ Rental
│        └─ XXXXX.ts
├─ pages/
│  ├─ pageA/
│  ├─ pageB/
.
.
.

Suspenseパターンとの相性×

そして、このように1つの大きな親コンポーネントにロジックを集約してから下ろすパターンがReact Suspenseと非常に相性が悪かった……。

Suspense

囲んだコンポーネント直下の非同期処理を受けてFallbackを返すSuspenseパターンは、必然的に非同期処理を行うコンポーネント以下も巻き込んでFallbackを表示してしまいます。
上図の状況であれば、一部のmoleculesでしか使わないデータのfetchによって、子コンポーネントすべてが再レンダリングされてしまいますね。
結果的に当初の目論見であったLoadingやエラー境界を仕込むことは叶わず、画面全体でそのような境界をもつ構造になりました。

  • ひとつの大きな多層コンポーネントで構成されたページが数多く生まれてしまった。
    • Atomic Designの考え方にハマりすぎた
    • UIを強く意識してしまい、データ構造がうまくハマらない構成になってしまった
  • 上記状態では親のfetchによるSuspendに子がすべて巻き込まれ、Fallbackの細かい出し分けなどSuspenseの有効な使い方が難しくなってしまった

Atomic Designの良し悪しについての議論は「Organismsが肥大化する」「OrganismsとMoleculesどちらに入れるか迷う」など、Organismsの扱い方について語られることが多い印象があります。
開発中もチームメンバーと何度も扱いを話し合い、「organismsの中にpresentation作ったらそれはもうmoleculesなのでは……?」と思いつつ整理のために導入したりするなど、基本的に悩みどころはOrganismsが中心でした。
逆にatoms, moleculesなどのコンポーネント層の再利用性の高さはやはり非常に魅力的、かつ世間的によく知られている分受け入れられやすいなと感じます。この利点を失わず、Organismsの部分になにかテコ入れができると良さそうです。

展望

流行の話

最近まで全然ついていけてなかったコンポーネント設計の流行りですが、↓の記事でめちゃくちゃ理解できました。
https://zenn.dev/mybest_dev/articles/c0570e67978673

個人的にはLayer型、Features型の混合が一番しっくりきていて、前半記事のまとめでも触れたようなLayer型(Atomic Design)とFeatures型のいいところを両取りできるような構成は、構成そのものの管理しやすさとどちらか一方からの移行しやすさなど含めてベストだと感じます。
また、どのようにミックスしていくかはサービスごとに異なるので、今後いろんな実践例が見れると面白いなと感じます。

Feature型と境界

コンポーネント設計に関する記事や登壇資料などをいくつか読んでみると、

  • 「関心事の分離」、とりわけUIと業務知識の分離を行うのは共通してみんなやっている
  • 業務知識層をどのようにどのように扱うかで差がある。Bulletproof-reactを始めとして最近は特にFeatures型が多い

↓の記事で解説されているところの「関心の単位」は、Suspenseを使った各種境界の境目としても機能しそうです。
https://zenn.dev/misuken/articles/bdd33790ed4cd0

各コンポーネントにおいて、状況的関心・サービス的関心のそれぞれで

  • 自分自身の機能を提供できたかの監視(非同期処理中のfallback)
  • 提供できなかった場合にはそれ以外の関心事に影響が出ないようにする(エラー境界)

を設けることで、コンポーネントの範囲がそのままサービスの各種機能の振る舞いの単位になっていくと思うし、新鮮で面白いなと感じます。

リクエストキャッシュライブラリを使ったアイデア

SWRやTanstack Queryなどのリクエストキャッシュライブラリを使うことで、取得効率の最適化はもちろん、APIからの取得結果を、再フェッチ負荷を気にせずにどこからでも呼び出すことも可能になります。

すごく雑な例ですが、

細かく分けてもフェッチが一回で済んでしまうため、好きなだけ分割して再利用性を高めることができる……気がしませんか?
さすがにこれはやりすぎだと思いますが、これにより上で紹介した各コンポーネントの関心事はより狭まり、取り回しが良くなりそうだなと思います。

おわりに

最後までご覧いただきありがとうございました!

エアークローゼット Advent Calendar 2023はまだまだ続きますので、ぜひ他のエンジニア、デザイナー、PMの記事もご覧いただければと思います。

また、エアークローゼットはエンジニア採用活動も行っておりますので、興味のある方はぜひご覧ください!

https://corp.air-closet.com/recruiting/developers/
https://www.wantedly.com/companies/airCloset/projects

Discussion