💡

React/Next.jsでAtomic Designを導入する初心者がとりあえず読む記事(ボイラーテンプレートあり

2022/06/16に公開約17,700字

この記事で得れる情報

  • これからReact/Next.jsとAtomic Dsignで何か作るってなった時にとりあえず多分必要なものがさらっと集約して学べる
    • 参考にしたサイトのまとめ・噛み砕き
    • 私の所感
  • 上記を既に入れたいと思ってる人は、入れ済みのすぐ使えるボイラーテンプレート

とりあえず作るもの先出し

こーんな感じの、いくつかのコンポーネントでできた簡単すぎるログイン画面を例に作られています。
ログイン画面

ボイラープレート

っていうかサンプルソースレベルです。あまり時間をかけずにさっと作ってるのでクレームは受け付けてません笑

https://github.com/hisashige/next-atomic-boilerplate

動機

こんにちは、地方在住のフルリモート開発リーダー、ひさちーと申します。

私は仕事では10人くらいの受託開発を行うチームのマネージャーやってまして、プロジェクト単位ではPMやスクラムマスターをすることが多いのですが…
しばらくフロントエンドのコードをあまり書かないうちに、Reactのカスタムフックが〜とか、Atomic Designが〜みたいな話が飛び交う中、まぁ概念は理解してはいるものの、ヒアリングした内容から適当に判断・指示してる状態でした…(勿論ちゃんとその場で必要な情報を集めて適切だなと思う指示はしてますよ!笑)

ただ、具体的に自分で手を動かしてやったことがなかったのでメンバーの話を聞いたり開発をまとめるにしても、こりゃそろそろ手を動かして勉強せないかんなぁ〜、と思っておりました…

なので!フロントエンドが得意な部下に意見を聞きながら自分でAtomic Designに基づいたReact/Next.jsの開発時のテンプレートを自分で作ってみることにしました〜

対象読者

  • 急にReact/Next.jsとか使ってAtomic Design採用するってなったけど、色々周辺ライブラリ何を揃えたらいいか迷ってる人
  • 色々必要な知識があるけど分散されていて、情報の集約を求めていた人
  • やりたいこと似てるから、ボイラーテンプレート仕入れて秒で開発始めたい人
  • チームメンバーにこれらを説明するときにめんどくさいからまずここ見てって言って終わらしたい人
  • これを元に開発を始める私のチームメンバー笑

などに参考になれば幸いです。

私の基本情報

技術系の記事って、読む人によってこれくらいのレベル感の人ならここまでは分かるけど、これくらいの人からしたら何言ってるかさっぱり分からない…とか多いですよね。
なのでイメージしやすいように、これを作る前の私のフロントエンドの知識はこんな程度です。

  • Atomic Designは概念的に知ってるが、大変そうやなぁとか思ってる。
  • Vue/Nuxt.jsはそこそこ自分でも作ってた時がある。
  • React/Next.jsは普通にwebの管理画面を作ったことがあるから、それなりには分かる。けどゴリゴリのReactエンジニアって言われたらそうではない。
  • Vue.jsのイベントライフサイクルのノリで考えてると、ReactのuseEffectとかのフックとかコンポーネント思考の考え方に最初ちょっと戸惑った記憶。まだカスタムフックとかいっぱい使って〜とかはやってない。

事前知識

Atomic Designの概念知識や、Reactなどのモダンフロントエンドの知識がない方は、まずそちらをインプットした方がいいかもしれません。

参考サイト:

必要な技術・ライブラリ

とりあえず、Atomic Designを採用して、Next.jsで開発するにおいて、何を揃えればいいか決めました。

  • Next.js
  • TypeScript
  • Chakra UI
  • styled-component
  • Storybook
  • Chromatic
  • hygen

※APIのコールや状態管理は入れてません。
長くなりすぎますし、Redux・SWR・React Queryなど、使いたいものは結構違う人が多いと思いますので、そちらはお好きなもの入れていただくと良いかと思います。

Next.js

https://nextjs.org/
Next.js
Next.jsはReactをベースにしたフロントエンドフレームワークです。

Next.jsはサーバーサイドレンダリング(SSR)やファイルベースルーティングなど多くの機能をゼロコンフィグで提供してくれます。また、開発会社Vercelが同名のプラットフォームVercelを展開しており、デプロイ/ビルド/配信までを一気通貫に提供しています。

メリットはたくさんあります。

  • SSR/SSG
  • ファイルベースルーティング
  • 開発サーバの部分的な高速リロード(Fast Refresh)
  • 画像最適化
  • ゼロコンフィグ

詳しくはこちらの記事なんかが参考になりました。
https://qiita.com/Yuki_Oshima/items/5c0dfd8f7af8fb76af8f

個人的にはもうReactやるときは脳死でNext.js使おうって気になります笑
Vue.jsであればNuxt.jsも同じような思想かと思いますが、私的には

  • あらかじめ設定しなくてもルーティングとか色々やってくれる
  • ディレクトリ構成とかもある程度素のReactよりは決まってるから、ちょっとでもオレオレアーキテクトから抜け出せて、多くの人が読みやすい
  • ビルドもSSRもSSGも選べるし、どうとでもなる
    とかだけでももう脳死採用待った無しだと思ってます(異論は普通にあると思います笑

TypeScript

https://www.typescriptlang.org/
TypeScript
これはもう言うこともない気がするので脳死で入れますw

Chakra UI

https://chakra-ui.com/
Chakra UI

Chakra UIは、UIコンポーネントライブラリの一つです。これを使用することで、1からCSSを記述することなくスタイルに一貫性を持たせたUIを楽に実装することができます。
いわゆるUIフレームワークですね。
今回Atomic Designにのっとり、Storybookを利用してコンポーネントを作成していくわけですが、とはいえ全部自前でCSSを書くのもめんどくさいので、Chakra UIを利用する事にしました。

採択理由は、

などです。

メルカリShopsさんなども同じ理由で採択されてるみたいなので、参考に載せておきます。
https://engineering.mercari.com/blog/entry/20210823-a57631d32e/

Storybook

https://storybook.js.org/
Storybook
出ましたね…
こちらがAtomic Designを採用してコンポーネント駆動開発をしていく際に肝となります。

どんなものかというと、

  • UIコンポーネントの管理・テストをすることが出来るオープンソースツール。
  • サンドボックス環境を構築し、その環境下でコンポーネントの挙動や表示を確認できる他、カタログのようにコンポーネントを一目で見ることができる。
  • React、Vue、Angularなどの主要なJSフレームワークで導入でき、利用範囲も広い。
    などの特徴があります。

メリットとしては、下記が上がるかと思います。

  • UIコンポーネントのカタログとして視覚的に確認できることでエンジニアとデザイナーの認識の齟齬を無くすことが出来る。
  • カタログから探すことが出来るので、再利用したい時にすぐに調べることが出来る。
  • コードの変更が即時に画面に反映されるため、開発作業が素早く行える。
  • UIの変更に対して、ビジュアルリグレッションテストをすることができる。

こちらの記事を参考にさせていただきました。
https://tech.stmn.co.jp/entry/2021/05/17/155842

Storybookを使うことで、Figmaで作ったAtomic Designに基づいたコンポーネントデザインに基づき、コンポーネント単位でまず作っていきながら、デザイナーさんに実際の画面での感じを確認したりもできるんですね〜
これぞコンポーネント駆動開発!

今回デザインは、Figmaを利用して事前にAtomic Designに基づくAtomやMolecule,Organismを作って行って、それを元にコンポーネントを作っていってるのですが、またそちらの記事も書いてみますね!

※Chakra UIを使う場合、公式サイトでのインストール手順にプラスして、
Storybook用に設定が必要です。最初少しだけ、あれ、、使えてないなとかつまづきました…
こちらの記事が参考になりました。
https://zenn.dev/json_hardcoder/articles/3e3db6ed5c583e
(ボイラーでは設定しております。)

Chromatic

https://www.chromatic.com/
Chromatic
Chromaticとは、Storybook のメンテナーが作成している Storybook 用のツールです。Storybook をビルドして公開したり、ストーリーごとのスクリーンショットを撮影し、差分を比較してくれる機能を備えています。
Chromatic を使うことにより、UI の予期せぬ変更を事前に検知することができます。

こちらの記事が参考になりました。
https://devblog.thebase.in/entry/2021/12/08/203039

Github Actionsなどで、pushをトリガーにChromatic上にその時点のStorybookをデプロイしてくれたり、そこでUIを確認するレビューを挟んだりができるわけですね。

hygen

https://mabui.org/hygen-nuxt-setup/
hygenはnode.jsで作られたコードジェネレーターです。
テンプレートからファイル生成する用途で幅広く活用できるツールになります。
今回の用途では、Atom/Molecule/Organism/Templateを何回も作ることになるAtomic Designで、コンポーネントを作成するのに必要なファイルの雛形の作成に利用します。

こちらの記事が参考になりました。
https://zenn.dev/takepepe/articles/hygen-template-generator

ボイラープレートを見ていく

ソース

早速こちらをcloneしてください。

git clone https://github.com/hisashige/next-atomic-boilerplate

以上、あざした〜

…でもいいのですが、少しソースを見ていきます笑

構成

構成はシンプルで以下の感じです。

全部書くと無駄なので、Next.jsの基本的なファイルとかまぁいいやって部分は省略してます。

next-atomic-boilerplate/
┣ .storybook/ - storybookの設定
┣ hygen/ - hygenの雛形ファイルなど
┣ public/ - 静的ファイル
┣ src/ - メインのプログラムファイル
┃ ┣ components/ - Atomic Designに基づくコンポーネントファイル
┃ ┣ atoms
┃ ┣ molecules
┃ ┣ organisms
┃ ┣ templates
┃ ┣ pages – 普通にNextのpagesディレクトリ
┣ .hygen.js - hygenの設定ファイル
┣ chkraTheme.js - Chakra UIの設定ファイル

使い方

ローカルサーバー立ち上げ

以下で普通にlocalhost:3000とかで開発サーバーを立ち上げます。

yarn dev

普通の開発ならこれで画面見ながら、作っていくのですが、(僕はそれまでフロントやる時は割とそういう思考回路だったのですが)

僕が今回一番考え方が変わったのが、
StorybookとAtomic Designを使ったコンポーネント駆動開発
というものを体感したことです!

コンポーネント駆動開発のメリット/デメリット

今まで僕はコンポーネントを切るって言っても、
ページをまず作ろうとして、そこで使いまわせそうなものはコンポーネントを作る
って感じでした。

これが、Atomic Designを使うと、ページからっていうよりは、Figmaなどで
デザインからまずコンポーネントを作っていく。それをStorybookで確認していく。最後に組み合わせてページを作ってデータなど注入する。
って感じになります。

いや、それを組み合わせるって卵が先か鶏が先かで結局同じことやん、、って思うと思うんですが、体感的には結構これが個人的に思想が全然違うなって思ったポイントでした。

なんとなく思った具体的メリットをあげると、
メリット

  • デザイン段階で既にどんなコンポーネントから作るか決めてて、コンポーネントから作るから、複数人で作業しやすい。被ったもの作ることがない。
  • デザイナーと連携しやすい
  • Storybookでコンポーネント単位でテストしてる状態になる

まぁいいとこばっかり言ってもあれですし、デメリットを感じることもあったのですが笑
デメリット

  • 普通にコストかかるから、大きなサービスでやるのはいいけど、受託開発でやるのはきついかも
  • デザイナーが優秀じゃないと厳しい

Storybook起動

話が脱線しすぎて、全車輪脱輪してるレベルですが、使い方の話に戻ると、
上記踏まえてstorybookがコンポーネントを作っていく際に便利です。

基本的に最初はyarn devで画面を見ていくというよりは、以下でstorybookの開発サーバーを立ち上げてコンポーネントの挙動を確認していくことになります。

yarn storybook

components/ 配下に作ってるコンポーネントがStorybookで確認できます。
今回は簡単な例でButtonを見ていきます。
Atoms/Button

コンポーネントファイルの歩き方

atoms/Buttonsには以下のファイルが入っています。

  • Button.tsx
  • index.stories.tsx
  • index.ts
  • modules.style.ts
  • type.ts

まずメインとなるのはButton.tsxです。

src/components/atoms/button/Button.tsx
import React, { memo } from "react";
import { AppButton } from "@/components/atoms/Button/module.style";
import { Props } from "./type";

export const Button: React.FC<Props> = memo(
  ({ type, size, label, ...props }) => {
    // ボタンタイプによる色とスタイルの決定
    let bg = "primary";
    let color = "white";
    let borderColor = "";
    switch (type) {
      case "secondary":
        bg = "white";
        color = "primary";
        borderColor = "primary";
        break;
      case "accent":
        bg = "accent";
        break;
      case "logout":
        borderColor = "white";
        break;
    }

    // 変数borderColorが存在すれば、propsにborder・borderColorを設定
    const chakraButtonProps = { ...props };
    if (borderColor) {
      Object.assign(chakraButtonProps, {
        border: "2px",
        borderColor: borderColor,
      });
    }
    return (
      <AppButton
        fontSize={size === "small" ? "lg" : "2xl"}
        p="12px 24px"
        buttonsize={size}
        bg={bg}
        color={color}
        {...chakraButtonProps}
      >
        {label}
      </AppButton>
    );
  }
);

Button.displayName = "Button";

普通にtsxをここで作っていきますが、注目すべきは、

  • AppButton

    • 今回はChakra UIを使ってますが、widthとheightのスタイルを当てたいため、styled-componentでChakraのButtonをラップしたものを、modele.style.tsで用意してインポートしています。
    src/components/atoms/button/modele.style.ts
    import styled from "styled-components";
    import { Button } from "@chakra-ui/react";
    
    import { ButtonSize } from "./type";
    
    export const AppButton = styled(Button)<{
      buttonsize: ButtonSize;
    }>`
      width: ${({ buttonsize }) => (buttonsize === "small" ? "auto" : "450px")};
      height: ${({ buttonsize }) => (buttonsize === "small" ? "40px" : "48px")};
    `;
    
  • type.tsにPropsやボタンタイプやサイズを定義しているためインポートして使用しています。

    src/components/atoms/button/type.ts
    export type ButtonType = "primary" | "secondary" | "accent" | "logout";
    
    export type ButtonSize = "small" | "medium" | "large";
    
    export interface Props {
      /**
       * ボタンタイプ
       */
      type?: ButtonType;
      /**
       * ボタンのサイズ
       */
      size?: ButtonSize;
      /**
       * ボタンのラベル
       */
      label: string;
      /**
       * 非活性フラグ
       */
      isDisabled?: boolean;
      /**
       * クリックハンドラー
       */
      onClick?: () => void;
    }
    

これでコンポーネントの作成は終了ですが、
index.stories.tsxを作成することで、Storybook上でコンポーネントにPropsを渡した様々な状態を確認することができます。

src/components/atoms/button/index.stories.tsx
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { withDesign } from "storybook-addon-designs";
import { action } from "@storybook/addon-actions";
import { Button } from "./";

export default {
  title: "Design System/Atoms/Button",
  component: Button,
  decorators: [
    (story: any) => <div style={{ padding: "0 2rem" }}>{story()}</div>,
    withDesign,
  ],
  argTypes: {},
} as ComponentMeta<typeof Button>;

const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  type: "primary",
  label: "ログイン",
  onClick: action("clicked"),
};
Primary.parameters = {
  design: {
    type: "figma",
    url: "",
  },
};

export const Secondary = Template.bind({});
Secondary.args = {
  type: "secondary",
  label: "ログイン",
  onClick: action("clicked"),
};
Secondary.parameters = {
  ...Primary.parameters,
};

export const Disabled = Template.bind({});
Disabled.args = {
  type: "primary",
  label: "ログイン",
  onClick: action("clicked"),
  isDisabled: true,
};
Disabled.parameters = {
  ...Primary.parameters,
};

export const Accent = Template.bind({});
Accent.args = {
  type: "accent",
  label: "ログイン",
  onClick: action("clicked"),
};
Accent.parameters = {
  ...Primary.parameters,
};

export const Small = Template.bind({});
Small.args = {
  type: "primary",
  size: "small",
  label: "ログイン",
  onClick: action("clicked"),
};
Small.parameters = {
  ...Primary.parameters,
};

export const Logout = Template.bind({});
Logout.args = {
  type: "logout",
  size: "small",
  label: "ログアウト",
  onClick: action("clicked"),
};
Logout.parameters = {
  ...Primary.parameters,
};

こうやってStorybook上でコンポーネントの動作を確認することで、まずコンポーネントを作って、動作確認してから、それを集めてさらに大きなものを作っていくという、コンポーネント駆動な開発が可能になります。

おまけ情報ですが、Figmaなどでデザインを作っている場合は、下記部分のurlにFigmaのアートボードのURLをコピペしておくと、Storybook上でFigmaと実際に実装したコンポーネントを比較できるので便利です。

Primary.parameters = {
  design: {
    type: "figma",
    url: "",
  },
};

Figma

moleculeやorganismやtemplateもやることは変わらないので、あとは同じように作るだけです。
ボイラーを参考に自分の作りたいものに合わせて作ってみてください。

組み合わせていく

基本的にはAtoms→Molecules→Organismsと流用できそうなものは使いながら大きく作っていって、最後はTemplatesでページ全体のレイアウトを定義していきます。
OrganismsのHeaderとLoginAreaを組み合わせてるだけです。

src/components/templates/LoginTemplate/LoginTemplate.tsx
import React, { memo } from "react";
import { Center } from "@chakra-ui/layout";
import { Props } from "./type";
import { Header } from "@/components/organisms/Header";
import { LoginArea } from "@/components/organisms/LoginArea";

export const LoginTemplate: React.FC<Props> = memo(
  ({
    inputEmail,
    handleInputEmailChange,
    inputPassword,
    handleInputPasswordChange,
    isDisabledLoginButton,
    isErrorLogin,
    isLogined,
    onLogin,
    onLogout,
  }) => {
    return (
      <>
        <Header isLogined={isLogined} onLogout={onLogout}></Header>
        <Center h="100%">
          <LoginArea
            inputEmail={inputEmail}
            handleInputEmailChange={handleInputEmailChange}
            inputPassword={inputPassword}
            handleInputPasswordChange={handleInputPasswordChange}
            isDisabled={isDisabledLoginButton}
            isError={isErrorLogin}
            onLogin={onLogin}
          ></LoginArea>
        </Center>
      </>
    );
  }
);

LoginTemplate.displayName = "LOGINTEMPLATE";

そしてpages/index.tsxでデータを注入してあげます。
ボイラーではAPIも何にも叩いてないので、なんの処理もなく定義しただけのデータを渡してるだけですが…笑

src/pages/index.tsx
import type { NextPage } from "next";
import { useState, useEffect } from "react";
import { LoginTemplate } from "@/components/templates/LoginTemplate";

const Home: NextPage = () => {
  /**
   * State
   */
  // メールアドレス入力値
  const [inputEmail, setInputEmail] = useState("");
  // パスワード入力値
  const [inputPassword, setInputPassword] = useState("");
  // ログインボタン非活性フラグ
  const [isDisabledLoginButton, setIsDisabledLoginButton] = useState(true);
  // ログイン処理失敗フラグ
  const [isErrorLogin, setIsErrorLogin] = useState(false);
  // ログイン済みフラグ
  const [isLogined, setIsLogined] = useState(false);

  /**
   *  イベントハンドラー
   */
  // メールアドレス入力値変更
  const handleInputEmailChange = (event: React.ChangeEvent<HTMLInputElement>) =>
    setInputEmail(event.target.value);
  // パスワード入力値変更
  const handleInputPasswordChange = (
    event: React.ChangeEvent<HTMLInputElement>
  ) => setInputPassword(event.target.value);
  // ログインボタンクリック
  const onLogin = () => {
    setIsLogined(true);
    console.log("login!");
  };
  // ログアウトボタンクリック
  const onLogout = () => {
    setIsLogined(false);
    console.log("logout!");
  };

  /**
   *  副作用
   */
  // メールアドレスとパスワードが両方入力済の場合のみ、ログインボタンを活性化
  useEffect(() => {
    if (inputEmail && inputPassword) {
      setIsDisabledLoginButton(false);
    } else {
      setIsDisabledLoginButton(true);
    }
  }, [inputEmail, inputPassword]);

  return (
    <LoginTemplate
      inputEmail={inputEmail}
      handleInputEmailChange={handleInputEmailChange}
      inputPassword={inputPassword}
      handleInputPasswordChange={handleInputPasswordChange}
      isDisabledLoginButton={isDisabledLoginButton}
      isErrorLogin={isErrorLogin}
      onLogin={onLogin}
      isLogined={isLogined}
      onLogout={onLogout}
    ></LoginTemplate>
  );
};

export default Home;

Chromatic

Githubにpushされると、自動でChromatic上にデプロイされるようにGithub Actinosの方も設定しております。

https://www.chromatic.com/docs/setup
https://www.chromatic.com/docs/github-actions
基本的には公式の手順が分かりやすく書かれておりますが、上記Github Actionsが成功するようにするために、以下のような手順を実施してださい。
  1. ChromaticにSignUpする。
  2. Chromaticでプロジェクトを作成する。
  3. ご自身のGithubリポジトリーをChromaticのプロジェクトに連携させる。
  4. 下記画面が出るため、表示された下記をコピペして実行する。(yarn addは実行済み)
npx chromatic --project-token=<YOUR_PROJECT_TOKEN>


5. GithubリポジトリのSecretにCHROMATIC_PROJECT_TOKENを登録する。

これで、pushをトリガーにChromaticにデプロイがされるかと思います。

便利な雛形作成君hygen

ここで、hygenについて触れておきます。

これまで見たようにコンポーネントから複数のフロントエンジニアが並行して作っていけるのがAtomi Designの利点の一つだと思っていますが、そこでファイルの構成や書きっぷりに違いが出てきたらどうでしょうか…
それはルールを作っておいても、単に同じようなファイルを毎回作成したり、コピペするのはめんどくさいですよね、、

そこでhygenです!

ボイラーにはもう既に導入してテンプレートも作っているので、下記コマンドを実行してみてください。

yarn hygen

下記のように対話式でそのコンポーネントはatomsなのかmoleculesなのかや、コンポーネント名、またどこまで周辺ファイルが必要かなどに回答していくと、用意していたテンプレートに沿って、雛形ファイルを作成してくれます。
hygenコマンド

出来上がったファイルたち
hygen生成ファイル

テンプレートの内容やどういうファイルを作るかは、自分で設計できるため、hygen/componentsディレクトリを参照してください。

これで、実際のコンポーネントを作る作業に徹して、快適なAtomic Lifeを満喫できますね!

今日の余談

今回数ヶ月ぶりにフロントエンドをガッツリ触りまして、ちょっとやらないうちに割と勉強することが多かったです…

フロントエンドは本当に今移り変わりが早くて、お〜hygenなんてものがあるのか〜いれてみっかーとかしてると思ったより時間かかりました笑

全然関係ない私事ですが、開発リーダーやマネージャーって、フルスタック性求められるから、フロントエンド・バックエンド・インフラなど、どこまで突き詰めて勉強すべきか、どのように自分というリソースを配分できるか、マルチタスクの才能が問われますよね。

僕は「もう全部やるのなんか無理だから、諦めて得意なメンバーに聞く、そろそろだなと思ったら一回自分でやってみる」というスタイルでやっていますw

一つの事につっこみすぎても自分の役目果たせないですし、自分でどこまでキャッチアップするかは難しいところですね。

(…でも久しぶりにコーディングすると、こっちの方が楽しいし、やっぱりなんらかのスペシャリストになろうかな、なんて思ってしまう若手中間管理職あるあるを日々感じてます。)

あざした〜

Discussion

ログインするとコメントできます