4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

新人エンジニアは知らないとやばい😱 「責務の分離」ってなーに?🤔( フロントエンドで解説)

4
Posted at

なぜ責務の分離が大切なのか

新人エンジニアが直面する「現場のコードが読みにくい」問題があると思います

ex)
コードがごちゃごちゃして読みにくい
どこから修正すればいいんだ汗
影響調査がうまくいかん(ここを変更したことで他の機能に影響ないかな汗)

これは責務があって、ごちゃごちゃになっているのが原因です。

良かったら参考までに

そもそも責務って何?🤔

なかなかスクールとか研修で教えてくれなかったりしますよね💦
簡単に言うと、そのコードが担当すべき仕事・役割のことです。
プログラミング的に言うと、
コンポーネント/関数/クラスごとに「何を担当するか」を明確にすることです。
これがないとどのファイルが何をやっているのか?がわからなくなり、
コードがぐちゃぐちゃになって他のメンバーが読みづらくなってしまいます。汗

責務の分離とは何か

コードを整理整頓するイメージです。
つまりコードやファイルの役割分担をして、
各ファイルやコードの役割を明確にしていく
ことです。

カバンの整理で責務の分離を例えてみる

悪い例: ぐちゃぐちゃのカバン

責務の分離をしていないと、財布も、スマホも、ペンも、飲み物も、充電器も
全部メインポケットに放り込んでいる状態です。

これだと「どこに何をしまったか」わからなくなってしまいますよね。
電車に乗る時、「あれ定期はどこに行った?💦」って毎回カバンをひっくり返して探すハメに...

良い例: 整理整頓されたカバン

  • 外ポケット → スマホ、定期(すぐ使うもの)
  • メインポケット → 財布、ペンケース、PC、会社の資料、家の鍵(メインアイテム、貴重品)
  • 小物入れ → イヤホン、充電器(細かいもの)

これなら必要なものがすぐ取り出せますよね🔥

コードも同じです

カバンと同じように、コードも意味・役割ごとに分けて整理すれば、
「あのコードどこだっけ?」と探す時間が減ります。

そして何より、他の人(チームメンバーや未来の自分)が見た時に、
どこに何があるか一目でわかる
ようになります👀

💡 AIに任せっきりだと危険

ChatGPTやClaudeCodeが生成したコードをそのまま使っていると、
どんどんコードがごちゃごちゃになっていきます。
だからこそ、人間が責任を持って整理整頓する力が必要です。

ダメな例から良い例で学ぶ

実際のコードで見ていきましょう!
ユーザー一覧を表示する画面を例に、責務の分離を実践します。

ダメな例(全部入りコンポーネント)

app/users-bad/page.tsx
'use client';

import { useEffect, useState } from 'react';

