✂️

フロントエンド開発で陥りがちな問題と機能分離の始め方

に公開

はじめに

複数人でWebアプリケーションを開発していると、最初の頃は順調でも、あるタイミングから急に作業が重たく感じることがあります。
例えば、よく起こる問題として以下が挙げられます。

  • 機能を改修するたびに、他の機能で不具合が発生する
  • 同時に作業を進めていると、開発者同士のコンフリクトが頻発する
  • テスト範囲がどんどん広がり、テストやリリース準備に時間がかかる

このような問題は、機能同士が過度に依存し合っていることが、原因の一つになっているかもしれません。
本記事では、それを防ぐための機能分離を、madgeやESLintを活用して行う方法を紹介します。

具体的な問題

業務で開発している売上管理システムには、以下のような機能があります。

  • ユーザー管理機能(スタッフの追加・編集、権限の変更)
  • ストア管理機能(各ストアの詳細情報の設定・管理)
  • 売上レポート機能(売上データの確定・報告作業)

これらが互いに直接コードを参照し合っていた結果、以下のような問題が起こっていました。

  • 開発時間の増加: 「ユーザー管理機能」を修正すると、「ストア管理機能」や「売上レポート機能」もテストが必要になる
  • バグの連鎖: 一つの機能の修正が、予期しない場所でエラーを引き起こす
  • 並行開発のしづらさ: 複数の開発者が同時に作業すると、コードの競合が頻発
  • 新機能追加が難しい: 新しく機能追加をする際、多数の既存機能への影響を調査する必要がある

機能が依存している例

// src/pages/user/UserListPage.tsx
import { getTenantsByStore } from 'src/features/tenants/api'
import { formatSalesData } from 'src/features/salesReport/utils'
// ユーザー管理機能(users)が、ストア管理機能(tenants)の内部処理を直接呼び出してしまっている

export const UserListPage = () => {
  ...
}
// src/pages/tenants/TenantDetailPage.tsx

import { UserRole } from 'src/features/user/types'
import { calculateRevenue } from 'src/features/salesReport/calculator'
// ストア管理機能(tenants)が、ユーザー管理機能(users)の型定義を直接参照してしまっている

export const TenantDetailPage = () => {
  ...
}

実際の導入手順

この問題を解決するため、以下のステップで機能分離を進めました:

  1. 依存関係の調査
  2. ESLintルールの設定
  3. 段階的な修正の実施

1. 依存関係の調査

まず、現状のコードがどれくらい複雑に依存しあっているか確認しました。
機能間の依存関係を分析し、循環参照を検出するためにmadgeを導入することをおすすめします。
https://www.npmjs.com/package/madge

npm install -g madge

madgeには主に以下の使い方があります。

  1. 循環参照の検出
npx madge --circular src/
  1. 詳細な依存関係の調査
madge --circular --debug --extensions ts,tsx,js,jsx src
  1. 特定モジュールへの依存確認
madge --depends path/to/module.js src/

コマンドを実行すると、「機能A → 機能B → 機能C → 機能A」のような循環を発見できます。

上記のコマンドを実行すると、以下のような結果が出力されます。

- Finding files
// 以下、個々のファイルのAST生成ログ
2025-07-15T12:25:20.307Z madge using src paths [ '/Users/taba/Develop/tenant-app/src' ]
2025-07-15T12:25:20.307Z madge using config { baseDir: null, excludeRegExp: false, fileExtensions: [ 'ts', 'tsx', 'js', 'jsx' ], includeNpm: false, requireConfig: null, webpackConfig: null, tsConfig: null, rankdir: 'LR', layout: 'dot', fontName: 'Arial', fontSize: '14px', backgroundColor: '#111111', nodeColor: '#c6c5fe', nodeShape: 'box', nodeStyle: 'rounded', noDependencyColor: '#cfffac', cyclicNodeColor: '#ff6c60', edgeColor: '#757575', graphVizOptions: false, graphVizPath: false, dependencyFilter: [Function (anonymous)], circular: true, debug: true, extensions: 'ts,tsx,js,jsx', version: [Function: version] }
2025-07-15T12:25:20.307Z madge using base directory /Users/taba/Develop/tenant-app/src
(中略)
2025-07-15T12:25:21.303Z typescript-eslint:typescript-estree:create-program:createSourceFile Getting AST without type information in TSX mode for: /Users/taba/Develop/tenant-app/estree.tsx
2025-07-15T12:25:21.304Z typescript-eslint:typescript-estree:create-program:createSourceFile Getting AST without type information in TS mode for: /Users/taba/Develop/tenant-app/estree.ts
2025-07-15T12:25:21.305Z typescript-eslint:typescript-estree:create-program:createSourceFile Getting AST without type information in TS mode for: /Users/taba/Develop/tenant-app/estree.ts

// 以下、循環依存の詳細なリスト
// 矢印が依存関係の方向を示している(>の左側が右側に依存)
1) features/mode/modeSlice.ts > features/services/index.ts > features/services/provider.ts > features/services/TAppServiceFactory.ts > features/services/impl/ApiTAppService.ts > types/store.ts > rootReducer.ts
2) features/services/index.ts > features/services/provider.ts > features/services/TAppServiceFactory.ts > features/services/impl/ApiTAppService.ts > types/store.ts > rootReducer.ts > features/network/activeSlice.ts
3) rootReducer.ts > features/network/activeSlice.ts > store.ts
4) features/services/index.ts > features/services/provider.ts > features/services/TAppServiceFactory.ts > features/services/impl/ApiTAppService.ts > types/store.ts > rootReducer.ts > features/network/authSlice.ts
(以下略)

