PLAIDでエンジニアをしている @kei-tamiya です。
本記事は PLAID Advent Calendar 2019 の 20日目になります。
社外で個人的に開発しているプロジェクトでAtomic Designを採用しており、ある程度知見が溜まったので、1つの運用例としてご紹介したいと思います。
前提
どんなプロジェクト?
- もともとVue.jsを用いて動いているアプリケーションを1からリファクタするプロジェクト
- 別リポジトリに、Nuxt.js, Typescript, Vue Apolloを使用して開発
- フロントエンドエンジニア3〜4人、デザイナー1人
- 全員リモートで、週1で30〜60minぐらいミーティングできる
- GraphQLのAPIサーバーはおよそある状態
- 主に大きなページごと(Userに関するページなど)をエンジニア1人が担当して開発する
もともと課題に感じていたこと・選定理由
もともとそこまで厳格に規約を決めずシンプルにVue.jsで開発をしていましたが、以下のような課題を感じていました。
- 複数のエンジニアがデザインカンプを見て各々の実装をすると、デザインや実装に統一感がなくなる
- 1つのVueファイルが多くの責務を持って肥大化してしまう
- 車輪の再発明しがち・逆に誰かがコンポーネント作るだろうと様子の見合いになって結局作らない/できても中途半端
- デザイナー・エンジニア間の共通言語を作ることで、将来の追加実装や他のプロダクトを開発する際にも応用できる状態にし、スムーズに開発できるようにしたい
そこで、Atomic Designを採用することでデザインや実装方針をある程度画一化し、品質を担保することを目指しました。
Atomic Designとは?
画面を「原子」や「分子」などの単位になぞらえてパーツやコンポーネントとして分割して、それらを組み合わせることで構築するUIデザイン手法です。
以下の記事が詳しいので特に参考にさせていただきました。
本記事では主に、実際にAtomic Designを用いた知見をまとめることとします。
Atomic Designを用いる上で特に意識したこと
結論からいうと、厳密に定義することを追い求めず、あくまで当初の課題を解決することを目的として、以下の点に注意して進めました。
- 完璧なAtomic Designを定義しない・求めない
- AtomとOrganismは先に整備して、基本的にはMoleculeに追加していく
- 肥大化するので、コンポーネントの種類ごとにディレクトリを切る
- 各コンポーネント自体は外側へのマージン情報を持たない・マージンはマージンのみを管理するコンポーネントを作る
完璧なAtomic Designを定義しない・求めない
いざAtomic Designを始めると、どこまでいっても「これはMoleculeじゃなくてOrganismではないか?」など、感覚の議論になりがちです。
当初はそれぞれの粒度を以下のように定義しましたが、言葉の定義だけでは粒度を統一することはエンジニア間でさえかなり難しく、その上でデザイナー・エンジニア間の意思疎通を行うのは無理があるなという感想を抱きました。
- Atomsは「それ以上機能が分解できないもの」。
- Moleculesは「ユーザの動作を促すもの」。 // どこまで作ればユーザは動作を促される..?
- Organismsは「コンテンツが完結しているもの」。 // 何をもってコンテンツは完結する..?
また、コンポーネントを整備する際はAtomから順に作りたくなりますが、実際の実装ではページをどう分割していくかを考えるためPages → Templates → Organisms →...と逆の順で考えることになり、どうしてもOrganismとMoleculeのどちらにするかを決められないケースがあり、もはや悩む時間や、感覚の議論をして手戻りが発生する時間が不毛だなと感じました。
例えば、以下のような場合に迷いが発生しました。
小さい単位(Atom)から考える場合
- Atomを組み合わせて作ったListItemはMoleculeに作るとして、ListはOrganismになる..?
大きい単位(Pages)から考える場合
PagesやTemplateは主にレイアウトのコンポーネントになるため、個別のコンポーネントは基本的にOrganismから考えることになり、以下のような迷いが発生しました。
A. 見出しなしのListだけの場合、Listだけで「コンテンツが完結」しているとみなすと、ListがOrganismになる..?
B. 見出し+Listの場合、見出し+Listで初めて「コンテンツが完結」するので、見出し+ListコンポーネントがOrganismsとなるが、Listは完結していないのでMoleculeになる..?
そしてしばらくすると、MoleculeにListがあり、Organismにも見出し+ListコンポーネントとListコンポーネントが氾濫してしまい、それ以降にジョインするエンジニアがどちらに作るべきかさらに迷う状況になってしまいました。
そこで、諦めて Atomic Designを自分たちが管理可能な形で解釈して、あくまでチームの当初の目的を満たすための設計手法の一つとして捉えることにしました。
AtomとOrganismは先に整備して、基本的にはMoleculeに追加していく
MoleculeかOrganismかで悩むぐらいなら、もういっそほとんどをMoleculeにしてしまう決断をしました。
共通のルールとそれぞれの分類ルールは以下のようなルールとしました。
共通ルール
- コンポーネントには外へのマージン情報を持たせない
- borderがあるコンポーネントの場合はborderの内側のpaddingはコンポーネントが持っても良い
- 同列単位以下のものはimport可能、自分より大きい単位のコンポーネントはimport不可(e.g. MoleculeからMoleculeのimportは可能、MoleculeからOrganismのimportは不可)
- LoaderやIconなどのAtomを他のAtomからimportするかは悩みポイントで、Loaderは基本importして使うが、Iconをpropsに持つButtonも許した。この辺はそこまで縛らない方針とした。
- 肥大化するので、コンポーネントの種類ごとにディレクトリを切って分類する
├── molecules
│ ├── cards
│ │ ├── AppCard.vue
│ │ ├── AppCardList.vue
│ │ ├── FlatCard.vue
│ │ ├── index.ts
import時にpathを深くしたくないので、importはindex.tsからのみ行うようにしています。
Atom
- 「それ以上機能が分解できない」という定義である程度分類できていたのでそのまま
Molecule
- 基本的にはAtom, Organismでなければ全部ここに追加する
- 例えば言葉で定義するとしたら、「その種類として完成する以下のものたち」のようなイメージ
- 種類が「Card」のコンポーネントの場合、CardListItem(ループで複数配置する前提のもの)だけでは完成していないので、CardListまでがMolecule
- CardListはCardListItem間のマージンを規定する責務を持つ
- 見出し(種類:Text)+CardList(種類:Card)の場合、別の種類なので見出し+ListコンポーネントはMoleculeには作らない
- 等間隔のマージンを規定するMoleculeをSeparatorとして用意する(これはAtomにすべきか悩ましい)
- 各種Listコンポーネントが任意のSeparatorを用いてListItem間のマージンを規定することで、マージンに関するロジックをSeparatorの責務にできる
├── molecules
│ ├── separators
│ │ ├── HorizontalMarginSeparator.vue // 水平方向の等間隔マージンを規定する
│ │ ├── HorizontalBorderSeparator.vue // 水平方向の等間隔でのborder区切りを規定する
│ │ ├── VerticalBorderSeparator.vue // 垂直方向の等間隔マージンを規定する
│ │ ├── VerticalMarginSeparator.vue // 垂直方向の等間隔でのborder区切りを規定する
│ │ ├── index.ts
Organism
- 「異なる要素間のマージンを規定するもの」
- 粒度は基本的に、HTMLのセクション関連単位(header, footer, nav, sectionなど)
- 一般的にWebの知識がある人同士で共有できる単位の方が意思の疎通がしやすい
例えば、見出しにはAtomのTextコンポーネント(classをTextとする)を使っている場合、AppSectionは直下のTextに対してのみ下方向マージンを与えるようにしました。
<template>
<div class="AppSection">
<slot />
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({});
</script>
<style lang="postcss" scoped>
.AppSection /deep/ > .Text {
margin-bottom: 20px;
}
</style>
これにより、AppSectionの中に見出し(Textコンポーネント)+Listコンポーネントを置くことで、異なる要素間のマージンが規定されます。
上述の見出し + Listコンポーネントの場合、以下のようになります。
Template
- Organismをレイアウトする・Organism間のマージンを規定する
2カラムレイアウトや聖杯レイアウトなど、アプリケーションで用いるレイアウトパターンをTemplateとして整備します。
OrganismがHTMLのセクション関連の単位で整備されているので、Organismをどのように配置するかの構造とスタイルを主に書きます。
Pages
- Nuxt.jsでいう通常ルーティングをPageとする
- 基本的にはPage内でのみデータアクセス可能
ディレクトリ構成
各ページのドメイン単位でcomponentも用意する
├── src
│ ├── components
│ │ ├── common
│ │ │ ├── atoms
│ │ │ ├── molecules
│ │ │ ├── organisms
│ │ │ └── templates
│ │ └── users
│ │ ├── molecules
│ │ └── organisms
│ │ └── templates
│ ├── pages
│ │ ├── index.vue
│ │ └── users
│ │ ├── _id
│ │ └── index.vue
src/components/common/
以下は、どのページからも使う汎用的なコンポーネントを配置しています。
src/pages/users/
以下がAtomic Designでいう「Page」にあたり、Userに関するコンポーネントはsrc/components/users/
以下に整備することとしました。
Userのページを作る上でも基本的にAtomic Designに則ってディレクトリを分割し、対応するcommon配下のコンポーネントを自由にimportして組み合わせれば基本的にはそこまでCSSを書き足さなくてもpropsに渡すだけで整うような状態を目指しています。
実際に使ってみて、どう感じているか
現時点で感じているメリット・デメリットで挙げてみます。
メリット
- 共通コンポーネントを使えばほぼスタイルを書かなくて良いので、似通ったデザインのページの場合は快適に開発できる
- 各ページに閉じるような共通では使わない特殊なコンポーネントの場合は、わざわざ厳密にAtomic Designを追い求めすぎない判断をとりやすくなった
- 新規に整備する共通コンポーネントは基本的にMoleculeに追加するだけなので、判断に迷わなくなった
- デザインカンプをエンジニアが受け取って独自に解釈して実装するしかなかった前と比べると、明らかに共通言語ができて意図を組んだ実装ができる/デザインを作成できるようになった
デメリット
- ある程度満足に開発を進められるレベルにコンポーネントを整備しきるまでにそこそこ時間がかかる
- 実際整備しきった効果を得るのは、今後長い目で複数人で運用していったときだと思うが、早期・初期からリターンを得られるわけではない
- 新規メンバーには定めたルールの一定の学習コストを払ってもらう必要がある
- 最初から完璧に全てのコンポーネントを用意してから開発できるわけではもちろんないので、都度リファクタや整備していくコストをかける必要はある
当初の課題は解決できたのか
学習コストを払う必要はあるものの、ある程度は解決できる状態になったのかと思います。
複数のエンジニアがデザインカンプを見て各々の実装をすると、デザインや実装に統一感がなくなる
→デザインや実装がずれる元凶だったマージンやサイズなどは共通コンポーネント内に閉じているので、基本的に整備した共通コンポーネントを使うことで統一できるようになりました。
1つのVueファイルが多くの責務を持って肥大化してしまう
→共通コンポーネントの粒度に沿って各ドメインもディレクトリ/ファイルを切るので、巨大なVueファイルはかなり減りました。
ただし、Moleculeの中で多くの責務を持ったコンポーネントが居座ってしまいがちなため、定期的に整備する必要があります。
車輪の再発明しがち・逆に誰かがコンポーネント作るだろうとお見合いになる
→最初にコストをかけて少人数で重要な共通コンポーネントをおよそ整備しきったので、コンポーネント開発においてコンフリクトはそこまで発生しない状態になりました。
しかしながら、今回のプロジェクトはそこまで複数の複雑な画面があるわけではなかったため、より大規模なプロジェクトだと継続的に上記問題は発生するかもしれません。
デザイナー・エンジニア間の共通言語を作ることで、将来の追加実装や他のプロダクトを開発する際にも応用できる状態にし、スムーズに開発できるようにしたい
→エンジニアが実装時に考慮する情報をデザイナーがある程度理解することができたため、実装とデザインの間で以前よりも無理がないよう最初から意識して設計できるような状態に徐々になっていると感じます。
まだ別のプロダクト開発を進めていないので実際に大きな恩恵は受けていませんが、デザイナー・エンジニア間での認識を擦り合せる時間をかなりとったため、共通言語ができたのは大きいです。
その他所感
どのように整備を進めると良いか
初期に整備する手順としては、以下の順で進めると良いなと感じました。
- MiroなどでAtom, Moleculeと思うコンポーネントのスクショをできる限り全て洗い出して、関連するものごとに分類する
- どこまで分割するとAtomとみなせるか、という共通認識を擦り合わせておいた方が良い
- 最初は少人数(エンジニア1〜2人・デザイナー1人など)で意思疎通して、マストなコンポーネントから一気にAtom整備を進めた方が良さそう
- Atomの場合、どういう「機能」を持ったAtomか言葉で定義して分類していく
- その機能はさらに分解できないか検討して分割していく
- デザイナーにマージン・ボーダー・文字サイズ・色・hover時の挙動・transition・最大横幅や改行ルールを洗い出し、説明してもらう
- 逆にデザイナーはPropsの概念を理解しづらいので、エンジニアがどういう粒度でコンポーネントを分割しようとしているか思考を説明すると良い
- デザイン的にはA, Bは全く別物だけど、Propsを受けることで1つのコンポーネントでコスパよく(可読性もよく)まとめられるので1つにできるし、したい、みたいな思考を伝えると良い
- 既存ライブラリのコンポーネントを使うことも考える
- Selectなど自前実装するのは骨が折れるコンポーネントも多いので、ライブラリを用いて車輪の再発明をせずに済ませるのも1つの選択肢
- スタイルを上書きすれば当初のデザインを再現できるのかどうか/その実現可能性に沿ってある程度デザインは調整可能かどうかの相談が必要
- ライブラリ選定のタイミングからデザイナーとエンジニアが協力して調査し、デザインガイドラインを一緒に整備するとより良さそう
Storybookは意思疎通の観点では重要
- デザイナー側の理解を促進できる
- デザイナーからの細かいUIの修正依頼を受ける窓口になる(実プロダクトの実装を進めてからの手戻りを減らせる)
- エンジニアのコンポーネント設計・実装をある程度まとめられる
- CardA, CardBの違いを認識するためコードを頑張って追うより、最初にStorybookで視覚的に認識できた方が早い
相応の覚悟とステークホルダーの理解が必要
- デザイナーとエンジニアがお互いを理解するためには、単純なデザインとプログラミングという境界を超えて歩み寄る覚悟が必要です
- 今すぐに恩恵を得られるものではないので、ある程度周囲の理解は必要になるかと思います
おわりに
Atomic Designをチームにとっての課題解決手法として独自に解釈し、使いやすい形で運用している例をご紹介させていただきました。
どの手法を採用したとしても永遠に完璧なものは存在しないので、常にチームにとっての最善を選び、失敗しながらも改善を続けられるのかが大事なのかなと思います。
独自の解釈をしているため、「Atomic Design」と銘打つのはおこがましいかもしれませんが、何か参考になる点があれば幸いです。