Atomic Designをやめてディレクトリ構造を見直した話
こんにちは。フロントエンドチームの金野と申します。
食べログでは現在、React+TypeScriptでフロントエンドのリプレースを進めています。
以前の記事で、食べログではAtomic Designをどのように取り入れているかの紹介をしました。
しかし、最近のリプレース作業では、Atomic Designとは異なるディレクトリ構造を採用しています。
今回の記事では、「なぜAtomic Designをやめたのか」という理由と、「どのようなディレクトリ構造にしたのか」を紹介します。
Atomic Designを導入したねらいと導入した結果
上記の記事で言及した通り、当初Atomic Designを導入したねらいは以下になります。
1. コンポーネントの責務がより明確になる
2. 見た目の粒度だけでなく、ロジックの責務も明確にできる
3. 「ドメインが入るか/入らないか」。「抽象的か/そうでないか」の区別が明確になる
4. 世間的にも浸透している概念のため、デザイナー・エンジニア間の共通言語を作れる
導入後はねらい通り、これら4つは実現できております!
Storybook上で汎用的に使えるUIを探しやすくなったり、設計・実装する際の迷いも少なくなりました。
Atomic Designの原典となる記事では、インターフェースをAtoms, Molecules, Organisms, Templates, Pagesという5つのレベルに分類することを提言していますが、いざ分類するとなると迷いがちであったり、現場ごとにルールを決めて運用するという話をよく聞きます。
食べログでも例にもれず、分類方法やロジックの責務については、以下のようにアレンジしたルールを定義しました。
Atoms:
・汎用的な機能を提供する。
・ドメインが入ってはいけない。
・Contextへのアクセスはしない。
・自分自身で状態はなるべく持たない。
・他のコンポーネントに依存していなければAtoms。
Molecules:
・汎用的な機能を提供する。
・ドメインが入ってはいけない。
・Contextへのアクセスはしない。
・自分自身で状態はなるべく持たない。
・他のAtomsやMoleculesのコンポーネントに依存している。
Organisms:
・ドメインが入ったらOrganisms。
・他に依存するコンポーネントがなかったとしても、ドメインが入った時点でOrganismsにする。
・useContextによるContext接続可。
・その機能のためのAPIを叩くのはここ。
Templates:
・部分導入した範囲内のレイアウトを決める。
・ロジックは持たない。
Pages:
・現在はただのラッパーに近い。
この記事での「ドメイン」「依存」は以下の意味で使用しています。
「ドメインが入っている」とは?
・特定のコンテンツ・コンテキストじゃないと使えない状態。
「他のコンポーネントに依存」とは?
・「他のコンポーネントがないと成り立たない」状態。
・つまり、別のコンポーネントをimportしている状態。
詳しくは食べログでのAtomic Design 〜どう分類しているか編〜もぜひ御覧ください。
このようにガイドラインを定めたことで、「これはAtoms?Molecules?」などと分類に迷うことはほとんどなくなりました。
また、汎用的なコンポーネントである「Atoms/Molecules」と、特定のコンテンツ・コンテキストじゃないと使えないコンポーネントである「Organisms」を明確に分類することで、どこでデータ取得やContextのアクセスを行うかも判断しやすくなりました。
Atoms/MoleculesはUIライブラリに載せるべき汎用的なコンポーネントであるため、デザイナーさんが使用するデザインシステムもAtomic Designに基づいて分類され見やすくなりました。
どうしてAtomic Designをやめたのか
「Atomic Design、導入して良いことばかりじゃないか!」と思われるかもしれません。実際良いことばかりでした。
ただ、Atomic Design導入のねらいの一つであった「2. 見た目の粒度だけでなく、ロジックの責務も明確にできる」に関しては、Atomic Designのオリジナルルールを独自にアレンジして実現した部分になります。
Atomic Designの原典では、インターフェースを機能や見た目の粒度に基づいて5つのレベルに分類することを提言していますが、データ取得をどのレベルで行うかなど、ロジックをどう扱うかについては言及されていません。
また、独自のルールを規定したことで、Atoms/Molecules/Organisms/Templates/Pagesの分類に迷うことはなくなりましたが、「ここまで細かく分ける必要ないのでは?」ということに気づいたのが最大の理由です。
「Atoms/Moleculesなどの汎用的・抽象的なコンポーネント」と「特定の文脈・コンテンツでしか使えないコンポーネントであるOrganisms」を分けることで得られたメリットは大きいですが、「Atoms」と「Molecules」を分けることで得られたメリットが特にありませんでした。
更に、Pagesという層も食べログでは余分なものとなっていました。
食べログではページ単位ではなく、コンポーネントごとにReactへのリプレースを進めているという事情があります。
どのようにReactを部分導入し、リプレースを進めているかは以下の記事もぜひ御覧ください。
そのため、食べログにおけるAtomic DesignのPages層はページ全体ではなく、特定の機能のみが内包されているケースがほとんどです。
よって、現時点ではただTemplateをラップするだけの役割になっており、なくても問題ない層となっていました。
このようにオリジナルのルールを拡張しつつも、当初は「Atomic Designのセオリーに従ってAtoms/Molecules/Organisms/Templates/Pagesという5つの層は保っておこう」ということで導入しましたが、もはや5つの分類にこだわらなくても問題ないことがわかってきました。
また、Atomic Designを辞めることで「4. 世間的にも浸透している概念のため、デザイナー・エンジニア間の共通言語を作れる」という恩恵がなくなってしまうのでは?という懸念も挙がりましたが、
UIデザイナーさんとコンポーネント設計についてすり合わせる際は、
「汎用的か」
「特定のコンテキストでしかつかえないものか」
「特定のページのみでしか登場しないものか」
という観点の確認はしますが、Atomic Designのどの層に該当するか、といった実装上の具体的な話まですることはこれまでほとんどありませんでした。
したがって、エンジニアやデザイナー間でコンポーネントの責任範囲を決める際は、Atomic Designという言葉を使わなくても、
「汎用的か」
「特定のコンテキストでしかつかえないものか」
「特定のページのみでしか登場しないものか」
という先程の観点のみ気にしておけば問題ないのではと考えました。
新しいプロジェクト作成を機にディレクトリ構造を見直し
これまではPC版食べログの一部コンポーネントのリプレースを進めていましたが、Smartphone版にも着手することになりました。
食べログのWebサイトはレスポンシブではなくPCとSPを別のRailsアプリで開発しているため、フロントエンドもPCとは別のReactプロジェクト、別のリポジトリで開発することにしました。
イチからReactプロジェクトを新しく作成するため、ディレクトリ構造もこれまでのPC版から見直す良い機会となりました。
Atomic Design使用時のディレクトリ構造
Atomic Designを採用していたときの従来のプロジェクトのディレクトリ構成は以下のようになっていました。
.
├── assets
├── src
│ ├── main
│ │ ├── entrypoints # 1
│ │ │ └── [Rails Subsystem Name]
│ │ │ └── [Rails Controller Name]
│ │ │ └── [Rails Action Name].tsx
│ │ ├── components
│ │ │ ├── mountUnits # 2
│ │ │ │ └── [PageName].tsx
│ │ │ ├── pages # 3
│ │ │ │ └── [Component Name]/
│ │ │ ├── templates # 4
│ │ │ │ └── [Component Name]/
│ │ │ ├── organisms # 5
│ │ │ │ └── [Component Name]
│ │ │ ├── molecules # 6
│ │ │ │ └── [Component Name]
│ │ │ │ ├── index.tsx
│ │ │ │ ├── presenter.tsx
│ │ │ │ └── style.tsx
│ │ │ ├── atoms # 7
│ │ │ │ └── [Component Name]/
│ │ │ └── provider
│ │ ├── ducks
│ │ ├── hooks
│ │ ├── styles
│ │ └── utils
│ └── stories
│ ├── components
│ │ ├── atoms
│ │ │ └── [Component Name]
│ │ │ └── index.stories.tsx
│ │ ├── molecules
│ │ ├── organisms
│ │ └── templates
│ └── utils
├── tests
│ ├── main
│ │ ├── components
│ │ │ ├── atoms
│ │ │ │ └── [Component Name]
│ │ │ │ └── index.test.tsx
│ │ │ ├── molecules
│ │ │ ├── organisms
│ │ │ ├── pages
│ │ │ ├── provider
│ │ │ └── templates
│ │ ├── ducks
│ │ ├── hooks
│ │ └── utils
│ ├── stories
│ └── utils
└── types
src/main/components配下にはAtomic Designの各層ごとにディレクトリを掘り、その中にコンポーネント単位でディレクトリを切っています。
他にもプロジェクト内には、マウントする一番ルートのコンポーネントを格納するmountUnitsディレクトリや、RailsのViewに対応したファイルをまとめるentrypointsディレクトリが存在します。
各ディレクトリの説明は以下になります。
#1: entrypoints
ページごとに作成するファイルです。Ruby on Railsの Controller-Actionと1対1になるように作成します。
ReactDOM.render()を呼び出しReact要素を呼び出すのはこのレイヤーになります。マウント先であるRailsの世界との橋渡しをする役割です。
# 2: mountUnits
entrypointsのReactDOM.render()の第一引数に指定するコンポーネントはこちらになります。
Reactをページ上に部分導入しているため、ReactDOM.createPortal()をこのレイヤーで呼び出しています。
# 3: pages
ReactDOM.createPortal()の第一引数に指定するコンポーネントです。
Atomic Designのpages層に相当しますが、ただtemplatesを呼び出すだけのwrapperとなっています。
# 4: templates
Atomic Designのtemplates層に相当します。
部分導入した範囲内のレイアウトを決め、ロジックは持たません。
# 5: organisms
Atomic Designのorganisms層に相当します。サービスとして意味のある単位の塊となり、Contextへの接続や非同期通信もこの層なら可能です。
# 6: molecules
Atomic Designのmolecules層に相当します。汎用的に使用できるコンポーネントですが、別のatomsもしくはmolecules層に依存しています。
# 7: atoms
Atomic Designのatoms層に相当します。汎用的に使用できるコンポーネントで、他のコンポーネントには依存していません。
各コンポーネントディレクトリ内のファイル構成
Atomic Designの層の各ディレクトリ内にはコンポーネントごとにディレクトリを配置し、その中のファイルは以下のような構成にしていました。
また、開発時にしか使用しないStorybook用のファイルを<RootDir>/stories/配下、Unit test用のファイルを<RootDir>/tests/配下にし、デプロイ時に必要なプロダクトコードとは完全にわけて管理していたのも特徴でした。
utilityをまとめるフォルダ内が「プロダクトコード用」「Storybook用」「Unit test用」とごちゃまぜになってしまいそうだったためです。
新しいディレクトリ構成
一方で、新しく作成したSmartphone用のプロジェクトでは、Atomic Designを辞めただけでなく他の構造も大幅に見直し、以下のようなディレクトリ構成を採用しました。
├── app
│ ├── entrypoints # 1
│ │ └── [Rails Subsystem Name]
| │ └── [Rails Controller Name]
│ │ └── [Rails Action Name].tsx
│ ├── components
│ │ ├── pages
│ │ │ └── [PageName]
│ │ │ ├── index.tsx # 2
│ │ │ ├── index.test.tsx
│ │ │ ├── hooks.test.tsx
│ │ │ ├── hooks.ts
│ │ │ ├── presenter.test.tsx
│ │ │ ├── presenter.tsx
│ │ │ ├── portals/ # 3
│ │ │ └── [Component Name]/ # 4
│ │ ├── projects # 5
│ │ └── uiParts # 6
│ │ ├── [Component Name]
│ │ │ ├── index.stories.tsx
│ │ │ ├── index.tsx
│ │ │ ├── presenter.test-d.tsx
│ │ │ ├── presenter.test.tsx
│ │ │ ├── presenter.tsx
│ │ │ └── style.tsx
│ ├── contexts
│ ├── ducks
│ ├── hooks
│ ├── styles
│ ├── types
│ └── utils
変更した点を簡単にまとめると以下のようになります。
・汎用的なコンポーネントである旧atomsと旧moleculesをuiPartsにまとめた。
・単なるwrapperとなっていた旧pagesをtemplatesとまとめた。
・旧organismsを、「特定のページでしか使われないもの」と「ページを跨いで使われるもの」に分けた。
・特定のページでしか使われないコンポーネントは、該当のページのディレクトリ内に配置した。
・分散していたUnit test用のファイルやStorybook用のファイルを一つのコンポーネントディレクトリにまとめた。
では、それぞれ詳しく見てみましょう。
#1: entrypoints
旧entrypointsと同様、ページごとに作成するコンポーネントです。Ruby on Railsの Controller-Actionと1対1になるように作成し、マウント先であるRailsの世界との橋渡しをする役割を担います。
ReactDOM.render()を呼び出しReact要素を呼び出すのはこのレイヤーになります。
#2: pages/[Page Name]/index.tsx
旧mountUnitsです。entrypointsのReactDOM.render()の第一引数に指定するコンポーネントはこちらになります。
コンポーネントごとにReact化をしているため、ReactDOM.createPortal()をこのレイヤーで呼び出しています。
#3: pages/[Page Name]/portals
旧pages兼templatesにあたります。上位層のReactDOM.createPortal()の第一引数に指定するコンポーネントはこちらになります。
各portalは特定のpageでしか呼び出されないため、pages/[Page Name]/ディレクトリ内に配置し入れ子構造にしています。
#4: pages/[Page Name]/[Component Name]/
旧organismsのうち、特定のページでしか呼び出されないものはcomponents/pages/[Page Name]/内にコンポーネント名でディレクトリを切って配置します。
# 5: projects
旧organismsのうち、ページを跨いで呼び出される共通の機能をprojectsというディレクトリでまとめています。「様々なページで使われるが、汎用的ではなく特定の用途でしか使われない機能」であり、食べログでは、口コミを投稿するためのモーダルなどがあたります。
#6: uiParts
旧atomsと旧moleculesをまとめて、uiPartsというディレクトリに格納しています。
このレイヤーにいるものはデザインシステムのUIライブラリに載せるべき汎用的なコンポーネントであるため、この中のコンポーネントのみStorybookに掲載することにしました。
各コンポーネントディレクトリ内のファイル構成
各コンポーネントディレクトリのファイル構成は、以下のようになっています。
Unit testファイル、Storybookファイルを同じディレクトリにまとめただけでなく、hooks.tsを作成し、ロジックをカスタムフックとしてなるべく切り出すようにしました。
一つのディレクトリにまとめた理由としては、開発時のファイル作成を効率化するためと、作成漏れにすぐ気づけるようにするためです。
その代わり、utils/フォルダ内がカオスになるのを防ぐため、packageに応じたディレクトリを切ってそこに関連するutilityや設定ファイルを格納するようにしています。
├── app
│
// 略
│
├── .jest
│ ├── fileTransformer.js
│ └── setupTest.ts
├── .msw
│ ├── handler.ts
│ ├── importOpenAPISpec.ts
│ ├── loadOpenAPISpec.ts
│ ├── openAPISpec.ts
│ ├── openAPIUtils.ts
│ ├── server.ts
│ └── worker.ts
├── .storybook
│ ├── main.ts
│ ├── preview-head.html
│ └── preview.tsx
└── .webpack
├── HookToManifestPlugin.ts
└── PatchHtmlWebpackPluginPlugin.ts
変えてみてどうだったか
ディレクトリ構造を見直したおかげで、以下のメリットがありました。
・旧atomsと旧moleculesをuiPartsに、そして旧Pagesと旧Templatesをまとめたおかげで、余計な層が不要になり実装やUnit test作成のコストが削減できた。
・旧organismsを、「特定のページでしか使われないもの」と「ページを跨いで使われるもの」に分けたおかげで、各機能の影響範囲がわかりやすくなった。
・分散していたUnit test用のファイルやStorybook用のファイルを一つのコンポーネントディレクトリにまとめたことで、関連するファイルを一覧しやすくなり、作成場所をtypoなどで間違えるリスクもなくなった。
これらのメリットは、Atomic Designをやめたから得られたというより、必要なレイヤーと各コンポーネントの影響範囲を改めて見直し再検討したからだと思います。
一度Atomic Designを導入したことからこそ、「抽象度や依存の有無に応じてディレクトリをわける」「ドメインが入るか入らないかを意識してディレクトリをわける」「ロジックをどの層に実装するかを事前に決めておく」という部分を意識できるようになったため、結果的にはAtomic Designを一度経験してみてよかったと考えています。
最後に
現在、食べログではフロントエンドに関わるポジションとして以下の2つを募集しています。
・フロントエンド統括チームに所属するフロントエンドエンジニア
・フロントエンドをメインにサービス開発を担当していくWEBエンジニア
・アーキテクチャについて探求したい
・難しい課題にチーム一丸となって取り組みたい
・React/TypeScriptでバリバリ開発したい
・レガシーなシステムのリファクタリングがしたい
・食べログというプロダクトに貢献したい
・大規模なシステムの開発に携わりたい
・柔軟に働ける環境で自分のスキルを活かしたい
どれかに当てはまった方は以下のリンクも是非御覧ください!