結果

417個のファイルを分析し、19個の循環依存を検出できました。

2. ESLintルールの設定

機能ごとの依存制限をESLintで管理します。以下のような設定を行いました。
https://eslint.org/docs/latest/rules/no-restricted-imports

const featureDirectories = [
  'members',
  'salesReport',
  'tenantDetail',
  'storeSalesReportSummary',
    /* 以下省略 */
]

const featureDirectoryImportRules = featureDirectories.map((dir) => ({
  target: `src/features/${dir}/**/*.{ts,tsx,js,jsx}`,
  from: `src/features/!(${dir})/**`,
}))

module.exports = {
  // ... 他の設定
  rules: {
    'import/no-restricted-paths': [
      'error',
      { zones: featureDirectoryImportRules },
    ],
  },
}

Atomic Designパターンを採用しているので、以下のようなコンポーネント階層の制限も設定しました。
https://www.npmjs.com/package/eslint-plugin-no-relative-import-paths

{
  files: [
    'src/components/{atoms,molecules,organisms}/**/*.{ts,tsx,js,jsx}',
  ],
  rules: {
    'no-relative-import-paths/no-relative-import-paths': [
      'error',
      {
        allowSameFolder: true,
        allowedDepth: 1, // 同じタイプのcomponent directory内でのimportのみ許可
        rootDir: 'src',
      },
    ],
  },
}

3. 段階的な修正の実施

1. 緩い制限からスタート

いきなり最終的に適用したい厳しいルールを設定すると、差分が数百ファイル出て大変なるので、まずはwarning設定から始めて、少しずつ修正を進めます。

'import/no-restricted-paths': [
  'warn',
  { zones: featureDirectoryImportRules },
],

2. 例外の管理

特定の機能で例外的なインポートが必要な場合は、明示的に設定しておきます。

// 特定の機能で例外的にインポートが必要な場合の対応
rules: {
  'import/no-restricted-paths': [
    'error',
    {
      zones: [
        {
          target: 'src/features/members/**',
          from: 'src/features/!(members)/**',
          except: ['src/features/auth/types.ts'], // 例外を明示的に設定
        },
      ],
    },
  ],
}

3. 共通機能の整理

散在していた共通処理を適切な場所に集約します。

複数の機能で共通して使われているコードを特定し、

  • src/hooks/
  • src/slices/
  • src/components/

など、明確な場所にまとめます。

// 修正前:機能ごとに散在
src/features/members/utils.ts
src/features/tenants/utils.ts
src/features/salesReport/utils.ts

// 修正後:共通エリアに集約
src/utils/member.ts
src/hooks/useApiTenants.ts
src/components/InputForm.tsx

4. 既存コードを段階的に修正

他の開発タスクのついでに修正するなどしながら、少しずつwarningを潰していきます。

//  ✅ 機能内のみで完結している
// src/features/members/pages/Create/index.tsx
import { useApiMember } from 'src/hooks/useApiMember'
import { StoreManagerRoles } from 'src/domain/role'
import { MemberPostRequest } from 'src/slices/services/api'
import { useAppDispatch, useAppSelector } from 'src/store'
import TemplatesMembersCreate from '../../templates/Create'

const PagesMembersCreate: React.FC = () => {
  const { postMembers } = useApiMember()
  const dispatch = useAppDispatch()

  const handleCreateMembers = async (members: MemberPostRequest[]) => {
    const result = await postMembers(orgCode, members)
    // 具体的な処理
  }

  return <TemplatesMembersCreate onCreateMembers={handleCreateMembers} />
}

機能分離による効果

機能分離により、以下の具体的な効果が得られました。

導入前: 一つの小規模な機能修正に2〜3日必要

  • 影響範囲の調査:1日
  • 修正作業:半日
  • 機能テスト・デグレチェック:1時間以上

導入後: 同じ修正が半日程度で完了

  • 影響範囲の調査:ほぼ不要or数時間
  • 修正作業:半日
  • 機能テスト・デグレチェック:10~20分程度

注意点

かなり密結合な機能を無理に分離すると、かえって複雑になる場合もあるので、過度な分離をしないように注意が必要です。(例:「商品追加」と「在庫更新」を完全に別機能にする → 常に同時実行が必要)
機能が独立してビジネス価値を提供できるかどうかが、判断基準として大事かもしれません。

まとめ

機能分離を行うことで以下のような効果が得られました。

  • 開発スピードの向上: 機能開発の開始~完了までの所要時間が大幅削減
  • アプリの品質向上: バグの連鎖が減少し、より安定した挙動が実現できた
  • 複数人での開発しやすさ: 並行して開発を行っても競合が起こりづらくなった
  • 可読性: 機能が独立しているため理解しやすくなった
  • メンテナンス性: リポジトリがメンテナンスしやすい状態になった
  • 技術的負債の軽減: 設計の整合性が維持されやすくなった

開発の中で作業の進めづらさを感じたら、ぜひ機能分離を試してみてください。

Discussion