💡

フロントエンドのディレクトリ設計を移行した話 | Atomic Design → Feature

2024/02/14に公開

はじめに

この記事は、業務でフロントエンドのディレクトリ設計思想を変更した際の作業をまとめた記事です📕

それなりの規模のプロジェクトでの移行作業のため、新規機能実装などに影響が出にくいようにリファクタリングを進めてきました。そこでの進め方や感想も含めてお伝えできればと思います。

前提

6年ほど運用しているReact Nativeのプロジェクトでの移行作業のお話です。
従来のディレクトリ設計思想はAtomic Designを採用していました。しかし、organismsのコンポーネントが300個近くにも及び、プロジェクトの規模が大きくなるにつれて様々な課題が浮き彫りになりました。これを機に設計思想の移行を決めました。

Layer型のAtomic DesignからFeature型のディレクトリ構造に移行していこうと思います😌

参考: ソフトウェアアーキテクチャの基礎

なぜ移行したか

なぜ移行することにしたかは大きく分けて2つ理由があります。

  • Atomic Designの定義の解釈が人それぞれで階層分けが難しい
  • 依存管理が難しくロジックがコンポーネントに分散されているため影響範囲の把握が難しい

Atomic Design の定義の解釈が人それぞれで階層分けが難しい


https://bradfrost.com/blog/post/extending-atomic-design/ から引用)

Atomic Designを使用したことがある人ならmoleculesの使い方で一度は悩んだことがあるかと思います。moleculesは状態はなるべく持たず、単独のatomsよりも具体的な機能を提供するコンポーネントです。しかしorganismsとの差別化がとても難しく、実装中に

organismsとmoleculesどちらにコンポーネント作ったらいいのかな?

と悩むこともあり、人によって定義が異なってしまっていました。そのため、いまいちmoleculesの使い方が統一されないままコンポーネントが多く作られたような状態になりやすいコンポーネント粒度でした。
※moleculesには150個ほどのコンポーネントがありました💧

依存管理が難しくロジックがコンポーネントに分散されている

Atomic DesignはLayer型なので「技術単位での分割」が基本になります。そしてorganismsには各サービスで使用される意味のある単位の塊で定義するため見た目の粒度だけでなく、ロジックの責務も明確にできるメリットがあります。

しかし規模が大きくなりコンポーネントの数が多くなってしまうと、画面やコンポーネントを新規で作成する際に、既存のロジックを持ったコンポーネントに気づかず重複してロジックを作成されることがありました。
その結果テストコードが重複したり、仕様の変更の際に抜け漏れてしまう懸念がありました。

Feature型に移行するメリット

  • ディレクトリ単位で関心を分離するためコードリーディング、管理しやすい
  • 影響範囲が明確にしやすく、変更容易性が高い
  • 特定のページでしか使われないコンポーネントは、該当のページのディレクトリ内に配置するために横断的に利用する汎用的なコンポーネントがごちゃつかない

ディレクトリ単位で関心を分離するためコードリーディング、管理しやすい

各機能やページごとに関連するファイル(Components、Hooks、Providers)を1つのディレクトリ内でまとめます。規模が大きくなるにつれてHooksやlibsなどカスタムフックにしたファイルが多くなり複雑になってしまうケースが多くありますが、特定の機能に関連するすべてのコードを一箇所にまとめることで簡単に見つけることができます。

今回例に出しているのはHomeというドメインもとに例を出していますが、Home機能に関連するコンポーネントやロジックを修正する場合、/features/Homeディレクトリ内のファイルのみを調査すれば良くなるので調査や修正がしやすいコードになります。

影響範囲が明確にしやすく、変更容易性が高い

変更時の影響範囲が明確になり、変更が容易になります。各機能が独立したディレクトリ内に閉じられているため、ある機能に加えた変更が他の機能に意図せず影響を与えるリスクが減り保守性が高いコードになります。

さらに、機能ごとにテストをグループ化することも容易になります。フロントエンドのテストとしてシナリオが重要な観点になりますが、Feature型では特定の機能に関連するテストの実行と管理がシンプルになります。

横断的に利用する汎用的なコンポーネントがごちゃつかない

