Reactの流行りのディレクトリ構成を再考する
feature分割とatomic design
Reactにおいて「feature単位でディレクトリを切る」「atomic designに従ってコンポーネントを分類する」といった記事が定期的に書かれていると思います。
しかしこれらは、本当に現場に適しているのでしょうか?
この記事では、両者の問題点を深掘りし批判的に考察します。
featureベース構成の問題点
1 featureを使う判断が各人で異なる
feature単位の構成とは、たとえば以下のようなものです。
src/
features/
User/
UserProfile.tsx
UserSettings.tsx
Admin/
AdminDashboard.tsx
AdminUserManagement.tsx
一見、「機能ごとに綺麗にまとまっている」ように見えます。
しかし、実際にプロジェクトが進行すると必ず直面します。
- 「これってUserに属するの?Adminに属するの?」
- 「共通フォームはどっちに置く?」
- 「通知機能は独立したfeatureにすべきか?」
つまり、featureの境界線がブレるのです。
設計書に書いた定義など、現場の運用ではすぐに形骸化します。
レビュワーが変わるたびに、「独立したfeatureとみなすか」「既存featureに含めるか」が微妙に違うため、結局分類がバラバラになります。
2 スパゲティな密結合が起こりやすい
featureを意識すると、「feature内で完結すべき」という意識は一応働きます。
しかし、現実はそんなに甘くありません。
// features/User/UserProfile.tsx
import { getAdminInfo } from '../Admin/adminUtils';
export function UserProfile() {
const adminInfo = getAdminInfo();
// ...
}
このように、ちょっとだけ他featureを参照する、というケースは頻発します。
最初は小さな依存でも、積み重なると収拾がつかなくなります。
さらに、TypeScriptで型まで共有し始めると、こうなります。
// features/User/types.ts
import { AdminUser } from '../Admin/types';
export interface UserWithAdmin extends AdminUser {
userProfileId: string;
}
もはやfeatureの境界は崩壊しています。
小さな依存が積もり積もって、巨大なスパゲティが生まれるのです。
3 共通モジュールを作る判断が遅れ、乱立する
逆にfeature単位に閉じようと意識するあまり、同じようなユーティリティや型定義が、各featureの中にコピペされて増殖します。
典型例:
// features/User/utils/formatDate.ts
export function formatDate(date: Date) { /* ... */ }
// features/Admin/utils/formatDate.ts
export function formatDate(date: Date) { /* ほぼ同じコード */ }
これに気づいて直せるかどうかは実装者・レビュワーの体調と記憶力に依存します。
本来、共通モジュールにすべきタイミングを逃す原因になります。
4 巨大feature化して崩壊する
「featureごとにまとめる」というルールは、逆に「その中で何でもかんでも足していい」という免罪符になりがちです。
特にUserやAdminのような巨大機能は、以下のような末路をたどります。
features/
User/
UserProfile.tsx
UserSettings.tsx
UserNotifications.tsx
UserDashboard.tsx
UserSubscriptionManagement.tsx
UserPrivacySettings.tsx
...
もはや「feature」ではなく、小さなアプリです。
結果、内部構造の整理が必要になるのに、初期の「featureでまとめればいい」という設計思想が足枷となり、リファクタリングできないまま肥大化していきます。
atomic design構成の問題点
1 原典はデザイン理論、コードに落とし込むのが難しい
atomic designは以下のような概念です。
- Atom:最小単位(ボタン、テキストフィールド)
- Molecule:小さな組み合わせ(ラベル+入力)
- Organism:さらに大きな組み合わせ(フォーム全体)
- Template / Page:レイアウト構成
これをそのままコード構成に落とし込むとこうなります。
src/
components/
atoms/
Button.tsx
Input.tsx
molecules/
FormField.tsx
organisms/
SignUpForm.tsx
一見、秩序がありそうです。
しかし、運用するとすぐに詰まります。
- 「このFormFieldってmoleculeで合ってる?」
- 「この複雑なInputは、organism?でもデザイン的にmolecule入れたい…」
分類がブレブレになり、作るたびに迷子になります。
2 オレオレatomic designになる
atomic designを実装に落とすとき、絶対に全員がチームでの独自解釈を作成します。
- 「atomsにも少しロジック持たせよう」
- 「moleculesだけlocal state持っていいことにしよう」
- 「organismsはredux接続してもいい」
これによってもはやatomic designと呼べない異形の構成が生まれます。
「うちのプロジェクトのatomic designルール」みたいなものが乱立し、「atomic designって何?」という状態に派生します。
3 organismが爆発する
一度でもatomic designをやった現場ならわかるはずです。
- atoms、moleculesは最初は手を付けるがほぼ増えない
- organismsの数が爆発する
- 結局organismsをfeature的な分け方をして密結合になる
つまり、atomsとmoleculeが形骸化され、ほぼorganismsディレクトリだけ使われる現象が起こります。
その対処をしようとしてfeatureっぽい構成を導入し密結合、ひいてはスパゲティ化します。
なぜこれらが流行ったのか
- 名前がキャッチーだった(feature、atomic design)
- 分割の仕方が意味深でかっこよかった
- ディレクトリ構成の銀の弾丸になってくれそうだった
しかし、開発者一人ひとりが成長していくと、「このルール本当に役立ってるのか?」と疑問に思う瞬間が必ず訪れます。
一応の代替案
批判するのに代替案を出さないのはずるいと思うので一応私の考えも共有します。
コメントで批判いただけるといろいろバランス取れるかなと思います。
私が提案するのはページ単位の局所的コンポーネントのシンプルな構成です。
構成例
src/
app/
(home)/
index.tsx
_components/
Hero.tsx
FeatureSection.tsx
_lib/
fetchHomeData.ts
settings/
index.tsx
_components/
ProfileForm.tsx
ChangePasswordForm.tsx
_lib/
updatePassword.ts
components/
Button.tsx
Modal.tsx
ポイント
- ページ内で完結するものはページ配下に置く
- 汎用的に使うものだけトップレベルに置く
- コピー&ペーストはある程度許容(無理な共通化はしない)
- フォルダが肥大化したら、その原因に合わせて都度対処する(トップレベルのフォルダを増やすなど)
これにより、
- スパゲティな密結合は起きにくい
- 分類で迷うことがない
- 初見でも構成が理解しやすい
という実用的なメリットが得られます。
まとめ
feature設計もatomic design設計も、銀の弾丸にはなりえません。
現場では次のことが大事です。
- スパゲティを防げるか
- チーム全体で統一できるか
- 運用コストを最小化できるか
銀の弾丸を探して構成を選ぶのではなく、自分たちのチームとプロジェクトにあう構成を愚直に考え続けることが、何より重要だと思います。
Discussion