STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

コンポーネント粒度と依存関係チェック feat. STORES予約フロントエンド

はじめに

STORES 予約 でエンジニアをしているyuta07です。

STORES 予約 の管理画面の新規開発はNext.jsを用いて開発しているのですが、日々のカジュアル面談や採用活動でフロントエンドの技術状況についての質問を受けることが増えてきました。

今回は STORES 予約 のフロントエンド開発の中でもコンポーネント粒度と依存関係チェックについて紹介します。

主な使用技術

  • Next.js、React、TypeScriptを採用。
  • スタイルはTailwind CSSを使用。
    • STORES 全体で統一されているconfigをユーティリティクラス(フォント・カラー等)として使用してスタイリングしています。
    • 一部特殊なアニメーションを実装する場合は、各コンポーネントファイルと同階層にCSS Modulesファイルを作成してスタイルを記述しています。
  • Lint・FormatにはESLintとPrettierを使用。
    • 今回の記事では、 STORES 予約 におけるESLintの strict-dependencies ルールの活用にフォーカスしています。

コンポーネントの分類

Atomic Designを、STORES 予約 でのフロントエンドアプリケーション開発に適する形で整理しています。

下記はSTORES 予約 での各コンポーネントのファイル構成のベースとなります。

- DesignedForm
  - index.stories.tsx
  - index.tsx
  - styles.module.css

STORES 予約 ではベースを保ちつつ、運用をしやすいように独自のルールを定義しています。

それでは、STORES 予約における各層の定義を紹介します。

Pages

Next.jsのルーティングにあたるコンポーネントです。

Pagesでは以下の責務を負います。

  • templatesを表示するために必要なデータの取得
    • データ取得後のレイアウトの出し分けやデータ取得中のローディングをtemplatesに書くことによる複雑化を抑制するため
  • データ取得時のエラーハンドリング
  • ログイン状態に基づく表示やリダイレクト

ファイル配置はNext.jsのルーティングに従い、コーディングは上記のルールに従う、といった形となります。

Templates

Atomic DesignにおけるTemplatesと同義で、表示すべき値をpropsで受け取ることで1つのページをレンダリングします。

以下、スマートリストの顧客一覧画面を例に説明します。

スマートリストに関してはこちらの記事をご参照ください。

https://product.st.inc/entry/2024/07/01/120000#スマートリストとは

Templatesは画面レイアウト全体が対象となります。

各ページに対応したフォルダを作成して、内部に独自のコンポーネントとフックを定義可能としています。

- templates
  - 各ページ名のフォルダ
    - BaseTemplate
      - components:BaseTemplateで使用するコンポーネント群
      - hooks:BaseComponentsで使用するhooks
      - index.tsx:コンポーネント
      - index.stoires.tsx:コンポーネントに対応するstories
      - messages.ts:この記事では触れてないが、i18n対応のための言語ファイル
      - styles.module.css:コンポーネントに対応するスタイル

各templateファイルの名前はpagesが index なら IndexTemplatenew なら NewTemplate といった風にPagesに合わせるようにしています。

1つのページから条件によって2つのTemplateを出し分けたいといったケースでは、そのケースにあった命名で2つのファイルを作成することになります。

例えば、/pages/settings からそれぞれのTemplatesを呼び出したい場合は

  • /templates/settings/GeneralSettingsTemplate
  • /templates/settings/SpecialSettingsTemplate

といった2つのTemplatesファイルを用意します。

1つのコンポーネントの中にcomponentsやhooksが存在する構成は少々特殊ですが、再利用性がない特定のTemplatesでの利用が前提となるコンポーネントをOrganismsやMoleculesに作成しないことで、コードの見通しが悪くなるのを防ぐことを目的としています。

Organisms

Atomic DesignにおけるOrganismsと同義で、MoleculesやAtomsを組み合わせたコンポーネントです。

STORES の顧客一覧画面を例に出すと青枠部分体が対象となります。

Organismsでは以下のどちらかを満たすことをルールとしています。

  • グローバルな値の参照・更新するコンポーネント
  • 限定されたページでのみ利用される 比較的複雑なコンポーネント

限定されたページ、というのは例えば /pages/user/pages/settings といったページを指します。

ドメインレベルで使用するコンポーネントのみにすることで、Organisms相当であっても再利用性のないコンポーネントが膨大に作成されることを避けるようにしています。

複数のページで利用される再利用性の高いコンポーネントであればMolecules相当のコンポーネントとして扱うようにしています。 (この場合、Moleculesではグローバルな値への参照・更新を禁じているため、グローバルステートへの依存を排除することになります。)

Templatesと同じく各ページごとにフォルダを作成して、その内部にコンポーネント・hooksファイルを格納することが可能です。

- organisms
  - reservations
    - BaseComponents
      - components:BaseComponentsで使用するコンポーネント群
        - ChildComponent
          - index.stories.tsx
          - index.tsx
          - styles.module.tsx
      - hooks:BaseComponentsで使用するhooks
        - useReservationHooks
          - index.ts
      - index.tsx:コンポーネント
      - index.stoires.tsx:コンポーネントに対応するstories
      - styles.module.css:コンポーネントに対応するスタイル

Templatesと同じ理由で再利用性がない特定のOrganismsでの利用が前提となるコンポーネントをMoleculesに作成しないようにしています。

この運用によって、STORES 予約 内のmoleculesディレクトリに作成するコンポーネントの数を抑えることができています。

