Skip to content

フォルダ構成

📂 Next.jsのフォルダ構成:状態管理の統合

Section titled “📂 Next.jsのフォルダ構成:状態管理の統合”

Next.jsプロジェクトのフォルダ構成は、状態管理ライブラリを導入することでさらに複雑になります。クリーンアーキテクチャの原則と、機能ベースのBulletproof Reactの思想を組み合わせることで、状態管理コードを整理し、保守性とスケーラビリティを維持できます。

なぜフォルダ構成が重要なのか

Section titled “なぜフォルダ構成が重要なのか”

問題のあるフォルダ構成の影響

Section titled “問題のあるフォルダ構成の影響”

問題1: 機能が散在する構成

// 問題のある構成: ファイルタイプで整理
/components
/Button.tsx
/UserCard.tsx
/OrderList.tsx
/hooks
/useUser.ts
/useOrder.ts
/utils
/userUtils.ts
/orderUtils.ts
// 問題点:
// - 関連するコードが散在している
// - 機能の全体像が把握しにくい
// - リファクタリングが困難
// - チーム開発でコンフリクトが起きやすい

問題2: 責務の混在

📚 Reactの基礎について: useState、useEffect、コンポーネントの詳細については、Reactガイドを参照してください。

components/UserCard.tsx
// 問題のあるコード: コンポーネントにビジネスロジックが混在
// 注意: useStateとuseEffectの詳細については、Reactガイドを参照してください
export default function UserCard({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 問題: API呼び出しがコンポーネント内にある
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
// 問題: ビジネスロジックがコンポーネント内にある
const calculateAge = (birthDate: string) => {
const today = new Date();
const birth = new Date(birthDate);
return today.getFullYear() - birth.getFullYear();
};
return <div>{user?.name} ({calculateAge(user?.birthDate)})</div>;
}

解決: 機能ベースの構成

// 解決: 機能ごとに整理
/features
/users
/components
/UserCard.tsx
/hooks
/useUser.ts
/api
/userApi.ts
/utils
/userUtils.ts
/types
/user.ts
// 利点:
// - 関連するコードが一箇所に集約
// - 機能の全体像が把握しやすい
// - リファクタリングが容易
// - チーム開発でコンフリクトが起きにくい

解決: 責務の分離

📚 カスタムフックの詳細: カスタムフックの作成方法については、ReactガイドのuseStateとuseEffectの「カスタムフックへの分離」セクションを参照してください。

features/users/hooks/useUser.ts
// 解決: ビジネスロジックを分離
// 注意: useState、useEffect、カスタムフックの詳細については、Reactガイドを参照してください
export function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
userApi.getUser(userId).then(setUser);
}, [userId]);
return user;
}
// features/users/utils/userUtils.ts
export function calculateAge(birthDate: string): number {
const today = new Date();
const birth = new Date(birthDate);
return today.getFullYear() - birth.getFullYear();
}
// features/users/components/UserCard.tsx
export default function UserCard({ userId }: { userId: string }) {
const user = useUser(userId);
const age = user ? calculateAge(user.birthDate) : null;
return <div>{user?.name} ({age})</div>;
}

1. 概念的なレイヤーの分離 🏛️

Section titled “1. 概念的なレイヤーの分離 🏛️”

クリーンアーキテクチャでは、コードを以下の3つのレイヤーに分離します。この階層構造により、外側のレイヤー(UI)は内側のレイヤー(データ)に依存しますが、その逆はありません。

  • UI層(プレゼンテーション) 🖥️

    • ユーザーインターフェース(UI)の表示と入力処理を担当します。Next.jsでは、app/ディレクトリ内のコンポーネントやページがこれに該当します。
  • アプリケーション層(ビジネスロジック) 🛠️

    • アプリケーション固有の機能やルールを定義します。ユーザー作成や注文処理といった、具体的なビジネスロジックがここに集約されます。
  • データアクセス層(インフラストラクチャ) 💾

    • データベース、外部API、ファイルシステムなどのデータソースとのやり取りを扱います。この層は、データ永続化の詳細を抽象化します。

これらのレイヤーを具体的なNext.jsのフォルダ構成に落とし込むには、機能ベースのBulletproof Reactの思想が役立ちます。

  • app/

    • Next.jsのルーティングとUI層を担います。ここには、ページ (page.tsx) や共有レイアウト (layout.tsx) など、ユーザーに直接表示されるコードを配置します。ビジネスロジックはここには含めず、シンプルに保ちます。
  • src/

    • プロジェクトの主要なコードを格納する推奨ディレクトリです。この中に、機能や共通コードを分離します。
  • src/features/(アプリケーション層)
    • アプリケーションの主要な機能(例:auth、user、blog)をフォルダごとに整理します。各機能フォルダ内には、その機能に特化したコンポーネント、フック、API呼び出しなどが含まれます。
