🫶
eslint-plugin-importで秩序のあるフロントエンド開発を実現する
導入
フロントエンド開発では、モジュール間の無秩序な依存関係が発生すると、コードの可読性やメンテナンス性が低下します。特に、チーム開発においてはプロジェクトの設計思想やディレクトリ戦略のコンテキストが遵守されず形骸化されてしまうと、リファクタリングやスケールが困難になります。
eslint-plugin-import
を活用することで、予期しない依存関係を静的解析によって防ぐことができます。
ディレクトリ戦略と依存関係の制限
今回はBulletproof-react
アーキテクチャのようなディレクトリ設計を想定します。
src/
├── features/ # 特定の機能を提供するファイル群
│ ├── hoge/
│ ├── foo/
├── components/ # 特定のドメイン(機能)に依存しない汎用コンポーネント群
├── hooks/ # 特定のドメイン(機能)に依存しない汎用カスタムフック群
├── api/ # API通信層
例えば、上記の構成を維持するために実現したいルールは、以下の通りです。
-
features/
間の直接的な依存関係を禁止したい -
components/
,hooks/
,api/
の、特定のfeatures/
への直接的な依存関係を禁止したい - UIフレームワーク(
mui
やantd
など)の使用はcomponents/
を経由させたい
ESLintによるルール適用
features/
間の依存制限
features
は独立した機能単位のモジュールであり、互いに直接依存することは保守性の低下を招きます。
依存関係が無秩序に広がると、
- 機能ごとの責務が曖昧になる
- リファクタリングが困難になる
- 単体テストが難しくなる
といった問題が発生します。そのため、features/
間の直接的な参照を制限することで、より堅牢なアーキテクチャを構築できます。
eslint.config.js
import importPlugin from "eslint-plugin-import";
const features = ["hoge", "foo"];
export default [
{
name: "import/custom",
plugins: {
import: importPlugin,
},
settings: {
"import/resolver": {
typescript: {},
},
},
rules: {
"import/no-restricted-paths": [
"error",
{
zones: features.map((feature) => ({
from: [`src/features/${feature}/**`],
target: [`src/features/!(${feature})/**`],
})),
},
],
},
},
];
具体例
// ❌ NG: features/foo から features/hoge を直接参照
import { someFunction } from "../hoge/utils"; // ESLint エラー
// ✅ OK: features 内で適切なインターフェースを提供
import { someFunction } from "features/hoge";
features/
への依存制限
汎用ファイルから上記と同様の理由から、特定のドメインを持つべきではない汎用的なコンポーネント・関数などについてもfeatures/
への直接的な参照を制限します。
eslint.config.js
import importPlugin from "eslint-plugin-import";
const features = ["hoge", "foo"];
export default [
{
name: "import/custom",
plugins: {
import: importPlugin,
},
settings: {
"import/resolver": {
typescript: {},
},
},
rules: {
"import/no-restricted-paths": [
"error",
{
zones: features.map((feature) => ({
from: [`src/features/${feature}/**`],
target: ["src/api/**", "src/components/**", "src/hooks/**"],
})),
},
],
},
},
];
特定ライブラリへの依存制限
UIフレームワークを直接参照することは、保守性や拡張性の観点からあまり好みではありません。
- デザインシステムの一貫性を保持することが難しい
- 直接 UI フレームワークを使用すると、スタイルやコンポーネントの統一が難しくなり、UI/UXにバラつきが生じてしまう可能性がある
- ライブラリ自体への依存が強くなってしまうことによる保守性・拡張性の低下
- 必要以上の機能が提供されている場合、それらを無駄に使用してしまい、管理コストが増加する可能性がある
- 将来的なライブラリ変更が困難になり、プロジェクトの柔軟性が損なわれる可能性がある
eslint.config.js
import importPlugin from "eslint-plugin-import";
const features = ["hoge", "foo"];
export default [
{
name: "import/custom",
plugins: {
import: importPlugin,
},
settings: {
"import/resolver": {
typescript: {},
},
},
rules: {
"import/no-restricted-paths": [
"error",
{
zones: features.map((feature) => ({
from: ["node_modules/@mui", "node_modules/antd"],
target: [`src/features/**`],
})),
},
],
},
},
];
まとめ
依存関係の制御をLinterに適用することで、特段意識せずとも、メンテナンス性とスケーラビリティが向上し、チーム開発においても秩序あるフロントエンド開発を実現するための助けになります。
特に新規メンバーやフロントエンド開発に慣れていないメンバーがコーディングする際に、適切な依存関係の指針を提供できる点が大きなメリットかなと思います。
Discussion