Molecules

Atomic DesignにおけるMoleculesと同義で、Atomsコンポーネントを組み合わせた再利用可能なコンポーネントを配置しています。 また、アプリケーション固有の情報を与えられるAtomコンポーネントもMoleculesに含めています。

例えば、粒度としては単なるボタンであるコンポーネントであっても、取得データによってラベルが変更されるボタンが当てはまります。

画像の青枠で囲ったタグ相当のボタンはスマートリスト機能で作成された名前が入るようになっています。 そのため、単なるボタンからアプリケーション固有の情報を与えられたボタンとなり、Molecules相当として扱っています。

画像赤枠の検索フォームも通常であればMolecules相当の再利用性のあるコンポーネントですが、検索コンポーネントは後述するSTANDで実装されています。

そのため、画像内のフォームは STORES 予約ではMoleculesとしては扱っていません。

枠線で囲った部分以外にも単独のMolecules/Atomsとして切り出すことが可能なコンポーネントがありますが、いずれも「再利用性のない特定のOrganismsでの利用が前提となるコンポーネント」となるため、Organismsフォルダ内のサブディレクトリに配置しています。

Moleculesでは以下をルールとしています。

  • 内部に状態を持つことは可能にしている
  • Contextによるグローバルな値の参照・更新は禁止

他の各層と同じくアプリケーション固有の情報を必要とする場合はサブディレクトリを作成しています。

- molecules
  - user
    - UserMoleculesComponent
      - index.stories.tsx
      - index.tsx
      - styles.module.css
  - reservation
  - settings

Atoms

Atomic DesginにおけるAtomsの定義と同じく、UIの単位としてそれ以上分けることができないコンポーネントを含めます。

Atomsでは以下をルールとしています。

  • 内部に状態を持たせない
  • props経由で状態を受け取る
  • Contextのアクセスは禁止

Organismsでの説明通り、ある特定のコンポーネントに依存するようなボタンを作成するような場合はそのコンポーネントのフォルダ内にコンポーネントとして切り出しています。

またMoleculesで述べた通り、アプリケーション固有の情報を与えられる場合は Molecules に含めるようにしています。

そのため、Atomsに切り出されるコンポーネントは特定のコンポーネントに依存するものではなく、共通利用されるもののみとなっています。

Atomsの粒度であればButtonやInputが含まれることになりますが、STORES ではSTANDというデザインシステムを運用しています。

STANDのコンポーネントは別途 stand ディレクトリ内で実装するルールを運用しています。

そのため、 STORES 予約 内で独自のButtonやInputを作成することはほとんどありません。

上記の理由から先ほどから例に出している STORES 予約の顧客一覧画面では Atoms ディレクトリから呼び出しているコンポーネントは存在しません。

この画面にはAtomsから参照しているコンポーネントは存在しない

STANDに関しては以下の記事をご参照ください。

speakerdeck.com

依存関係チェック

Eslintによる依存ルール

eslint-plugin-strict-dependencies を利用することで、依存関係を検査しています。

github.com

'strict-dependencies/strict-dependencies': [
  'error',
  [
    {
      module: 'src/atoms',
      allowReferenceFrom: [
        'src/molecules',
        'src/organisms',
        'src/templates',
        'src/pages',
      ],
      allowSameModule: false,
    },
    {
      module: 'src/molecules',
      allowReferenceFrom: [
        'src/organisms',
        'src/templates',
        'src/pages',
      ],
      allowSameModule: true,
    },
    {
      module: 'src/organisms',
      allowReferenceFrom: [
        'src/templates',
        'src/pages',
      ],
      allowSameModule: false,
    },
    {
      module: 'src/templates',
      allowReferenceFrom: ['src/pages'],
      allowSameModule: false,
    },
    {
      module: 'src/pages',
      allowReferenceFrom: ['__tests__'], // e2eテスト用のディレクトリ
      allowSameModule: false,
    },
    ~ その他のルール ~
  ],
],

上記は一部抜粋したルールとなります。実際には他ディレクトリを含めたルールや相対パスにおけるimportを禁止させることで、 strict-dependencies の適用を強制させています。

簡単な図にするとこのような依存関係となります。

Pagesを親とすると親 -> 子は参照可能だが、子 -> 親の参照は不可にしているAtomic Designに準拠したルールになります。

このルールが入る以前は依存関係が守られておらず 、OrganismsからTemplatesのファイルが呼び出されるなど本来のAtomic Designの思想から逸脱した運用を行なっている箇所がありました。

依存関係のチェックを加えることで、コンポーネントを作成するときに「このコンポーネントの粒度は何か」・「サブディレクトリに入れるべきか」を以前より考えるようになり、正しいコンポーネント配置が可能になりました。

課題は、依然として正しいディレクトリに配置されていないコンポーネントが存在し、eslint-disable で回避していることです。

導入前の段階で、正しいディレクトリ配置になるよう修正したのですが、影響度の大きいコンポーネントは移せずじまいでした。 そのため、依存関係のチェックで引っかからないよう正しい配置にしたり、コンポーネントの粒度を細かくして対応する必要があります。

おわりに

以上となります。

今回はコンポーネント分類と依存関係規約にフォーカスして紹介させていただきました。

この記事で、少しでも現在の STORES 予約 フロントエンド開発のイメージをつかんでいただければ幸いです。