フロントエンド開発で陥りがちな問題と機能分離の始め方
はじめに
複数人で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 = () => {
...
}
実際の導入手順
この問題を解決するため、以下のステップで機能分離を進めました:
- 依存関係の調査
- ESLintルールの設定
- 段階的な修正の実施
1. 依存関係の調査
まず、現状のコードがどれくらい複雑に依存しあっているか確認しました。
機能間の依存関係を分析し、循環参照を検出するためにmadgeを導入することをおすすめします。
npm install -g madge
madgeには主に以下の使い方があります。
- 循環参照の検出
npx madge --circular src/
- 詳細な依存関係の調査
madge --circular --debug --extensions ts,tsx,js,jsx src
- 特定モジュールへの依存確認
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で管理します。以下のような設定を行いました。
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パターンを採用しているので、以下のようなコンポーネント階層の制限も設定しました。
{
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