export default function UsersPage() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [searchTerm, setSearchTerm] = useState('');

  // API通信
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchUsers();
  }, []);

  // フィルタリング処理
  const filteredUsers = users.filter((user) =>
    user.name.toLowerCase().includes(searchTerm.toLowerCase())
  );

  // ローディング表示
  if (loading) {
    return (
      <div className="text-center py-12">
        <div className="text-2xl">読み込み中...</div>
      </div>
    );
  }

  // エラー表示
  if (error) {
    return (
      <div className="text-red-600 p-5">
        <h2 className="text-xl font-bold">エラーが発生しました</h2>
        <p>{error}</p>
      </div>
    );
  }

  // メイン表示
  return (
    <div className="p-5 max-w-3xl mx-auto">
      <h1 className="text-3xl font-bold mb-5">ユーザー一覧</h1>
      
      {/* 検索フォーム */}
      <div className="mb-5">
        <input
          type="text"
          placeholder="ユーザー名で検索..."
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          className="w-full p-3 text-base border border-gray-300 rounded"
        />
      </div>

      {/* ユーザーリスト */}
      <div>
        {filteredUsers.length === 0 ? (
          <p className="text-gray-500">ユーザーが見つかりませんでした</p>
        ) : (
          filteredUsers.map((user) => (
            <div
              key={user.id}
              className="border border-gray-200 rounded-lg p-4 mb-3 bg-gray-50"
            >
              <h3 className="text-xl font-semibold mb-2">{user.name}</h3>
              <p className="text-gray-600 mb-1">📧 {user.email}</p>
              <p className="text-gray-600 mb-1">🏢 {user.company.name}</p>
              <p className="text-gray-600">📍 {user.address.city}</p>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

😰 何が問題なのか?

このコード、一見動くし、「別にいいんじゃない?」って思うかもしれません。

でも実際にこのコードで開発を続けると...

1. スクロール地獄になる

  • 1ファイルに100行以上
  • 「あのコードどこだっけ?」って毎回スクロール
  • 修正箇所を探すだけで疲れる

2. 何がどこにあるか分からない

  • API通信のコード
  • フィルタリングのロジック
  • 見た目のコンポーネント
  • 全部ごちゃ混ぜ → 「どこ直せばいいの?」状態

3. 再利用できない

  • 他のページでもユーザー情報を使いたい
  • でもこのコードはコピペするしかない
  • 同じコードがあちこちに...保守地獄の始まり💀

4. チーム開発が地獄

  • Aさんが検索機能を修正中
  • Bさんが表示デザインを変更中
  • → 同じファイルを同時に触ってコンフリクト祭り🎆

5. テストが書けない

  • 「API通信だけテストしたい」→ 無理
  • 「検索ロジックだけテストしたい」→ 無理
  • 全部が絡み合ってるからテスト不可能

✅ 良い例: 責務を分離したコード

さっきのコードを機能ごとに整理整頓してみましょう!

📁 ファイル構成

src/
  app/
    users/
      page.tsx                       # ページ(統合のみ)
  
  features/
    user/
      components/
        UserCard.tsx                 # ユーザーカード
        UserList.tsx                 # ユーザーリスト
        UserSearchInput.tsx          # 検索フォーム
      api/
        userApi.ts                   # API通信
      hooks/
        useUsers.ts                  # ユーザー取得
        useUserSearch.ts             # 検索ロジック
      types/
        user.ts                      # 型定義
      index.ts                       # 公開API
  
  shared/
    components/
      LoadingSpinner.tsx             # ローディング
      ErrorMessage.tsx               # エラー表示
    lib/
      fetcher.ts                     # 共通fetch

カバンで例えると:

Bad: 全部メインポケットにぐちゃぐちゃ
Good: 小物入れ、外ポケット、メインポケットに分類

1️⃣ 共通部品を作る(shared)

全機能で使い回せる部品を作ります。

src/shared/components/LoadingSpinner.tsx
export const LoadingSpinner = () => {
  return (
    <div className="text-center py-12">
      <div className="text-2xl">読み込み中...</div>
    </div>
  );
};
src/shared/components/ErrorMessage.tsx
type Props = {
  message: string;
};

export const ErrorMessage = ({ message }: Props) => {
  return (
    <div className="text-red-600 p-5">
      <h2 className="text-xl font-bold">エラーが発生しました</h2>
      <p>{message}</p>
    </div>
  );
};
src/shared/lib/fetcher.ts
export const fetcher = async <T,>(url: string): Promise<T> => {
  const response = await fetch(url);
  
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  
  return response.json();
};

💡 ポイント:

  • LoadingSpinnerErrorMessageどの機能でも使える
  • 他の機能(投稿一覧、商品一覧など)でも再利用可能
  • 一度作ればずっと使える!

2️⃣ ユーザー機能を作る(features/user)

型定義

まずはデータの形を決めます。

src/features/user/types/user.ts
export type User = {
  id: number;
  name: string;
  email: string;
  company: {
    name: string;
  };
  address: {
    city: string;
  };
};

💡 ポイント:

  • TypeScriptの型を別ファイルに分離
  • 「ユーザーってこういうデータだよ」が一目瞭然
  • 他のファイルでもimport type { User }で使える

API通信

外部との通信を専門に担当するファイル。

src/features/user/api/userApi.ts
import { fetcher } from '@/shared/lib/fetcher';
import type { User } from '../types/user';

const BASE_URL = 'https://jsonplaceholder.typicode.com';

export const userApi = {
  // 全ユーザー取得
  getAll: () => fetcher<User[]>(`${BASE_URL}/users`),
  
  // 特定ユーザー取得
  getById: (id: number) => fetcher<User>(`${BASE_URL}/users/${id}`),
};

💡 ポイント:

  • API通信はここだけ見ればOK
  • 「APIのURLを変更したい」→ ここだけ修正
  • 他のコンポーネントは影響を受けない

カスタムフック(ロジック)

ビジネスロジックを担当。

src/features/user/hooks/useUsers.ts
import { useEffect, useState } from 'react';
import { userApi } from '../api/userApi';
import type { User } from '../types/user';

export const useUsers = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const loadUsers = async () => {
      try {
        setLoading(true);
        const data = await userApi.getAll();
        setUsers(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : '不明なエラー');
      } finally {
        setLoading(false);
      }
    };

    loadUsers();
  }, []);

  return { users, loading, error };
};

💡 ポイント:

  • 「ユーザーデータを取得する」ロジックだけに集中
  • 他のページでもconst { users } = useUsers()で使える
  • テストも書きやすい!

src/features/user/hooks/useUserSearch.ts
import { useMemo, useState } from 'react';
import type { User } from '../types/user';

export const useUserSearch = (users: User[]) => {
  const [searchTerm, setSearchTerm] = useState('');

  const filteredUsers = useMemo(() => {
    if (!searchTerm) return users;
    
    return users.filter((user) =>
      user.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [users, searchTerm]);

  return {
    searchTerm,
    setSearchTerm,
    filteredUsers,
  };
};

💡 ポイント:

  • 検索ロジックを分離
  • useMemoでパフォーマンス最適化
  • 「検索の仕様を変えたい」→ ここだけ修正

コンポーネント(見た目)

UIだけを担当。ロジックは一切持たない。

src/features/user/components/UserSearchInput.tsx
type Props = {
  value: string;
  onChange: (value: string) => void;
};

export const UserSearchInput = ({ value, onChange }: Props) => {
  return (
    <input
      type="text"
      placeholder="ユーザー名で検索..."
      value={value}
      onChange={(e) => onChange(e.target.value)}
      className="w-full p-3 text-base border border-gray-300 rounded"
    />
  );
};

💡 ポイント:

  • 見た目だけに集中
  • propsで値を受け取るだけ
  • デザイン変更したい → ここだけ触ればOK

src/features/user/components/UserCard.tsx
import type { User } from '../types/user';

type Props = {
  user: User;
};

export const UserCard = ({ user }: Props) => {
  return (
    <div className="border border-gray-200 rounded-lg p-4 mb-3 bg-gray-50">
      <h3 className="text-xl font-semibold mb-2">{user.name}</h3>
      <p className="text-gray-600 mb-1">📧 {user.email}</p>
      <p className="text-gray-600 mb-1">🏢 {user.company.name}</p>
      <p className="text-gray-600">📍 {user.address.city}</p>
    </div>
  );
};

💡 ポイント:

  • 1ユーザー分の表示だけを担当
  • 他のページでも使い回せる
  • Storybookでプレビューもできる!

src/features/user/components/UserList.tsx
import type { User } from '../types/user';
import { UserCard } from './UserCard';

type Props = {
  users: User[];
};

export const UserList = ({ users }: Props) => {
  if (users.length === 0) {
    return <p className="text-gray-500">ユーザーが見つかりませんでした</p>;
  }

  return (
    <div>
      {users.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
};

💡 ポイント:

  • リスト表示だけを担当
  • UserCardを使って表示
  • コンポーネントの組み合わせ!

公開API

外部に公開するものだけをまとめる。

src/features/user/index.ts
export { useUsers } from './hooks/useUsers';
export { useUserSearch } from './hooks/useUserSearch';
export { UserCard } from './components/UserCard';
export { UserList } from './components/UserList';
export { UserSearchInput } from './components/UserSearchInput';
export type { User } from './types/user';

💡 ポイント:

  • 「ユーザー機能で使えるもの」を一箇所にまとめる
  • 他のファイルはfrom '@/features/user'だけでimportできる
  • 内部実装を隠せる(カプセル化)

3️⃣ ページで統合

最後に、全部を組み合わせるだけ!

src/app/users/page.tsx
'use client';

import { 
  useUsers, 
  useUserSearch, 
  UserSearchInput, 
  UserList 
} from '@/features/user';
import { LoadingSpinner } from '@/shared/components/LoadingSpinner';
import { ErrorMessage } from '@/shared/components/ErrorMessage';

export default function UsersPage() {
  const { users, loading, error } = useUsers();
  const { searchTerm, setSearchTerm, filteredUsers } = useUserSearch(users);

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage message={error} />;

  return (
    <div className="p-5 max-w-3xl mx-auto">
      <h1 className="text-3xl font-bold mb-5">ユーザー一覧</h1>
      
      <div className="mb-5">
        <UserSearchInput value={searchTerm} onChange={setSearchTerm} />
      </div>

      <UserList users={filteredUsers} />
    </div>
  );
}

💡 ポイント:

  • ページファイルはたったの30行!
  • やってることは「部品を組み合わせるだけ」
  • コードが超読みやすい!

🎉 何が良くなったのか?

見比べてみたらわかると思います‼︎

項目 Bad Example Good Example
ファイル数 1ファイル 10ファイル
1ファイルの行数 100行超え 10〜50行
再利用 できない できる
修正のしやすさ どこ直せばいいか分からない 修正箇所が明確
テスト 書けない 書ける
チーム開発 コンフリクト祭り スムーズ

具体的なメリット

責務の分離をすることで以下のメリットがあります。

1. 見通しが良い

「検索機能を修正したい」
→ features/user/hooks/useUserSearch.ts を開くだけ

2. 再利用できる

// 別のページでも使える!
import { UserCard } from '@/features/user';

<UserCard user={currentUser} />

3. チーム開発がスムーズ

Aさん: UserCard.tsx を修正中
Bさん: useUsers.ts を修正中
→ お互い影響しない!コンフリクトなし!

4. テストが書ける

// useUsersだけをテスト
test('ユーザーを取得できること', async () => {
  const { result } = renderHook(() => useUsers());
  // ...
});

5. 拡張しやすい

新機能「投稿一覧」を追加したい
→ features/post/ を作るだけ
→ 既存コードは一切触らない!

💡 責務の分離のポイント

今回のポイントです。

分類 役割 配置場所
型定義 データ構造を定義 features/user/types/
API層 外部通信を担当 features/user/api/
Custom Hooks ロジックを担当 features/user/hooks/
コンポーネント 見た目を担当 features/user/components/
共通部品 全体で使うもの shared/
ページ 全体を統合 app/users/page.tsx

📦 改めてカバンで例えると

悪い例

大きなカバン1つに全部詰め込む
→ 何がどこにあるか分からない
→ 毎回全部ひっくり返して探す

良い例

機能ごとのポーチ(features)
  ├─ ユーザー機能ポーチ
  │   ├─ API通信グッズ
  │   ├─ ロジックグッズ
  │   └─ UI部品
  └─ 投稿機能ポーチ
  
共通ポーチ(shared)
  └─ どこでも使うもの

それぞれが自分の仕事に集中できる = 責務の分離 🎯ってことです。

参考資料

Claude Codeに添削

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?