Salaries.jpのフロントエンド実装について ~ componentsとAtomic Design編 ~

こんにちは。テクノロジー本部 第2開発部のYuto SAGAWAです。*1
現在 Salaries.jp というサービスの開発に携わっています。 今回はフロントエンドの実装から、componentsとAtomic Designについて紹介します alt

概要

近年のWebフロントエンド開発においてコンポーネントは当たり前な存在になってきました。 さらに、コンポーネント実装においてAtomic Designというデザイン手法も主流になりつつあるように思えます。 そこで本記事では、Salaries.jpでのcomponents実装とAtomic Designの実践方法について紹介していきます。 また、Storybookの使い方や、良かった点改善点なども紹介したいと思います。

フロントエンドの構成・特徴

- Next.js
- React.js
- Express.js (Custom Server)
- Styling
  - Material UI
  - styled-components
- Storybook

フロントエンドのフレームワークにはNext.jsを使用。 Material UIを主に使用し、グラフ部分ではstyled-componentsを使用しました。 また、Storybookを用いてcomponentsのカタログ作成も行いました。 グラフ部分の実装やその他フロントエンド関連の話は別の記事で執筆できればと考えています。

components

ディレクトリの構造は components という名でディレクトリを作成し、 その配下にAtomic Designからpages以外のディレクトリを区切りました。

/components
 ├── atoms
 ├── molecules
 ├── organisms
 └── templates

それぞれ要素のディレクトリ配下には各コンポーネント毎にディレクトリを区切っています。

/components
 ├── atoms
 |   └── * (Button, Box, Input...)
 |       |── index.md
 |       |── index.stories.tsx
 |       └── index.tsx
 ├── molecules
 ├── organisms
 └── templates

Atomic Design

Atomic Designとは

alt refs: https://atomicdesign.bradfrost.com/chapter-2/

Atomic Designは簡単に説明すると、上記画像のように要素に分けてそれらを組み合わせて実装するUIデザインの手法です。

実装手順

Atomic Designの実装のフローとしては atoms から順に作ってい行きますが、 Salaries.jpでは少し違うフローで実装を進めました。 alt それぞれ順番に紹介していきます。

1. Atoms実装

まず最初はAtomic Design通り、Atomsの実装からはじめました。

ボタン テキストフィールド チェックボックス など最小の部品となります。

AtomsはMaterial UIをラップしたコンポーネントで、そこに必要なpropsを追加して拡張するようにしています。 また、Material UI自体のスタイリングには makeStyles を用いて、Propsで渡された値を使用したり固定で設定するなど行いました。

下記コードは、Material UIのButtonコンポーネントをラップしてpropsに width, height を追加したサンプルコードになります。

import { default as MuiButton, ButtonProps } from '@material-ui/core/Button';
import { makeStyles } from '@material-ui/core/styles';
interface IButtonProps extends ButtonProps {
  width?: string;
  height?: string;
}
const useStyles = makeStyles({
  root: {
    width: ({ width }: IButtonProps) => width || '',
    height: ({ height }: IButtonProps) => height || '',
  }
});
export const Button = (props: IButtonProps): JSX.Element => {
  const styles = useStyles(props);
  const classes = {
    root: styles.root
  };
  return (
    <MuiButton classes={classes} {...props}>
      {props.children}
    </MuiButton>
  );
};

2. Organisms実装

Atomsの実装の次はOrganisms実装に移ります。 Organismsはページの大部分を実装しました。 フォーム データテーブル ヘッダ フッタ などAtomsを組み合わせて余白などを調整しつつ実装します。

3. Organisms実装しながら共通化できそうな部分はMoleculesに分離

Organismsを実装しながら共通化できそうな部分があればMoleculesに分離します。

Moleculesを飛ばしてOrganismsを先に実装するようなフローになった理由としては 事前にMolecuels の粒度を定義するのが難しかったことが挙げられます。

Moleculesは、Atomsにおける ボタン やOrganismsにおける フォーム のように一般的なキーワードで定義しづらく、 Atomic Designに基づいたコンポーネント実装を実践する中で何がMoleculesに当たるかを判断することが難しいと感じています。

Moleculesの定義が明確にできていないままAtomsから順番に実装していくと、 MoleculesとMoleculesを組み合わせたコンポーネントを作りたいが、Organismsの粒度ではないコンポーネントが出てくることがあります。 苦肉の策として「MoleculesとMoleculesを組み合わせたMolecules」を作るとAtomic Designの秩序が崩れてしまいます。

したがって、SalariesではMoleculesを「Organismsから切り出した再利用可能なコンポーネント」と定義し、 Organismsのリファクタリングの一環としてMoleculesを抽出し分離することにしました。

4. Pages実装

Pagesの実装はページのレイアウト実装とOrganismsの呼び出し。 さらにローディング表示やエラーなどのフィードバック用のコンポーネントもPagesに含まれています。