特定のページや機能でのみ使用されるコンポーネントを、それぞれのディレクトリ内に配置します。一方で、複数の場所で再利用可能な汎用コンポーネントは、partsやlayoutなどの中央集約されたディレクトリに保持します。これにより、横断的に利用されるコンポーネントと特定の機能に固有のコンポーネントが明確に区別され、プロジェクトの構造が整理されます

移行してみる

進め方

定期リリースに極力影響が出ないように進めていきたいので以下にように段階に分けて対応を進めていきました。

  • STEP① 汎用性の高いComponentsをまとめるディレクトリを作成する

    • atoms → parts
      • partsディレクトリを作成
      • atomsディレクトリの中身を移動
    • organisms、molecules → layout
      • layoutディレクトリを作成
      • organisms, moleculesディレクトリの中身を移動
    • src/components/pages → src/pages
      • src直下にpagesを先に移動した方がのちの作業でコンフリクトが起きにくいので先に移動
  • STEP② 画面・ドメイン毎に管理するディレクトリを作成する

    • featuresに移行完了した1つ大きいドメインを作成する。(STEP3でメンバーに共有するための参考PRを作成する)
      • featuresディレクトリを作成
      • templatesディレクトリの中身を移動
        • トップコンポーネントのindex.tsx以外のpagesディレクトリのを移動
  • STEP③ チーム全体で進めていく

    • チームで新しい設計思想に慣れてもらうためにFeature型にリファクタリングするissueを作成しいつでも手が空いた際に着手できる状態にする
    • 新規で画面を作成する際にはFeature型で書いてもらうように周知
      • 事前共有と参考PR・ドキュメントを用意

STEP①とSTEP②(featuresディレクトリに移行までは完了したドメインを最低1つ作成するところまで)は一人でやった方が効率が良いので自分の方で行いました。

その後、STEP③ではチーム全体で進めていくことにしました。
新規の開発に影響が少ないように徐々に移行することを心がけ参考PRとドキュメントをもとにリファクタリングissueを作成し、進めています。

【before】 移行前のディレクトリ構造

移行前のディレクトリ構造は以下のようになっていました。

.
├── components/                      # UIコンポーネント
│   ├── atoms/                       # 最小単位のコンポーネント
│   │   └── Text/                    
│   │       ├── Text.tsx             # テキストコンポーネント
│   │       └── Text.stories.tsx     # Textコンポーネントのストーリー
│   │
│   ├── molecules/                   
│   │   └── Home/                    
│   │       ├── item.tsx             # Home特有のitemコンポーネント
│   │       └── item.stories.tsx     # itemコンポーネントのストーリー
│   │
│   ├── organisms/                   
│   │   └── Home/
│   │       ├── Assignment.tsx       # 別画面でも利用されるAssignmentコンポーネント
│   │       ├── Assignment.stories.tsx  # Assignmentコンポーネントのストーリー
│   │       ├── items.tsx            # Home特有のitemsコンポーネント
│   │       └── items.stories.tsx    # itemsコンポーネントのストーリー
│   │
│   └── templates/                   # ページテンプレート
│       └── Home/                    
│           ├── Page.tsx             # ページレイアウト
│           └── Page.stories.tsx     # Pageコンポーネントのストーリー
│
├── hooks/                           # アプリケーション全体のカスタムフック
│   |── app.tsx                      # 共通カスタムフック
│   └── useHome.tsx                  # Homeページ用のカスタムフック
│
├── providers/                       # アプリケーション全体のコンテキストプロバイダー
│   |── app.tsx                      # 共通コンテキストプロバイダ
│   └── homeProvider.tsx             # Homeページ用のコンテキストプロバイダー
│
└── pages/                           # ページコンポーネント
    └── Home/                        
        ├── __tests__/               # テストファイル
        │   ├── index.test.tsx       # Homeページのテスト
        │   └── index.apollo.tsx     # Apolloクライアントのテスト
        ├── Connected.tsx            # APIアクセスやアクションハンドリング
        ├── Plain.tsx                # データ整形やエラーハンドリング
        └── index.tsx                # ルーター登録、グローバルステート参照

【after】 移行後のディレクトリ構造

移行後のディレクトリ構造は以下のように改善しました💡

