{}const=>[]async()letfn</>var
開発Web

プロジェクトが 1 年で失敗しないようにフロントエンドアーキテクチャを構築する方法

なぜ一部のプロジェクトは半年後にスパゲッティコードに変わり、他のプロジェクトは何年も生き続け、発展するのでしょうか?フォルダー構造から抽象化レイヤーまで、フロントエンドアーキテクチャを構築するための実績のある原則を分析します。スケール、テスト、メンテナンスが簡単なコードを作成する方法を学びましょう。

К

Kodik

著者

5分で読める

新しいプロジェクトを始めるところを想像してみてください。熱意が高まり、コードはすばやく書かれます。数ヶ月後、あなたはすでにutils.jsでその奇妙な機能が必要な理由を忘れています。6 か月後、新しい機能を追加することはクエストになります。1年後、プロジェクトは泥沼化し、どんな変更もすべてを壊す可能性があります。

よく知られていますか?これは、よく考え抜かれたアーキテクチャがないことの典型的な結果です。長年にわたって生き残り、発展するフロントエンドプロジェクトを構築する方法を見てみましょう。

なぜプロジェクトは失敗するのでしょうか?

ソリューションについて話す前に、プロジェクトが失敗する主な理由を理解しましょう。

スパゲッティコード。 すべてはすべてに関連しています。1つの場所を変更すると、他の3つのコンポーネントが壊れます。

構造の欠如。 ファイルは無秩序に散らばっています。必要なコンポーネントを見つけるのは、まるで考古学の探査のようなものです。

ロジックの重複。 同じ機能が、5つの異なる場所で5つの異なる方法で実装されています。

文書がありません。 1か月後には、コードがどのように機能するかを覚えていないでしょう。

技術的負債。 「後で直す」は「絶対に直さない」に変わります。

🔥 10万人以上の学生が参加中

理論を読むのに疲れた?
コーディングの時間だ!

Kodik — 実践でプログラミングを学ぶアプリ。AIメンター、インタラクティブなレッスン、実際のプロジェクト。

🤖 AI 24時間
🎓 修了証
💰 無料
🚀 始める
今日参加

基礎:正しいフォルダ構造

優れたアーキテクチャは、ファイルの整理から始まります。ほとんどのプロジェクトで実績のある構造は次のとおりです。

src/
├── components/          # 再利用可能なコンポーネント
│   ├── ui/             # 基本的なUI要素(ボタン、入力)
│   ├── layout/         # レイアウトのコンポーネント(ヘッダー、フッター)
│   └── features/       # ビジネスコンポーネント
├── pages/              # アプリのページ
├── services/           # API の操作
├── store/              # グローバル状態(Vuex、Redux、Pinia)
├── utils/              # 補助機能
├── hooks/              # カスタムフック(React用)
├── composables/        # コンポーネント機能(Vue用)
├── types/              # TypeScriptの種類
├── constants/          # アプリケーション定数
└── assets/             # 静的リソース

原則: 各フォルダは1つの責任領域を担当します。UIコンポーネントが必要な場合は、components/uiに移動します。API を操作するための関数が必要な場合は、services を参照してください。

単一責任の原則

各モジュールは1つのことをしなければなりませんが、それをうまく行う必要があります。

悪い点:

// UserCard.js - すべてを一度に実行します
function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  
  // データの取得
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);
  
  // 検証
  const isValid = user?.email && user?.name;
  
  // フォーマット
  const formattedDate = new Date(user?.created).toLocaleDateString();
  
  // レンダリングも...
  return <div>...</div>;
}

良い:

// services/userService.js
export const fetchUser = async (userId) => {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
};

// utils/validation.js
export const validateUser = (user) => {
  return user?.email && user?.name;
};

// utils/dateFormatter.js
export const formatDate = (date) => {
  return new Date(date).toLocaleDateString();
};

// components/UserCard.js - 表示のみ
function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
  
  if (!validateUser(user)) return null;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <span>{formatDate(user.created)}</span>
    </div>
  );
}

現在では、各機能は個別にテストされ、さまざまな場所で使用され、簡単に変更されます。

抽象化層:分割統治

優れたアーキテクチャは、玉ねぎのように層を重ねて構築されます。

1. データレイヤー

データの送受信を担当します。すべてのAPIリクエストはここにあります。

// services/api/userApi.js
const API_BASE = 'https://api.example.com';

export const userApi = {
  getUser: (id) => fetch(`${API_BASE}/users/${id}`).then(r => r.json()),
  updateUser: (id, data) => fetch(`${API_BASE}/users/${id}`, {
    method: 'PUT',
    body: JSON.stringify(data)
  }),
  deleteUser: (id) => fetch(`${API_BASE}/users/${id}`, { method: 'DELETE' })
};