<>
  <Loading />
  <Feedback />
  <Header />
  <Menu />
  <Organisms />
  <Footer />
</>

5. 同じレイアウトのページをTemplatesにLayoutとして実装

Header, Menu, Footer など決まったレイアウトがパターン化されてくるので、それらをTemplatesにLayoutとしてディレクトリを区切り、レイアウトのパターン毎にファイルを作成します。

const Layout = (props) => {
  return (
    <>
      <Header />
      <Menu />
      {props.children}
      <Footer />
    </>
  );
}

6. PagesにTemplatesからLayoutを適用

分離したLayoutをPagesから呼び出します。

<>
  <Loading />
  <Feedback />
  <Layout>
    <Organisms />
  </Layout>
</>

Storybook

各コンポーネントのディレクトリにはそれぞれ3つのファイルを作成しています。

- index.md
- index.stories.tsx
- index.tsx

index.tsx はコンポーネントのファイルで、残り2つがStorybook用のファイルになります。

index.md

Storybook上で表示されるコンポーネントのNoteの役割を果たします。 構成としては シンプルで、コンポーネントの使い方とPropsとNoteの3つになります。

ボタンコンポーネントのNoteのサンプル alt

index.stories.tsx

Storybook上で表示されるCanvasの役割を果たします。 それぞれボタンのタイプごとにCanvasを作成しました。

const Basic = (): JSX.Element => {
  return (<Button>Button</Button>);
};
const fullWidth = (): JSX.Element => {
  return (
    <Button fullWidth>
      Button With Full Width
    </Button>
  );
};
const Disabled = (): JSX.Element => {
  return (
    <Button disabled variant="contained" color="default">
      Button With Disabled
    </Button>
  )
};

良かった点と改善点

ここまでAtomic Designとcomponents実装について紹介してきました、ここからは良かった点と改善点について触れたいと思います。

良かった点

Atoms単位のコンポーネントを作るだけでもUIの変更の融通がききやすくなるので非常に重要だと再確認することができました。 UIの変更に耐えるためにもAtoms実装は必ず必要なものとなるでしょう。

ページすべてのデザインが完成していない中での開発ということもあり、Atomic Design従来通りとは違う実装フローにはなりましたが、 作っていかないと気づかないこともあり、今回の実装フローは上手くいったと思います。

改善点

Atomic Designはそれぞれの層でどの粒度で区切るかを、 出来上がったプロトタイプからコンポーネントを切り抜いた画面をキャプチャしてチャットツール上で議論しました。

エンジニアだけでの議論だったため、デザイナを含めデザインツール上でのやり取りにして、エンジニアとデザイナ双方で共通の理解を進めれれば良かったと思いましたが、 それぞれ関心事の違いもありどこまで認識合わせをするかも課題の一つだと感じました。

また、UI実装では似てるけど少し違うようなケースが存在します。そういったケースを無理やり共通化する必要はありませんでした。 Propsで出し分けの制御をすることになりますが、コンポーネントに渡す値の状態管理が複雑になったりと不具合が起こりやすくなります。 コンポーネントにおいては、無理に共通化はせずにできる範囲でMoleculesやOrganismsなどに切り出しするのが良いでしょう。

Storybookに関してはあまり活用することができませんでした。 成果物のデザイナの確認はStorybook上ではなく、実際のページ上での確認がメインとなっていました。 Canvasの作成の仕方にも工夫が必要だったと感じました。 Note部分は主に開発中に参照する予定でしたが、必要な情報が手に入らず活用することができませんでした。 よく使うPropsなどをセットにして実際のサンプルコードを乗せる方針が良かったかと感じています。

最後に

Atomic Designが必ず最適であるとは言い切れませんが、UIの変更に耐えるための一つの手段ではあると考えられます。

Atomic Designは5層ありますが、すべてのコードで必ず5層も必要ではないと考えられこともあります。 その場合でもAtomic Designを元にアレンジをして一定の粒度でコンポーネントを分離するのが重要だと思います。

プロジェクトに適した方法でAtomic Designの実践してみてはいかがでしょうか。

Yuto SAGAWA

Yuto SAGAWA

エンジニアリング統括部 サービス開発部 第1グループ エンジニア

現在は退職

f:id:ib_ofuji:20191113232601j:plain

吉次 洋毅 Hiroki Yoshitsugu

エンジニアリング統括部 サービス開発部 第2グループ シニアエンジニア

2014年に高専専攻科を修了後、飲食店検索サービスを提供するWeb企業に入社。PHPをメインにバックエンドの領域の開発やプロジェクトマネジメントに従事。2016年にインテリジェンス(現パーソルキャリア)に入社。「doda AIジョブサーチ」の開発などを経て、現在はSalariesの開発を担当している。

※2021年5月現在の情報です。

*1:※Yuto SAGAWAは退職していますが、本人の同意を得て掲載を継続しています。