/src/features/auth/
├── components/
├── hooks/
└── api/
  • src/lib/(データアクセス層)
    • データベース接続、外部APIクライアント、認証ライブラリなど、アプリケーション全体で再利用されるインフラストラクチャ関連のコードを配置します。
/src/lib/
├── db.ts // データベース接続
├── auth.ts // 認証ヘルパー
└── utils.ts // 汎用ユーティリティ
  • src/components/(UI層の共有コンポーネント)

    • アプリケーションの複数の場所で再利用される、汎用的なUIコンポーネント(例:Button.tsxModal.tsx)を配置します。
  • src/styles/

    • グローバルなスタイルやテーマを定義します。

状態管理コードを導入する際は、そのコードをUIから分離し、アプリケーション層に配置するのがベストプラクティスです。

特定の機能に関連する状態は、その機能のディレクトリ内に配置します。

例: features/cart/ ディレクトリ

/src/features/cart/
├── components/
│ └── CartDisplay.tsx
├── store/ // 状態管理コードを配置
│ ├── cartSlice.ts
│ └── cartSelector.ts
└── hooks/
└── useCart.ts

アプリケーション全体で共有される、どの機能にも属さない状態(例:テーマ設定、言語設定)は、src/storeのようなルートレベルのディレクトリに配置します。

例: src/store/ ディレクトリ

/src/store/
├── theme/
│ └── themeSlice.ts
├── user/
│ └── userSlice.ts
└── index.ts

4. まとめ:なぜこの構成が優れているのか? 💡

Section titled “4. まとめ:なぜこの構成が優れているのか? 💡”

このアプローチは、以下の点でアプリケーションの品質を向上させます。

  • 責務の分離: UI、ビジネスロジック、状態管理、データアクセスが明確に分離されます。これにより、開発者は関心のあるレイヤーに集中できます。

  • 再利用性: 状態管理のロジックはUIから独立しているため、異なるUIコンポーネントから同じ状態管理ロジックを再利用できます。

  • テストの容易性: 各レイヤーが独立しているため、UIやデータ層に依存することなく、状態管理ロジックを単独でテストできます。

  • スケーラビリティ: 新しい機能を追加する際、既存の機能に影響を与えることなく、新しいfeaturesディレクトリと関連する状態管理コードを追加できます。

アーキテクチャパターンの選択判断

Section titled “アーキテクチャパターンの選択判断”

機能ベース構成 vs レイヤーベース構成

Section titled “機能ベース構成 vs レイヤーベース構成”

機能ベース構成(Feature-Based):

/features
/auth
/users
/orders
/products

メリット:

  • 関連するコードが一箇所に集約
  • 機能の追加・削除が容易
  • チーム間の責務が明確

デメリット:

  • 共通コードの重複のリスク
  • 横断的関心事(認証、ロギング)の扱いが難しい

レイヤーベース構成(Layer-Based):

/components
/hooks
/utils
/api

メリット:

  • 共通コードの再利用が容易
  • 横断的関心事の管理が簡単

デメリット:

  • 関連するコードが散在
  • 機能の全体像が把握しにくい

推奨: ハイブリッド構成

/src
/features # 機能ベース(主要な機能)
/auth
/users
/orders
/components # レイヤーベース(共通UI)
/ui
/layout
/lib # レイヤーベース(インフラ)
/api
/db
/utils

小規模プロジェクト(1-3人、< 50コンポーネント):

/app
/components
/lib

中規模プロジェクト(4-10人、50-200コンポーネント):

/app
/src
/features
/components
/lib

大規模プロジェクト(10+人、200+コンポーネント):

/app
/src
/features
/auth
/components
/hooks
/api
/store
/shared
/components
/hooks
/utils
/lib

判断基準:

features/users/components/UserCard.tsx
// 1. 機能固有のコンポーネント → features配下
// - ユーザー機能にのみ使用される
// 2. 複数の機能で使用される → shared/components配下
// shared/components/ui/Button.tsx
// - アプリケーション全体で使用される
// 3. レイアウト関連 → components/layout配下
// components/layout/Header.tsx
// - レイアウト構造に関連する

判断基準:

features/cart/store/cartSlice.ts
// 1. 機能固有の状態 → features配下
// - カート機能にのみ関連
// 2. グローバルな状態 → store配下
// store/theme/themeSlice.ts
// - アプリケーション全体で使用
// 3. サーバー状態 → features配下のapi
// features/users/api/userApi.ts
// - React QueryやSWRで管理

このようなフォルダ構成は、初期のセットアップに少し時間がかかりますが、長期的に見ると、チーム開発や大規模なプロジェクトにおいて、コードの管理と保守を大幅に簡素化します。

シニアエンジニアとして考慮すべき点:

  1. プロジェクトの規模: 小規模ならシンプルに、大規模なら構造化
  2. チームの構成: 機能チームなら機能ベース、専門チームならレイヤーベース
  3. 将来の拡張性: プロジェクトの成長を見据えた設計
  4. 一貫性: チーム全体で統一された構成を維持