UNPKG

eslint-plugin-fsd-lint

Version:

ESLint plugin for enforcing Feature-Sliced Design (FSD) architecture

650 lines (475 loc) β€’ 22.8 kB
# πŸš€ eslint-plugin-fsd-lint πŸš€ [![npm version](https://img.shields.io/npm/v/eslint-plugin-fsd-lint)](https://www.npmjs.com/package/eslint-plugin-fsd-lint) [![npm downloads](https://img.shields.io/npm/dt/eslint-plugin-fsd-lint)](https://www.npmjs.com/package/eslint-plugin-fsd-lint) [![npm bundle size](https://img.shields.io/bundlephobia/min/eslint-plugin-fsd-lint)](https://bundlephobia.com/package/eslint-plugin-fsd-lint) [![License](https://img.shields.io/npm/l/eslint-plugin-fsd-lint)](https://github.com/effozen/eslint-plugin-fsd-lint/blob/main/LICENSE) [English](README.md) | [ν•œκ΅­μ–΄](README.ko.md) > ESLint 9+와 μƒˆλ‘œμš΄ Flat Config μ‹œμŠ€ν…œμ„ μ§€μ›ν•©λ‹ˆλ‹€. ## πŸ“– μ†Œκ°œ `eslint-plugin-fsd-lint`λŠ” **Feature-Sliced Design(FSD)** μ•„ν‚€ν…μ²˜λ₯Ό μ€€μˆ˜ν•˜λ„λ‘ κ°•μ œν•˜λŠ” ESLint ν”ŒλŸ¬κ·ΈμΈμž…λ‹ˆλ‹€. μ΅œμ‹  **ESLint 9+** λ₯Ό μ™„λ²½νžˆ μ§€μ›ν•˜κ³ , **Flat Config** ν˜•μ‹μ„ λ”°λ₯΄κΈ° λ•Œλ¬Έμ— μžλ°”μŠ€ν¬λ¦½νŠΈμ™€ νƒ€μž…μŠ€ν¬λ¦½νŠΈ ν”„λ‘œμ νŠΈμ— λ§€λ„λŸ½κ²Œ 톡합할 수 μžˆμŠ΅λ‹ˆλ‹€. ### ✨ μ™œ 이 ν”ŒλŸ¬κ·ΈμΈμ„ μ‚¬μš©ν•΄μ•Ό ν•˜λ‚˜μš”? - **Flat Config 지원**: **ESLint 9+** 및 **μƒˆλ‘œμš΄ Flat Config μ‹œμŠ€ν…œ**κ³Ό μ™„λ²½ ν˜Έν™˜ - **μ—„κ²©ν•œ FSD μ€€μˆ˜**: κΈ°λŠ₯ 기반 ν”„λ‘œμ νŠΈ κ΅¬μ‘°μ—μ„œ μ•„ν‚€ν…μ²˜ μœ„λ°˜μ„ λ°©μ§€ - **μœ μ§€λ³΄μˆ˜μ„± ν–₯상**: λͺ…ν™•ν•œ λͺ¨λ“ˆ 뢄리와 μ˜μ‘΄μ„± 관리λ₯Ό μœ λ„ - **μΌκ΄€λœ μ½”λ“œ ν’ˆμ§ˆ 보μž₯**: import νŒ¨ν„΄κ³Ό λͺ¨λ²” 사둀λ₯Ό ν‘œμ€€ν™” - **크둜슀 ν”Œλž«νΌ ν˜Έν™˜μ„±**: Windows와 Unix 기반 μ‹œμŠ€ν…œ λͺ¨λ‘μ—μ„œ μ›ν™œν•˜κ²Œ μž‘λ™ - **μœ μ—°ν•œ 폴더 이름 μ§€μ •**: μ‚¬μš©μž μ •μ˜ 폴더 이름 νŒ¨ν„΄(`1_app`, `2_pages` λ“±) 지원 - **λ‹€μ–‘ν•œ 별칭 ν˜•μ‹**: `@shared`와 `@/shared` λͺ¨λ‘ 지원 - **포괄적인 ν…ŒμŠ€νŠΈ 컀버리지**: μ‹€μ œ μ‹œλ‚˜λ¦¬μ˜€μ™€ μ—£μ§€ μΌ€μ΄μŠ€λ‘œ μ² μ €νžˆ ν…ŒμŠ€νŠΈλ¨ ### πŸ” Feature-Sliced Designμ΄λž€? Feature-Sliced Design(FSD)은 ν”„λ‘ νŠΈμ—”λ“œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ κ΅¬μ‘°ν™”ν•˜λŠ” ν˜„λŒ€μ μΈ μ•„ν‚€ν…μ²˜ νŒ¨ν„΄μž…λ‹ˆλ‹€. λ³Έ ν”ŒλŸ¬κ·ΈμΈμ€ **μ˜¬λ°”λ₯Έ λ ˆμ΄μ–΄ 뢄리, import μ œν•œ, μ˜μ‘΄μ„± 관리** λ“±μ˜ μ£Όμš” FSD 원칙을 μ μš©ν•˜κ³ , 규λͺ¨κ°€ 컀져도 μœ μ§€λ³΄μˆ˜κ°€ μš©μ΄ν•œ μ½”λ“œλ² μ΄μŠ€λ₯Ό λ§Œλ“€λ„λ‘ λ•μŠ΅λ‹ˆλ‹€. --- ## πŸ“¦ μ„€μΉ˜ λ‹€μŒ λͺ…λ Ήμ–΄λ‘œ `eslint-plugin-fsd-lint`λ₯Ό **npm** λ˜λŠ” **pnpm**으둜 μ„€μΉ˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€: ### npm μ‚¬μš© μ‹œ: ```shell npm install --save-dev eslint-plugin-fsd-lint ``` ### pnpm μ‚¬μš© μ‹œ: ```shell pnpm add -D eslint-plugin-fsd-lint ``` ### Peer Dependencies 이 ν”ŒλŸ¬κ·ΈμΈμ€ ESLint 9+ 버전을 ν•„μš”λ‘œ ν•©λ‹ˆλ‹€. ν”„λ‘œμ νŠΈμ— ESLintκ°€ μ„€μΉ˜λ˜μ–΄ μžˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”: ```shell npm install --save-dev eslint ``` > πŸ’‘ 팁: μ—¬λŸ¬ νŒ¨ν‚€μ§€λ₯Ό κ°€μ§„ λͺ¨λ…Έλ ˆν¬ ν™˜κ²½μ—μ„œ μž‘μ—… 쀑이라면, ν”„λ‘œμ νŠΈ 루트 λ ˆλ²¨μ— `eslint-plugin-fsd-lint`λ₯Ό μ„€μΉ˜ν•΄ λͺ¨λ“  μ›Œν¬μŠ€νŽ˜μ΄μŠ€μ—μ„œ 섀정을 κ³΅μœ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. --- ## πŸš€ μ‚¬μš© 방법 & μ„€μ • ### πŸ”§ Flat Config μ„€μ • (`eslint.config.mjs`) `eslint-plugin-fsd-lint`λŠ” **ESLint 9+** 버전에 맞좰 **Flat Config μ‹œμŠ€ν…œ**κ³Ό λ§€λ„λŸ½κ²Œ λ™μž‘ν•˜λ„λ‘ μ„€κ³„λ˜μ—ˆμŠ΅λ‹ˆλ‹€. ν”„λ‘œμ νŠΈμ—μ„œ μ‚¬μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” `eslint.config.mjs`에 λ‹€μŒκ³Ό 같은 섀정을 μΆ”κ°€ν•˜μ„Έμš”: ```js import fsdPlugin from 'eslint-plugin-fsd-lint'; export default [ // ꢌμž₯ 프리셋 μ‚¬μš© fsdPlugin.configs.recommended, // λ˜λŠ” κ°œλ³„μ μœΌλ‘œ κ·œμΉ™ ꡬ성 { plugins: { fsd: fsdPlugin, }, rules: { // FSD λ ˆμ΄μ–΄ import κ·œμΉ™ κ°•μ œ (예: featuresλŠ” pagesλ₯Ό import λΆˆκ°€) 'fsd/forbidden-imports': 'error', // 슬라이슀/λ ˆμ΄μ–΄ κ°„ μƒλŒ€ 경둜 import κΈˆμ§€, 별칭(@) μ‚¬μš© // 기본적으둜 같은 슬라이슀 λ‚΄ μƒλŒ€ κ²½λ‘œλŠ” ν—ˆμš© (μ„€μ • κ°€λŠ₯) 'fsd/no-relative-imports': 'error', // Public API (index 파일)λ₯Ό ν†΅ν•œ import만 ν—ˆμš© 'fsd/no-public-api-sidestep': 'error', // 같은 λ ˆμ΄μ–΄ λ‚΄ 슬라이슀 κ°„ 직접 import λ°©μ§€ 'fsd/no-cross-slice-dependency': 'error', // λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 λ ˆμ΄μ–΄μ—μ„œ UI import λ°©μ§€ 'fsd/no-ui-in-business-logic': 'error', // μ „μ—­ μŠ€ν† μ–΄ 직접 import κΈˆμ§€ 'fsd/no-global-store-imports': 'error', // FSD λ ˆμ΄μ–΄ 기반으둜 import μˆœμ„œ κ°•μ œ 'fsd/ordered-imports': 'warn', }, }, ]; ``` **기본적으둜 κ°•μ œλ˜λŠ” μ£Όμš” 원칙:** - **λ ˆμ΄μ–΄ μž„ν¬νŠΈ**: μƒμœ„ λ ˆμ΄μ–΄λŠ” ν•˜μœ„ λ ˆμ΄μ–΄λ₯Ό μž„ν¬νŠΈν•  수 μ—†μŠ΅λ‹ˆλ‹€ (예: `features`λŠ” `pages`λ₯Ό μž„ν¬νŠΈν•  수 μ—†μŒ). - **슬라이슀 격리**: 같은 λ ˆμ΄μ–΄ λ‚΄μ˜ μŠ¬λΌμ΄μŠ€λŠ” μ„œλ‘œ 직접 μž„ν¬νŠΈν•΄μ„œλŠ” μ•ˆ λ©λ‹ˆλ‹€ (`no-cross-slice-dependency`). - **Public API μ‚¬μš©**: λ‹€λ₯Έ 슬라이슀/λ ˆμ΄μ–΄μ—μ„œμ˜ μž„ν¬νŠΈλŠ” ν•΄λ‹Ή 슬라이슀/λ ˆμ΄μ–΄μ˜ Public API(`index.js` λ˜λŠ” `index.ts`)λ₯Ό 톡해 이루어져야 ν•©λ‹ˆλ‹€. λ‚΄λΆ€ λͺ¨λ“ˆ 직접 μž„ν¬νŠΈλŠ” κΈˆμ§€λ©λ‹ˆλ‹€ (`no-public-api-sidestep`). - **μ ˆλŒ€ 경둜 (별칭)**: μ„œλ‘œ λ‹€λ₯Έ μŠ¬λΌμ΄μŠ€λ‚˜ λ ˆμ΄μ–΄ κ°„μ˜ μž„ν¬νŠΈμ—λŠ” μ ˆλŒ€ 경둜(예: `@`와 같은 λ³„μΉ­μœΌλ‘œ 섀정됨)λ₯Ό μ‚¬μš©ν•˜μ„Έμš”. μƒλŒ€ κ²½λ‘œλŠ” 일반적으둜 κΈˆμ§€λ©λ‹ˆλ‹€ (`no-relative-imports`). - **슬라이슀 λ‚΄ μƒλŒ€ 경둜**: _같은 슬라이슀 λ‚΄μ—μ„œμ˜_ μž„ν¬νŠΈμ—λŠ” μƒλŒ€ κ²½λ‘œκ°€ **ν—ˆμš©λ©λ‹ˆλ‹€** (예: feature λ‚΄μ—μ„œ `../lib`의 헬퍼λ₯Ό μž„ν¬νŠΈ). μ΄λŠ” `no-relative-imports`의 `allowSameSlice` μ˜΅μ…˜μœΌλ‘œ μ„€μ • κ°€λŠ₯ν•©λ‹ˆλ‹€ (κΈ°λ³Έκ°’: true). ### πŸ“Œ μ‚¬μš© κ°€λŠ₯ν•œ ꡬ성 이 ν”ŒλŸ¬κ·ΈμΈμ€ μ„Έ κ°€μ§€ λ‹€μ–‘ν•œ 엄격성 μˆ˜μ€€μ˜ 사전 μ •μ˜λœ ꡬ성을 μ œκ³΅ν•©λ‹ˆλ‹€: ```js import fsdPlugin from 'eslint-plugin-fsd-lint'; export default [ // ν‘œμ€€ ꢌμž₯ ꡬ성 fsdPlugin.configs.recommended, // μ—„κ²©ν•œ ꡬ성 (λͺ¨λ“  κ·œμΉ™μ΄ error) // fsdPlugin.configs.strict, // κΈ°λ³Έ ꡬ성 (덜 엄격함) // fsdPlugin.configs.base, ]; ``` ### πŸ› οΈ κ³ κΈ‰ ꡬ성 κ³ κΈ‰ μ˜΅μ…˜μ„ 톡해 κ·œμΉ™μ˜ λ™μž‘μ„ μ»€μŠ€ν„°λ§ˆμ΄μ¦ˆν•  수 μžˆμŠ΅λ‹ˆλ‹€: ```js import fsdPlugin from 'eslint-plugin-fsd-lint'; export default [ { plugins: { fsd: fsdPlugin, }, rules: { // 별칭 ν˜•μ‹ 및 폴더 νŒ¨ν„΄ ꡬ성 'fsd/forbidden-imports': [ 'error', { // @shared λ˜λŠ” @/shared ν˜•μ‹ λͺ¨λ‘ 지원 alias: { value: '@', withSlash: false, // @/shared ν˜•μ‹μ„ μœ„ν•΄ true μ‚¬μš© }, // λ²ˆν˜Έκ°€ 맀겨진 폴더 접두사 지원 folderPattern: { enabled: true, regex: '^(\\d+_)?(.*)', extractionGroup: 2, }, }, ], // 기타 κ·œμΉ™... }, }, ]; ``` ### πŸ“‚ μ˜ˆμ‹œ ν”„λ‘œμ νŠΈ ꡬ쑰 λ‹€μŒμ€ FSD 원칙을 μ€€μˆ˜ν•˜λŠ” ν”„λ‘œμ νŠΈ ꡬ쑰 μ˜ˆμ‹œμž…λ‹ˆλ‹€: ```plaintext src/ β”œβ”€β”€ app/ (λ˜λŠ” 1_app/) β”‚ β”œβ”€β”€ providers/ β”‚ β”œβ”€β”€ store.js β”‚ β”œβ”€β”€ index.js // μ•±μ˜ Public API β”‚ β”œβ”€β”€ processes/ (λ˜λŠ” 2_processes/) β”‚ β”œβ”€β”€ auth/ β”‚ β”œβ”€β”€ onboarding/ β”‚ β”œβ”€β”€ pages/ (λ˜λŠ” 3_pages/) β”‚ β”œβ”€β”€ HomePage/ β”‚ β”‚ β”œβ”€β”€ ui/ // HomePageλ₯Ό μœ„ν•œ UI μ»΄ν¬λ„ŒνŠΈ β”‚ β”‚ └── index.ts // HomePage의 Public API β”‚ β”œβ”€β”€ ProfilePage/ β”‚ β”œβ”€β”€ widgets/ (λ˜λŠ” 4_widgets/) β”‚ β”œβ”€β”€ Navbar/ β”‚ β”‚ β”œβ”€β”€ ui/ β”‚ β”‚ └── index.ts // Navbar의 Public API β”‚ β”œβ”€β”€ Sidebar/ β”‚ β”œβ”€β”€ features/ (λ˜λŠ” 5_features/) β”‚ β”œβ”€β”€ login/ β”‚ β”‚ β”œβ”€β”€ ui/ // login featureλ₯Ό μœ„ν•œ UI μ»΄ν¬λ„ŒνŠΈ β”‚ β”‚ β”œβ”€β”€ model/ // login을 μœ„ν•œ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 β”‚ β”‚ └── index.ts // login feature의 Public API β”‚ β”œβ”€β”€ registration/ β”‚ β”œβ”€β”€ entities/ (λ˜λŠ” 6_entities/) β”‚ β”œβ”€β”€ user/ β”‚ β”‚ β”œβ”€β”€ ui/ β”‚ β”‚ β”œβ”€β”€ model/ β”‚ β”‚ └── index.ts // user entity의 Public API β”‚ β”œβ”€β”€ post/ β”‚ β”œβ”€β”€ shared/ (λ˜λŠ” 7_shared/) β”‚ β”œβ”€β”€ ui/ // μž¬μ‚¬μš© κ°€λŠ₯ν•œ UI μ»΄ν¬λ„ŒνŠΈ β”‚ β”‚ └── Button/ // μ˜ˆμ‹œ: Button μ»΄ν¬λ„ŒνŠΈ β”‚ β”œβ”€β”€ lib/ // μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜, 헬퍼 β”‚ β”œβ”€β”€ config/ // 곡유 μ„€μ • β”‚ └── index.ts // shared λ ˆμ΄μ–΄μ˜ Public API (선택 사항) ``` **ꡬ쑰 기반 Import μ˜ˆμ‹œ:** - **ν—ˆμš©λ¨ (별칭 Import - 슬라이슀/λ ˆμ΄μ–΄ κ°„):** ```javascript // features/login/ui/LoginForm.tsx import { Button } from '@shared/ui/Button'; // OK: Featureκ°€ 별칭을 톡해 Shared UI μ‚¬μš© import { getUser } from '@entities/user'; // OK: Featureκ°€ Public APIλ₯Ό 톡해 User μ—”ν‹°ν‹° μ‚¬μš© ``` - **ν—ˆμš©λ¨ (μƒλŒ€ 경둜 Import - 같은 슬라이슀 λ‚΄):** ```javascript // features/login/ui/LoginForm.tsx import { validateInput } from '../lib/validation'; // OK: 'login' feature 슬라이슀 λ‚΄μ—μ„œ μƒλŒ€ 경둜 import ``` - **κΈˆμ§€λ¨ (μƒλŒ€ 경둜 Import - 슬라이슀/λ ˆμ΄μ–΄ κ°„):** ```javascript // features/login/ui/LoginForm.tsx import { config } from '../../../app/config'; // BAD: λ‹€λ₯Έ λ ˆμ΄μ–΄λ‘œμ˜ μƒλŒ€ 경둜 import { User } from '../../entities/user/model/types'; // BAD: μƒλŒ€ 경둜 + Public API 우회 ``` - **κΈˆμ§€λ¨ (Public API 우회):** ```javascript // features/login/ui/LoginForm.tsx import { userSlice } from '@entities/user/model/slice'; // BAD: λ‚΄λΆ€ λͺ¨λ“ˆ 직접 import ``` - **κΈˆμ§€λ¨ (슬라이슀 κ°„ μ˜μ‘΄μ„±):** ```javascript // features/login/model/auth.ts import { startRegistration } from '@features/registration'; // BAD: 'login' featureκ°€ 'registration' feature 직접 import ``` > πŸ’‘ 팁: μ΄λŸ¬ν•œ import κ·œμΉ™, 특히 별칭 μ‚¬μš©κ³Ό Public API μ€€μˆ˜λŠ” μ½”λ“œλ² μ΄μŠ€μ˜ λ¦¬νŒ©ν† λ§κ³Ό μœ μ§€λ³΄μˆ˜λ₯Ό 훨씬 μ‰½κ²Œ λ§Œλ“­λ‹ˆλ‹€. --- ## πŸ” μ§€μ›ν•˜λŠ” κ·œμΉ™(Rules) 이 ν”ŒλŸ¬κ·ΈμΈμ€ **Feature-Sliced Design(FSD)의 λͺ¨λ²” 사둀**λ₯Ό κ°•μ œν•˜λŠ” 일련의 ESLint κ·œμΉ™μ„ μ œκ³΅ν•©λ‹ˆλ‹€. 각 κ·œμΉ™μ€ **λͺ…ν™•ν•œ λͺ¨λ“ˆ ꡬ쑰 μœ μ§€, import μ œμ•½, μ•„ν‚€ν…μ²˜ μœ„λ°˜ λ°©μ§€**에 도움이 λ©λ‹ˆλ‹€. | κ·œμΉ™(Rule) | μ„€λͺ… | | --------------------------------- | --------------------------------------------------------------------------------------------------------- | | **fsd/forbidden-imports** | μƒμœ„ λ ˆμ΄μ–΄μ—μ„œμ˜ importλ‚˜ 슬라이슀 κ°„ ꡐ차 importλ₯Ό λ°©μ§€ν•©λ‹ˆλ‹€. | | **fsd/no-relative-imports** | μ„œλ‘œ λ‹€λ₯Έ μŠ¬λΌμ΄μŠ€λ‚˜ λ ˆμ΄μ–΄ κ°„μ˜ μƒλŒ€ 경둜 importλ₯Ό κΈˆμ§€ν•©λ‹ˆλ‹€. 같은 슬라이슀 λ‚΄μ—μ„œλŠ” μƒλŒ€ 경둜 ν—ˆμš©. | | **fsd/no-public-api-sidestep** | Public APIλ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šκ³  λ‚΄λΆ€ λͺ¨λ“ˆμ„ 직접 importν•˜λŠ” 것을 λ°©μ§€ν•©λ‹ˆλ‹€. | | **fsd/no-cross-slice-dependency** | 같은 λ ˆμ΄μ–΄ λ‚΄ μ„œλ‘œ λ‹€λ₯Έ 슬라이슀 κ°„μ˜ 직접 μ˜μ‘΄μ„±μ„ κΈˆμ§€ν•©λ‹ˆλ‹€ (features뿐만 μ•„λ‹ˆλΌ λͺ¨λ“  λ ˆμ΄μ–΄μ— 적용). | | **fsd/no-ui-in-business-logic** | λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 λ ˆμ΄μ–΄(e.g., entities)μ—μ„œ UIλ₯Ό importν•˜μ§€ λͺ»ν•˜λ„둝 ν•©λ‹ˆλ‹€. | | **fsd/no-global-store-imports** | μ „μ—­ μƒνƒœ(`store`)λ₯Ό 직접 importν•˜μ§€ λͺ»ν•˜λ„둝 ν•©λ‹ˆλ‹€. | | **fsd/ordered-imports** | λ ˆμ΄μ–΄λ³„λ‘œ importλ₯Ό κ·Έλ£Ήν™”ν•˜λ„λ‘ κ°•μ œν•©λ‹ˆλ‹€. | --- ## πŸ“Œ κ·œμΉ™ 상세 & μ˜ˆμ‹œ ### **1️⃣ fsd/forbidden-imports** **μƒμœ„ λ ˆμ΄μ–΄μ—μ„œμ˜ import와 슬라이슀 κ°„ ꡐ차 importλ₯Ό λ°©μ§€ν•©λ‹ˆλ‹€.** βœ… **ν—ˆμš©:** `features` β†’ `entities` λ˜λŠ” `shared` ❌ **κΈˆμ§€:** `features` β†’ `app` 직접 import ```js // ❌ 잘λͺ»λœ 예 (featureμ—μ„œ app을 import) import { config } from '../../app/config'; // βœ… μ˜¬λ°”λ₯Έ 예 (featureμ—μ„œ entities/sharedλ₯Ό import) import { getUser } from '../../entities/user'; import { formatCurrency } from '../../shared/utils'; ``` <br/> ### 2️⃣ fsd/no-relative-imports μ„œλ‘œ λ‹€λ₯Έ μŠ¬λΌμ΄μŠ€λ‚˜ λ ˆμ΄μ–΄ κ°„μ˜ μƒλŒ€ 경둜 importλ₯Ό κΈˆμ§€ν•©λ‹ˆλ‹€. βœ… ν—ˆμš©: ν”„λ‘œμ νŠΈ 별칭을 μ‚¬μš©ν•˜κ±°λ‚˜ 같은 슬라이슀 λ‚΄μ—μ„œ μƒλŒ€ 경둜 μ‚¬μš© ❌ κΈˆμ§€: μ„œλ‘œ λ‹€λ₯Έ 슬라이슀 κ°„μ˜ μƒλŒ€ 경둜 import ```javascript // ❌ 잘λͺ»λœ 예 (μ„œλ‘œ λ‹€λ₯Έ 슬라이슀 κ°„ μƒλŒ€ 경둜 import) import { fetchUser } from '../another-slice/model/api'; // βœ… μ˜¬λ°”λ₯Έ 예 (같은 슬라이슀 λ‚΄μ—μ„œ μƒλŒ€ 경둜 import) import { fetchData } from '../model/api'; // βœ… μ˜¬λ°”λ₯Έ 예 (μŠ¬λΌμ΄μŠ€λ‚˜ λ ˆμ΄μ–΄ κ°„μ—λŠ” 별칭 import) import { Button } from '@shared/ui/Button'; // @/shared ν˜•μ‹λ„ 지원 import { Button } from '@/shared/ui/Button'; ``` #### μ‚¬μš© κ°€λŠ₯ν•œ μ˜΅μ…˜: ```javascript // eslint.config.js νŒŒμΌμ—μ„œ: 'fsd/no-relative-imports': ['error', { // 같은 슬라이슀 λ‚΄μ—μ„œ μƒλŒ€ 경둜 import ν—ˆμš© (κΈ°λ³Έκ°’: true) allowSameSlice: true, // νƒ€μž… μ „μš© import에 μƒλŒ€ 경둜 μ‚¬μš© ν—ˆμš© (κΈ°λ³Έκ°’: false) allowTypeImports: false, // 이 κ·œμΉ™μ„ λ¬΄μ‹œν•  수 μžˆλŠ” ν…ŒμŠ€νŠΈ 파일 νŒ¨ν„΄ testFilesPatterns: ['\\.test\\.', '\\.spec\\.'], // λ¬΄μ‹œν•  import νŒ¨ν„΄ ignoreImportPatterns: [] }] ``` <br/> ### 3️⃣ fsd/no-public-api-sidestep features, widgets, entities의 λ‚΄λΆ€ λͺ¨λ“ˆμ„ 직접 importν•˜μ§€ λͺ»ν•˜λ„둝 ν•©λ‹ˆλ‹€. βœ… ν—ˆμš©: index.ts(곡개 API)λ₯Ό ν†΅ν•œ import ❌ κΈˆμ§€: feature λ‚΄λΆ€ νŒŒμΌμ— 직접 μ ‘κ·Ό ```javascript // ❌ 잘λͺ»λœ 예 (feature λ‚΄λΆ€ 파일 직접 import) import { authSlice } from '../../features/auth/slice.ts'; // βœ… μ˜¬λ°”λ₯Έ 예 (곡개 APIλ₯Ό 톡해 import) import { authSlice } from '../../features/auth'; ``` <br/> ### 4️⃣ fsd/no-cross-slice-dependency 같은 λ ˆμ΄μ–΄ λ‚΄ μ„œλ‘œ λ‹€λ₯Έ 슬라이슀 κ°„μ˜ 직접 μ˜μ‘΄μ„±μ„ λ°©μ§€ν•©λ‹ˆλ‹€ (λͺ¨λ“  λ ˆμ΄μ–΄μ— 적용, features만 ν•΄λ‹Ήν•˜μ§€ μ•ŠμŒ). βœ… ν—ˆμš©: ν•˜μœ„ λ ˆμ΄μ–΄λ₯Ό ν†΅ν•œ 톡신 ❌ κΈˆμ§€: 같은 λ ˆμ΄μ–΄ λ‚΄ μ„œλ‘œ λ‹€λ₯Έ 슬라이슀 κ°„μ˜ 직접 import ```javascript // ❌ 잘λͺ»λœ 예 (같은 λ ˆμ΄μ–΄μ—μ„œ λ‹€λ₯Έ 슬라이슀 import) import { processPayment } from '../../features/payment'; // βœ… μ˜¬λ°”λ₯Έ 예 (entities/sharedλ₯Ό 쀑간에 μ‚¬μš©) import { PaymentEntity } from '../../entities/payment'; // ❌ λ˜ν•œ 잘λͺ»λœ 예 (entities μŠ¬λΌμ΄μŠ€κ°€ λ‹€λ₯Έ entities 슬라이슀 import) import { Product } from '../../entities/product'; // 이 κ·œμΉ™μ€ 이제 features뿐만 μ•„λ‹ˆλΌ λͺ¨λ“  λ ˆμ΄μ–΄μ— μ μš©λ©λ‹ˆλ‹€! ``` <br/> ### 5️⃣ fsd/no-ui-in-business-logic λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 λ ˆμ΄μ–΄(예: entities)μ—μ„œ UIλ₯Ό importν•˜μ§€ λͺ»ν•˜λ„둝 ν•©λ‹ˆλ‹€. βœ… ν—ˆμš©: UIλŠ” widgetsλ‚˜ pages λ‚΄μ—μ„œλ§Œ μ‚¬μš© ❌ κΈˆμ§€: entitiesμ—μ„œ UI μ»΄ν¬λ„ŒνŠΈ import ```javascript // ❌ 잘λͺ»λœ 예 (entityμ—μ„œ widget import) import { ProfileCard } from '../../widgets/ProfileCard'; // βœ… μ˜¬λ°”λ₯Έ 예 (widgetμ—μ„œ entity 데이터λ₯Ό μ‚¬μš©) import { getUser } from '../../entities/user'; ``` <br/> ### 6️⃣ fsd/no-global-store-imports μ „μ—­ μƒνƒœ(store)λ₯Ό 직접 importν•˜μ§€ λͺ»ν•˜λ„둝 ν•©λ‹ˆλ‹€. βœ… ν—ˆμš©: useStore λ˜λŠ” useSelector와 같은 ν›… μ‚¬μš© ❌ κΈˆμ§€: store 객체λ₯Ό 직접 import ```javascript // ❌ 잘λͺ»λœ 예 (store 직접 import) import { store } from '../../app/store'; // βœ… μ˜¬λ°”λ₯Έ 예 (ν›… μ‚¬μš©) import { useStore } from 'zustand'; import { useSelector } from 'react-redux'; ``` <br/> ### 7️⃣ fsd/ordered-imports λ ˆμ΄μ–΄λ³„λ‘œ importλ₯Ό κ·Έλ£Ήν™”ν•˜λ„λ‘ κ°•μ œν•©λ‹ˆλ‹€. βœ… ν—ˆμš©: λ ˆμ΄μ–΄λ³„λ‘œ import μ •λ ¬ ❌ κΈˆμ§€: λ¬΄μž‘μœ„ μˆœμ„œμ˜ import ```javascript // ❌ 잘λͺ»λœ 예 (μž„μ˜μ˜ μˆœμ„œλ‘œ import) import { processPayment } from '../features/payment'; import { getUser } from '../entities/user'; import { formatCurrency } from '../shared/utils'; import { loginUser } from '../features/auth'; import { Header } from '../widgets/Header'; import { useStore } from '../app/store'; // βœ… μ˜¬λ°”λ₯Έ 예 (λ ˆμ΄μ–΄λ³„ κ·Έλ£Ήν™”) import { useStore } from '../app/store'; // App import { loginUser } from '../features/auth'; // Features import { processPayment } from '../features/payment'; import { getUser } from '../entities/user'; // Entities import { formatCurrency } from '../shared/utils'; // Shared import { Header } from '../widgets/Header'; // Widgets ``` > πŸ’‘ 팁: `npx eslint --fix` λͺ…령을 μ‚¬μš©ν•˜λ©΄, FSD λ ˆμ΄μ–΄ κ·œμΉ™μ— 따라 importλ₯Ό μžλ™ μ •λ ¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€. --- ## πŸ›  μžλ™ μˆ˜μ •(Auto-fix) 지원 `eslint-plugin-fsd-lint` λ‚΄ 일뢀 κ·œμΉ™μ€ ESLint의 `--fix` μ˜΅μ…˜μ„ 톡해 **μžλ™μœΌλ‘œ μˆ˜μ •**될 수 μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 κ°œλ°œμžλ“€μ€ **μˆ˜λ™μœΌλ‘œ μ½”λ“œλ₯Ό μˆ˜μ •ν•˜μ§€ μ•Šμ•„λ„** λΉ λ₯΄κ²Œ κ·œμΉ™ μœ„λ°˜μ„ ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ### βœ… μžλ™ μˆ˜μ •μ΄ μ§€μ›λ˜λŠ” κ·œμΉ™ λ‹€μŒ κ·œμΉ™μ€ μžλ™ μˆ˜μ •μ„ μ§€μ›ν•©λ‹ˆλ‹€: | κ·œμΉ™(Rule) | μ„€λͺ… | | ----------------------- | ------------------------------------------------------------------------- | | **fsd/ordered-imports** | Feature-Sliced Design(FSD) λ ˆμ΄μ–΄ κΈ°μ€€μœΌλ‘œ import μˆœμ„œλ₯Ό μžλ™ μ •λ ¬ν•©λ‹ˆλ‹€. | ### πŸ”§ ESLintμ—μ„œ `--fix` μ‚¬μš©ν•˜κΈ° ν”„λ‘œμ νŠΈμ˜ νŠΉμ • νŒŒμΌμ— λŒ€ν•œ μžλ™ μˆ˜μ •μ„ μ μš©ν•˜λ €λ©΄ λ‹€μŒκ³Ό 같이 μ‹€ν–‰ν•˜μ„Έμš”: ```shell npx eslint --fix your-file.js ``` ν”„λ‘œμ νŠΈ λ‚΄ λͺ¨λ“  νŒŒμΌμ— λŒ€ν•΄ μžλ™ μˆ˜μ •μ„ μ μš©ν•˜λ €λ©΄: ```shell npx eslint --fix . ``` ### πŸ“Œ μžλ™ μˆ˜μ • μ „ν›„ μ˜ˆμ‹œ ❌ μˆ˜μ • μ „ (`fsd/ordered-imports` μœ„λ°˜) ```javascript import { processPayment } from '../features/payment'; import { getUser } from '../entities/user'; import { formatCurrency } from '../shared/utils'; import { loginUser } from '../features/auth'; import { Header } from '../widgets/Header'; import { useStore } from '../app/store'; ``` <br/> βœ… μˆ˜μ • ν›„ (`npx eslint --fix` 적용됨) ```javascript import { useStore } from '../app/store'; // App import { loginUser } from '../features/auth'; // Features import { processPayment } from '../features/payment'; import { getUser } from '../entities/user'; // Entities import { formatCurrency } from '../shared/utils'; // Shared import { Header } from '../widgets/Header'; // Widgets ``` > πŸ’‘ 팁: `fsd/ordered-imports` κ·œμΉ™μ€ FSD λ ˆμ΄μ–΄λ₯Ό κΈ°μ€€μœΌλ‘œ κΉ”λ”ν•˜κ³  ꡬ쑰적인 import μˆœμ„œλ₯Ό μœ μ§€ν•©λ‹ˆλ‹€. --- ## πŸ†• μƒˆλ‘œμš΄ κΈ°λŠ₯ ### 1. 크둜슀 ν”Œλž«νΌ ν˜Έν™˜μ„± 이 ν”ŒλŸ¬κ·ΈμΈμ€ 이제 λ‚΄λΆ€μ μœΌλ‘œ 파일 경둜λ₯Ό μ •κ·œν™”ν•˜μ—¬ Windows와 Unix 기반 μ‹œμŠ€ν…œ λͺ¨λ‘μ—μ„œ μ›ν™œν•˜κ²Œ μž‘λ™ν•©λ‹ˆλ‹€. ### 2. μœ μ—°ν•œ 폴더 이름 νŒ¨ν„΄ 이제 폴더에 번호 접두사 λ“±μ˜ 이름 μ§€μ • κ·œμΉ™μ„ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€: ```js // 폴더 νŒ¨ν„΄ 지원 ꡬ성 "fsd/forbidden-imports": ["error", { folderPattern: { enabled: true, regex: "^(\\d+_)?(.*)", extractionGroup: 2 } }] ``` 이λ₯Ό 톡해 λ‹€μŒκ³Ό 같은 ꡬ쑰λ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€: ``` src/ 1_app/ 2_pages/ 3_widgets/ 4_features/ 5_entities/ 6_shared/ ``` ### 3. λ‹€μ–‘ν•œ 별칭 ν˜•μ‹ 지원 이제 ν”ŒλŸ¬κ·ΈμΈμ€ `@shared`와 `@/shared` ν˜•μ‹ λͺ¨λ‘ μ§€μ›ν•©λ‹ˆλ‹€: ```js // 별칭 ν˜•μ‹ ꡬ성 "fsd/forbidden-imports": ["error", { alias: { value: "@", withSlash: false // @/shared ν˜•μ‹μ„ μœ„ν•΄ true μ‚¬μš© } }] ``` ### 4. ν–₯μƒλœ cross-slice-dependency κ·œμΉ™ `no-cross-slice-dependency` κ·œμΉ™μ€ 이제 기본적으둜 features뿐만 μ•„λ‹ˆλΌ λͺ¨λ“  λ ˆμ΄μ–΄μ— μ μš©λ©λ‹ˆλ‹€: ```js // features λ ˆμ΄μ–΄λ§Œ μ œν•œ (λ ˆκ±°μ‹œ λ™μž‘) "fsd/no-cross-slice-dependency": ["error", { featuresOnly: true }] ``` ### 5. 사전 μ •μ˜λœ ꡬ성 ν”„λ‘œν•„ 이제 μ—¬λŸ¬ ꡬ성 프리셋을 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€: - `recommended` - ν‘œμ€€ ꢌμž₯ μ„€μ • - `strict` - μ΅œλŒ€ κ°•μ œ μˆ˜μ€€ - `base` - μ‰¬μš΄ λ„μž…μ„ μœ„ν•œ 덜 μ—„κ²©ν•œ μ„€μ • ### 6. 포괄적인 ν…ŒμŠ€νŠΈ 컀버리지 이 ν”ŒλŸ¬κ·ΈμΈμ€ 이제 λͺ¨λ“  κ·œμΉ™μ— λŒ€ν•œ κ΄‘λ²”μœ„ν•œ ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€λ₯Ό ν¬ν•¨ν•©λ‹ˆλ‹€: - κΈ°λ³Έ import μ‹œλ‚˜λ¦¬μ˜€ - μ—£μ§€ μΌ€μ΄μŠ€μ™€ λ³΅μž‘ν•œ νŒ¨ν„΄ - 경둜 λ³€ν˜• (Windows, Unix, ν˜Όν•©) - μ‚¬μš©μž μ •μ˜ ꡬ성 - μ‹€μ œ μ‚¬μš© μ˜ˆμ‹œ ### 경둜 별칭 지원 ν”ŒλŸ¬κ·ΈμΈμ€ 이제 λ‹€μ–‘ν•œ 경둜 별칭 ν˜•μ‹μ„ μ˜¬λ°”λ₯΄κ²Œ μ²˜λ¦¬ν•©λ‹ˆλ‹€: ```javascript // 두 ν˜•μ‹ λͺ¨λ‘ μ§€μ›λ©λ‹ˆλ‹€ import { UserCard } from '@entities/user'; import { UserCard } from '@/entities/user'; ``` ### 동적 import 지원 λͺ¨λ“  κ·œμΉ™μ΄ 이제 동적 importλ₯Ό μ§€μ›ν•©λ‹ˆλ‹€: ```javascript // μœ νš¨ν•œ 동적 import const UserCard = await import('@entities/user'); const { UserCard } = await import('@entities/user'); // μœ νš¨ν•˜μ§€ μ•Šμ€ 동적 import (κ·œμΉ™μ— μ˜ν•΄ 감지됨) const UserCard = await import('@entities/user/ui'); ``` ### 포괄적인 ν…ŒμŠ€νŠΈ 컀버리지 λͺ¨λ“  κ·œμΉ™μ΄ 이제 μ² μ €ν•˜κ²Œ ν…ŒμŠ€νŠΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€: - κΈ°λ³Έ import μ‹œλ‚˜λ¦¬μ˜€ - μ—£μ§€ μΌ€μ΄μŠ€μ™€ λ³΅μž‘ν•œ νŒ¨ν„΄ - 경둜 λ³€ν˜• (Windows, Unix, ν˜Όν•©) - μ‚¬μš©μž μ •μ˜ ꡬ성 - μ‹€μ œ μ‚¬μš© 예제 - 경둜 별칭 ν˜•μ‹ - 동적 import νŒ¨ν„΄ ## κ·œμΉ™ ### no-public-api-sidestep λ‚΄λΆ€ λͺ¨λ“ˆμ—μ„œμ˜ 직접 importλ₯Ό λ°©μ§€ν•˜κ³  public API μ‚¬μš©μ„ κ°•μ œν•©λ‹ˆλ‹€. ```javascript // βœ… μœ νš¨ν•¨: public API μ‚¬μš© import { UserCard } from '@entities/user'; import { UserCard } from '@/entities/user'; // 이것도 μœ νš¨ν•¨ import { UserCard } from '@entities/user/index'; // ❌ μœ νš¨ν•˜μ§€ μ•ŠμŒ: λ‚΄λΆ€ 직접 import import { UserCard } from '@entities/user/ui/UserCard'; import { UserCard } from '@entities/user/model/types'; // βœ… μœ νš¨ν•¨: public APIλ₯Ό μ‚¬μš©ν•˜λŠ” 동적 import const UserCard = await import('@entities/user'); const { UserCard } = await import('@entities/user'); // ❌ μœ νš¨ν•˜μ§€ μ•ŠμŒ: public APIλ₯Ό μš°νšŒν•˜λŠ” 동적 import const UserCard = await import('@entities/user/ui/UserCard'); ``` --- ## 🀝 μ»¨νŠΈλ¦¬λ·°μ…˜(κΈ°μ—¬) `eslint-plugin-fsd-lint`λ₯Ό κ°œμ„ ν•˜κΈ° μœ„ν•œ λͺ¨λ“  κΈ°μ—¬λ₯Ό ν™˜μ˜ν•©λ‹ˆλ‹€! μƒˆλ‘œμš΄ κ·œμΉ™ μ œμ•ˆμ΄λ‚˜ κ°œμ„  사항이 μžˆλ‹€λ©΄ 자유둭게 Pull Requestλ₯Ό λ³΄λ‚΄μ£Όμ„Έμš”. μžμ„Έν•œ λ‚΄μš©μ€ [κΈ°μ—¬ κ°€μ΄λ“œ](CONTRIBUTING.md)λ₯Ό μ°Έκ³ ν•˜μ„Έμš”. --- ## πŸ“ λΌμ΄μ„ μŠ€ 이 ν”„λ‘œμ νŠΈλŠ” MIT λΌμ΄μ„ μŠ€λ‘œ λ°°ν¬λ©λ‹ˆλ‹€. μžμ„Έν•œ λ‚΄μš©μ€ [LICENSE](LICENSE.md) νŒŒμΌμ„ μ°Έκ³ ν•˜μ„Έμš”. <br/>