2.ビジネスロジックレイヤー

データを処理し、ビジネスロジックのルールを適用します。

// services/userService.js
import { userApi } from './api/userApi';

export const userService = {
  async getActiveUsers() {
    const users = await userApi.getUsers();
    return users.filter(user => user.isActive);
  },
  
  async promoteToAdmin(userId) {
    const user = await userApi.getUser(userId);
    if (!user.email.endsWith('@company.com')) {
      throw new Error('Only company emails can be admins');
    }
    return userApi.updateUser(userId, { role: 'admin' });
  }
};

3.状態レイヤー

アプリケーションのグローバルな状態を管理します。

// store/userStore.js (Pinia/Vue)
export const useUserStore = defineStore('user', {
  state: () => ({
    currentUser: null,
    users: []
  }),
  
  actions: {
    async loadUser(id) {
      this.currentUser = await userService.getUser(id);
    }
  }
});

4. プレゼンテーション層

ユーザーにデータを表示するコンポーネント。

// components/UserProfile.vue
<script setup>
import { useUserStore } from '@/store/userStore';

const userStore = useUserStore();
const { currentUser } = storeToRefs(userStore);

onMounted(() => {
  userStore.loadUser(route.params.id);
});
</script>

<template>
  <div v-if="currentUser">
    <h1>{{ currentUser.name }}</h1>
    <p>{{ currentUser.email }}</p>
  </div>
</template>

黄金律: 上位レイヤーは下位レイヤーを使用できますが、その逆はできません。コンポーネントはストアを使用し、ストアはサービスを使用しますが、サービスはコンポーネントをインポートすることはありません。

継承の代わりにコンポジション

現代のフロントエンドでは、コンポジションが継承に勝っています。巨大な基本クラスの代わりに、小さな再利用可能な関数を作成します。

Reactフックの例:

// hooks/useLocalStorage.js
export function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });
  
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  
  return [value, setValue];
}

// hooks/useDebounce.js
export function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(handler);
  }, [value, delay]);
  
  return debouncedValue;
}

// 使用
function SearchComponent() {
  const [query, setQuery] = useLocalStorage('searchQuery', '');
  const debouncedQuery = useDebounce(query, 500);
  
  // これで、デバウンス検索とlocalStorage保存が可能になりました
  useEffect(() => {
    if (debouncedQuery) {
      searchApi(debouncedQuery);
    }
  }, [debouncedQuery]);
}

タイプ化はあなたの親友です

TypeScriptは初心者には余計なもののように思えるかもしれませんが、プロジェクトを多くの問題から救うことができます。

// types/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'user' | 'admin' | 'moderator';
  createdAt: Date;
}

export interface ApiResponse<T> {
  data: T;
  error?: string;
  status: number;
}

// services/userService.ts
export async function getUser(id: number): Promise<ApiResponse<User>> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

IDEは利用可能なフィールドを提案し、TypeScriptは本番環境ではなく開発段階でエラーをキャッチします。

構成と定数

コードにマジックナンバーと文字列を散らかさないでください。それらを一箇所に集めましょう。

// constants/config.js
export const API_CONFIG = {
  BASE_URL: process.env.VITE_API_URL || 'https://api.example.com',
  TIMEOUT: 5000,
  RETRY_ATTEMPTS: 3
};

export const UI_CONSTANTS = {
  ITEMS_PER_PAGE: 20,
  DEBOUNCE_DELAY: 300,
  TOAST_DURATION: 3000
};

export const ROUTES = {
  HOME: '/',
  PROFILE: '/profile',
  SETTINGS: '/settings'
};

ページ上の要素の数を変更する必要がある場合、どこで変更すればよいかがわかります。

エラー処理

エラーに対する体系的なアプローチは、成熟したアーキテクチャの特徴です。

// utils/errorHandler.js
export class ApiError extends Error {
  constructor(message, status, data) {
    super(message);
    this.status = status;
    this.data = data;
  }
}

export async function handleApiCall(apiFunction) {
  try {
    return await apiFunction();
  } catch (error) {
    if (error instanceof ApiError) {
      // ユーザーにわかりやすいメッセージを表示します
      toast.error(error.message);
      
      // 開発者のためのログ
      console.error('API Error:', error.status, error.data);
    } else {
      // 予期しないエラー
      toast.error('Something went wrong. Please try again later.');
      console.error('Unexpected error:', error);
    }
    throw error;
  }
}

// 使用
async function loadUserData(userId) {
  await handleApiCall(async () => {
    const user = await userApi.getUser(userId);
    if (!user) {
      throw new ApiError('User not found', 404);
    }
    return user;
  });
}

建築的なソリューションを文書化する

プロジェクトのルートに ARCHITECTURE.md ファイルを作成します。