.
├── components/                  # 共通コンポーネントのディレクトリ
│   ├── parts/                   # 共通で利用するatomsのイメージ
│   │   ├── Text.tsx             # テキストコンポーネント
│   │   └── Text.stories.tsx     # テキストコンポーネントのストーリー
│   │
│   └── layout/                  # 共通で利用するorganisms、moleculesのイメージ
│       ├── Assignment/          # Assignmentコンポーネント
│       │   ├── Assignment.tsx         # 別画面でも利用されるAssignmentコンポーネント
│       │   └── Assignment.stories.tsx   # Assignmentコンポーネントのストーリー
│       │
│       └── Order/               # Orderコンポーネント (別のドメイン)
│
├── features/                    # 機能・ドメインごとのコンポーネント
│   └── Home/                    # Home機能
│       ├── components/
│       │   ├── ListItem         # ListItemコンポーネント群
│       │   │   ├── item.tsx             # Home特有のitemコンポーネント
│       │   │   ├── item.stories.tsx     # itemコンポーネントのストーリー
│       │   │   ├── items.tsx            # Home特有のitemsコンポーネント
│       │   │   └── items.stories.tsx    # itemsコンポーネントのストーリー
│       │   │
│       │   ├── Connected.tsx            # APIアクセスやアクションハンドリング
│       │   ├── Plain.tsx                # データ整形やエラーハンドリング
│       │   ├── Page.tsx                 # ページコンポーネント
│       │   └── Page.stories.tsx         # ページコンポーネントのストーリー
│       │
│       ├── hooks/               # Home固有のカスタムフック
│       └── providers/           # Home固有のコンテキストプロバイダ
│
├── pages/                       # ページのトップコンポーネント
│   └── Home/
│       └── index.tsx            # Homeページのトップコンポーネント
│
├── hooks/                       # アプリケーション全体のカスタムフック
│   └── app.tsx                  # 共通カスタムフック
│
└── providers/                   # アプリケーション全体のコンテキストプロバイダー
    └── app.tsx                  # 共通コンテキストプロバイダ

移行してみた感想

  • ディレクトリの移動はvscode(Visual Studio Code)にはパス自動更新機能があるのでスムーズにできたがorganisms、molecules → layoutに移動する際には名前が同じディレクトリが多数存在したため上書きができず大変😥
  • コンフリクトが起きやすい作業PRのマージは回数を減らすためにまとめたのが良かった
    • ディレクトリ間の移動が多いのでコンフリクトが起きやすいのは STEP① でまとめていたため影響を極力少なくできた。
  • コンポーネント内のパスの管理をチーム内で認識のすり合わせをする必要があった
    • 横断的に使っているコンポーネントとローカルのコンポーネントの利用をわかりやすいように相対パスを利用する
      • src/Home/components/ItemList/Item.tsx../ItemList/Item.tsx
  • ルールをチーム内統一する際に負荷がかかりにくいようにできた
    • ディレクトリ思想に一致するコンポーネントを作りやすくする
      • 新規で画面を作成する際に .hygen を使用してコンポーネントを自動生成できるようにする
Loaded templates: .hygen
       added: src/features/NewPage/components/Connected.tsx
       added: src/pages/NewPage/index.tsx
       added: src/features/NewPage/components/Page.tsx
       added: src/features/NewPage/components/Plain.tsx
       added: src/features/NewPage/providers/NewPageProvider.tsx
       added: src/features/NewPage/components/Page.stories.tsx
       added: src/pages/NewPage/__tests__/index.test.tsx
       added: src/features/NewPage/hooks/useQuery.tsx
  • 設計思想を変えることのモチベーションがメンバーごとに異なることの理解とコミュニケーションが必要
  • Feature型移行の作業に取りかかりやすいタスク調整とissue作成

最後に

Atomic Designを4年間使用してきた経験から、新しい設計思想に完全に慣れるにはまだ時間がかかると感じていますが、ページ・ディレクトリ単位でドメインが管理されている方が影響範囲が明確でわかりやすいと感じました。
どうしても規模が大きくなってしまうとAtomic Designでは共通コンポーネントが紛れてしまいわかりにくくなるのが課題に感じていたのでそこが解消できるといいなと思っています😊

まだSTEP③を運用中になるので今後チーム間で何か課題が出るかもしれないですが、その際は追記しようと思います🙇‍♂️

参考文献

Discussion