# プロジェクトのアーキテクチャ

# # 構造
- `/components` - переиспользуемые компоненты
- `/pages` - страницы приложения
- `/services` - бизнес-логика и API

# # 契約
- Компоненты именуются в PascalCase
- Утилиты и сервисы в camelCase
- Константы в SCREAMING_SNAKE_CASE

# # レイヤー
1. API Layer (services/api/)
2. Business Logic (services/)
3. State Management (store/)
4. UI Components (components/)

# # 重要な決定
- Используем Pinia для состояния
- Axios для HTTP запросов
- День.js для работы с датами

コードレビューとリンティング

ESLintとPrettierをすぐに設定します。これにより、コードの読みやすさに関する問題の90%を回避できます。

// .eslintrc.js
module.exports = {
  rules: {
    'no-console': 'warn',
    'no-unused-vars': 'error',
    'complexity': ['error', 10], // 複雑な機能について警告します
    'max-lines-per-function': ['warn', 50]
  }
};

アーキテクチャのテスト

優れたアーキテクチャはテストしやすいものです。

// userService.test.js
import { userService } from './userService';
import { userApi } from './api/userApi';

jest.mock('./api/userApi');

test('promoteToAdmin rejects external emails', async () => {
  userApi.getUser.mockResolvedValue({
    id: 1,
    email: 'external@gmail.com'
  });
  
  await expect(
    userService.promoteToAdmin(1)
  ).rejects.toThrow('Only company emails');
});

機能がテストしにくい場合は、アーキテクチャが不完全であることを示しています。

スケーリング: 特徴ベースの構造

プロジェクトが大きくなると、ファイルの種類ではなく、機能ごとにコードをグループ化します。

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   ├── services/
│   │   ├── store/
│   │   └── types/
│   ├── products/
│   │   ├── components/
│   │   ├── services/
│   │   └── store/
│   └── cart/
│       ├── components/
│       └── store/
└── shared/              # 一般的なコンポーネントとユーティリティ

認証に関するすべてが1つのフォルダにまとまりました。見つけやすく、削除しやすく、他の開発者に簡単に転送できます。

初心者がよくする間違い!

早すぎる最適化

100万人のユーザーのためのアーキテクチャをすぐに構築する必要はありません。シンプルながら拡張性のあるソリューションから始めましょう。

過剰な抽象化

ボタンが1つしかない場合は、5つの基本クラスからなるシステムを作成する必要はありません。

規則を無視する

フレームワークの一般的な慣行を使用してください。自転車を発明しないでください。

リファクタリングがない

アーキテクチャの改善に時間をかけます。技術的負債は気づかないうちに蓄積します。

実用的なアドバイス。

READMEから始めましょう。 コードを書く前に、プロジェクトの仕組みを説明してください。これにより、アーキテクチャについて考えることができます。

定期的にリファクタリングしてください。 既存のコードを改善するために、週に 1 時間を割いてください。

他の人から学ぶ。 オープンソースプロジェクトを研究し、それらがどのように編成されているかを確認します。

やり直すことを恐れないでください。 構造が機能しない場合は、長年それを使用するよりも、今すぐ修正することをお勧めします。

役立つツール。

  • ESLint/Prettier — コードの自動フォーマット

  • Husky —コミット前のコードチェック

  • TypeScript — 大規模プロジェクトの標準化

  • Storybook — コンポーネントの開発を分離する

  • Jest/Vitest — ロジックのテスト

結論

優れたアーキテクチャは、最初から完璧なコードではありません。これは、プロジェクトを進化させるための体系的なアプローチです。シンプルでありながら論理的な構造から始めましょう。単一責任の原則に従ってください。重要な決定を文書化します。そして何よりも、コードを定期的に見直し、改善します。

1年後、あなたは建築に費やした時間のすべてに感謝するでしょう。あなたのプロジェクトは生き、発展し、喜びをもたらし、触るのが怖いスパゲッティの塊にはなりません。

これらの内容は、 コディケ — すべてを詳細に分析し、タスクを実践して強化します。私たちは、コードを書くだけでなく、長く使える正しいスケーラブルなアプリケーションを構築することを教えています。

サポートが必要な場合は、すでに 2000人以上の仲間が活動しているTelegramチャンネル、ここでは、どんな質問でも尋ねたり、アーキテクチャのソリューションについて話し合ったり、経験豊富な開発者からコードのレビューを受けたりできます。

真のプロフェッショナルが育つコミュニティに参加しましょう! 🚀

🎯先延ばしをやめよう

記事は気に入った?
実践の時間だ!

Kodikでは読むだけでなく、すぐにコードを書く。理論 + 実践 = 本当のスキル。

即座に実践
🧠AIがコードを説明
🏆修了証

登録不要 • カード不要