FSD를 위한 ESLint 플러그인 개발 일지
아래의 링크에서 사용할 수 있습니다.
배경

항해 플러스의 과정 중 '클린 코드' 교육이 있다.
과정을 들으면서 리팩토링의 핵심 요인이나, 코드 품질을 높이기 위한 방법을 배웠다.
그리고, 그 과정에서 Feature-Sliced-Design(FSD)의 개념을 배우고 적용해보았다.
강제되는 규칙이 많아, 프로젝트나 네이버 웹툰 인턴을 하며 경험중인 실무에서 이를 적용해보려고 하는데, 마땅한 Lint 플러그인이 없어서 불편함을 느꼈다.
그래서, 이를 개발하기로 결정했다.
문제 상황
만약 도구를 쓴다면, 무조건 만들려고 하기 보다는 이미 있는 것을 활용하는 것이 좋다.
내가 가진 개발 철학 중 하나이다. 소프트웨어의 장점 중 하나는 '재사용성'이다. 그래서, 리서치 과정을 꼭 거치는 편이다.
공식 문서에서는 Steiger라는 도구를 권장하고 있었다.
그러나, 내가 필요한 설정은 없었고, ESLint가 아닌 별도 라이브러리이기에 설치해서 관리해야한다는 어려움이 있었다.
| 비교 항목 | Steiger (별도 CLI 툴) | ESLint 플러그인 |
|---|---|---|
| 설치 및 유지보수 | - 프로젝트에 별도 툴(클라이언트/서버) 설치 필요 - 추가 의존성 및 버전 관리 필요 - 툴 업데이트 시 팀 전체에 재설치 안내 | - 이미 존재하는 ESLint 구성에 플러그인만 추가 - 별도 툴 관리 부담 감소 - ESLint 버전 관리만 신경 쓰면 됨 |
| 개발 워크플로 및 에디터 통합 | - 전용 CLI를 실행해야 하므로, IDE 내 즉각적인 피드백이 어려울 수 있음 - 에디터마다 Steiger 관련 플러그인이 있을 수도 있지만, 보편적이지 않을 수 있음 | - VSCode, WebStorm 등 거의 모든 IDE에서 ESLint를 바로 연동 - 실시간으로 린트 에러 확인 가능 - GitHooks/CI 파이프라인에서 별도 세팅 없이 자동 검사 가능 |
| 규칙 제어 및 세분화(린트 룰) | - 제공되는 규칙을 기반으로 사용해야 함 - 규칙 세분화나 직접적인 커스터마이징(오픈소스 기여 제외)은 제한적 - CLI 기반이므로, 에디터 내 오토픽스(autofix) 기능 사용이 어려울 수 있음 | - ESLint의 Rule 시스템을 그대로 활용 가능 - 규칙 세분화 및 에러/워닝 등 심각도 설정이 자유롭고, 예외 처리를 쉽게 구성 - AST 분석을 통한 깊이 있는 룰 작성 가능 - ESLint autofix 등 자동 수정 활용 가능 |
| 온보딩 및 교육 | - 새로 들어온 팀원에게 Steiger 도구 설치 및 사용법을 추가로 안내해야 함 - ESLint 경험만 있는 사람에게는 별도 툴 학습 부담 | - 대부분의 개발자가 ESLint에 익숙 - “ESLint 룰이 추가됐다” 정도로 안내하면 되므로 학습 부담 최소화 - 마이그레이션 및 팀 합의가 수월 |
| 확장성 및 커스터마이징 | - 도구 자체가 제공하는 옵션, 버전을 고려해야 함 - 고도화나 수정이 필요한 경우 툴 자체 업데이트를 기다려야 함 - 오픈소스 기여 과정이 복잡할 수 있음 | - 오픈소스로 릴리스 시, 빠른 피드백 및 기여 가능 - TypeScript 확장 시 @typescript-eslint/parser 등과 쉽게 연동 - 프로젝트 요구사항에 맞게 룰을 추가/변경하기 용이 |
위와 같은 이유로, ESLint 플러그인을 리서치햇다.
검색하는 과정에서 몇가지 도구를 찾았다. 그러나, 이들은 저마다 문제를 안고 있었다.
| 도구 | 설명 | 불편한 점 |
|---|---|---|
| Eslint plugin for FSD best practices | FSD BP를 기준으로 만들어진 ESLint이다. | 마지막 관리가 4~5년전으로, 관리가 제대로 이루어지지 않고 있다. 이에 따라서, ESLint 9 이상의 최신 버전에서 강제되는 Flat Config에 대한 대응이 부족했다. |
| ESLint plugin fsd import | 아마도 개인이 사용하기 위해 만들어진 Lint 같았다. | 관리가 제대로 되고 있지 않으며 기능도 제한적이다. 신뢰성이 적어 사용이 어려웠다. |
| @feature-sliced/eslint-config | 135개의 스타와 8000명 이상이 다운받은 FSD를 위한 Lint 이다. | 제대로 된 관리가 2022년이며, Flat Config의 대응이 되고 있지 않았다. |
이 외에도 여러가지 도구를 찾아보았으나, 마음에 쏙 드는 플러그인이 없었다.
필요한 요소 중, 특히 핵심적으로 다루어야 하는 부분이 Flat Config에 대한 대응이었다.

특히나, 공식문서에서 ESLint 8.x 버전 이전의 지원이 끊긴다고 언급하고 있으므로, 신생 프로젝트에서는 ESLint 9 이상을 사용이 필요했다.
내가 찾은 방법에서는 특정 ESLint Plugin이 Flat Config를 제공하지 않을 경우, 플러그인 자체의 코드를 수정하지 않고서는 이를 Flat Config에 적용할 수 있는 방법이 없었다.
그래서, 이를 해결하기 위해 직접 ESLint Plugin을 만들기로 결정했다.
프로젝트 목표
Feature-Sliced-Design의 컨벤션을 준수하는ESLint Plugin을 만든다.Flat Config를 지원한다.ESLint Plugin을 만들면서,ESLint의 동작 방식을 이해한다.
사실 있는 설정을 가져다쓰기만 했지, ESLint에 대해서 제대로 이해하고 있지 않았다.
그래서 이번에 도전을 하면서, ESLint에 대해 제대로 이해하고, 이를 통해 Feature-Sliced-Design의 컨벤션을 준수하는 ESLint Plugin을 만들어보기로 했다.
계획
처음 시도해보는 도전이라, 모든게 새로웠고, 모든게 장벽이었다.
부족한게 많기에 전략적으로 접근할 필요가 있었다.
목표를 수립하고, 이를 효과적으로 달성하기 위해서 계획을 세웠다.
| 계획 | 내용 |
|---|---|
| 1. 기본 개념 이해 및 환경 준비 | - ESLint의 기본 개념과 작동 방식을 학습. - ESLint 플러그인을 만들기 위한 기본 환경 설정. - NPM 패키지 및 모듈 관리 기본 학습. |
| 2. ESLint 규칙의 기본 구조 이해 | - 간단한 커스텀 규칙 작성해보기. - ESLint의 AST(Abstract Syntax Tree)와 ESLint Rule API 이해. |
| 3. Feature-Sliced Design 이해, 관련 규칙 설계 및 구현 | - Feature-Sliced Design의 구조와 철학 학습. - 설계 원칙을 ESLint 규칙으로 변환하는 방법 구상. - FSD를 위한 규칙 초안 작성. |
| 4. ESLint 플러그인 개발 | - 여러 규칙을 플러그인으로 패키징. - 플러그인 테스트 및 디버깅. |
| 5. 배포 준비 및 테스트 | - 플러그인의 문서 작성. - 플러그인을 NPM에 배포하는 과정 학습. |
| 6. 실제 프로젝트에 적용해보며 테스트 | - 실제 프로젝트에 적용해보면서 정상적으로 동작하는지 사용성 테스트 진행. |
GPT와 함께 짠 커리큘럼이다. 이를 바탕으로 천천히 따라가면서 프로젝트를 진행하기로 했다.
본 글도 이 과정대로 진행할 예정이다.
1단계: 기본 개념 이해 및 환경 준비
목표
- ESLint와 Prettier의 역할과 작동 방식을 이해한다.
- ESLint 플러그인 개발을 위한 기본 환경을 설정한다.
- NPM을 사용한 패키지 관리와 개발 환경 설정을 익힌다.
ESLint란 무엇인가?
들어가기에 앞서서, ESLint가 무엇인지 간단하게 알아보자.
Find and fix problems in your javascript code.
위의 설명에서 확인할 수 있듯이, 자바스크립트 코드에서 문제를 찾아서 수정하는 도구이다.
ESLint statically analyzes your code to quickly find problems. It is built into most text editors and you can run ESLint as part of your continuous integration pipeline.
ESLint는 코드를 정적으로 분석하여 빠르게 문제를 찾아내는 것과, 일관된 코드 품질 유지를 목적으로 한다.
라이브러리나 프레임워크에 종속되지 않고, 대부분의 텍스트 에디터에서 사용할 수 있다.
또한, CI 파이프라인에서도 ESLint를 사용할 수 있다.
ESLint의 주요 기능 요약
ESLint의 주요 기능을 요약하면 다음과 같다.
| 기능 | 설명 |
|---|---|
| 문법 오류 감지 | 코드에서 발생 가능한 오류를 잡아낸다. |
| 코드 스타일 체크 | 팀 규칙이나 코드 스타일을 유지하도록 도와준다. |
| 자동 수정(Auto-fix) | 가능한 경우, 문제를 자동으로 고쳐준다. |
| 확장 가능 | 플러그인이나 규칙을 추가하여 원하는 대로 확장할 수 있다. |
ESLint가 중요한 이유
ESLint는 코드 품질을 유지하고, 팀원들 간의 코드 컨벤션을 일관되게 유지하는데 중요한 역할을 한다.
특히, 대규모 프로젝트에서는 이러한 도구가 필수적이다.
디버깅 시간을 줄여주고, 컨벤션과 관련된 불필요한 자원 낭비를 예방해준다.
즉, 생산성을 높여주는 도구이기에 중요하다고 할 수 있다.
ESLint의 동작 원리
ESLint는 다음과 같은 과정으로 동작한다.
1. 코드 입력
ESLint는 검사할 JavaScript 또는 TypeScript 파일을 읽어들인다.
2. 파싱 (Parsing)
코드를 분석하기 위해 파서(Parser)를 사용하여 AST(Abstract Syntax Tree, 추상 구문 트리)로 변환한다.
기본적으로 ESLint는 espree라는 파서를 사용하지만, TypeScript를 지원하려면 @typescript-eslint/parser 같은 커스텀 파서를 사용할 수도 있다.
AST는 코드 구조를 트리 형태로 표현하는 데이터 구조이다.
예시로 이 코드는 다음과 같은 AST로 변환된다.
Program
├── VariableDeclaration (const sum)
│ ├── VariableDeclarator
│ │ ├── Identifier (sum)
│ │ ├── ArrowFunctionExpression
│ │ ├── Parameters (a, b)
│ │ ├── Body (BinaryExpression +)
3. 규칙(Rules) 적용
ESLint는 AST를 순회하면서 설정된 규칙(Rules)을 적용한다.
규칙은 노드 유형(Node Type)에 따라 동작하며, 특정한 패턴이 발견되면 오류를 보고하거나 자동으로 수정할 수 있다.
예를 들어서 no-console 규칙이 활성화되어 있다면, console.log() 같은 코드가 있으면 ESLint가 경고를 띄워준다.
이때, ESLint는 각 노드에 대해 방문자(Visitor) 패턴을 사용하여 규칙을 검사한다.
이 규칙은 temp라는 변수를 사용하면 ESLint가 경고를 띄우는 간단한 예제이다.
객체(Element)를 유지한 채 새로운 기능(검사 규칙)을 추가하기 위해
ESLint는 코드(AST)를 직접 변경하지 않는다.
대신, 특정한 노드(객체)에 대해 새로운 기능(규칙 적용)을 수행할 수 있도록 create(context) 안에서 방문자 함수를 정의한다.
각 노드 유형을 방문(Visit)하면서 특정 작업을 수행하기 위해
방문자 패턴에서는 Visitor 객체가 객체(Element)를 순회하며 특정 작업을 수행하는데, ESLint에서는 각 AST 노드 타입마다 방문할 수 있는 메서드를 제공한다.
예제 코드에서 Identifier(node) 함수는 AST에서 Identifier(변수 이름) 노드를 방문할 때 실행되는 방문자(Visitor) 역할을 수행한다.
4. 리포팅 (Reporting)
- 검사 결과를 ESLint가 콘솔이나 파일로 출력한다.
src/index.js
2:5 error Unexpected console statement no-console
2:5→ 2번째 줄, 5번째 칸에서 문제가 발생error→ 에러 수준Unexpected console statement→ 규칙에서 정의한 메시지no-console→ 적용된 ESLint 규칙 이름
5. 자동 수정 (Auto-fixing)
- 일부 규칙은 자동으로 고칠 수 있다. (--fix 옵션 사용)
예를 들어 semi(세미콜론 강제) 규칙이 적용된 경우:
자동 수정 후:
ESLint의 주요 컴포넌트
ESLint는 다음과 같은 주요 컴포넌트로 구성되어 있다.
1. Parser (파서)
- 코드를 AST로 변환하는 역할을 한다.
- 기본적으로
espree를 사용하지만, TypeScript나 JSX를 지원하려면 다른 파서를 지정해야 한다.- TypeScript 지원:
@typescript-eslint/parser - Babel 지원:
babel-eslint
- TypeScript 지원:
2. Rule Engine (규칙 엔진)
- AST를 순회하면서 규칙을 실행 및 적용하는 역할을 한다.
- ESLint는 내장 규칙이 있지만, 커스텀 규칙을 추가할 수도 있다.
eslint-plugin을 통해 사용자 정의 규칙을 추가할 수 있다.
3. Formatter (출력 포맷터)
- ESLint의 결과를 콘솔이나 파일로 출력하는 역할을 한다.
- 기본적으로
stylish포맷터를 사용하지만,--format옵션을 통해 JSON, HTML, Markdown 등의 다양한 포맷터를 사용할 수 있다. - 예)
eslint src --format json
4. Fixer (자동 수정기)
--fix옵션을 사용하면, ESLint가 일부 규칙을 자동으로 수정해준다.- 하지만 논리적인 문제는 수정할 수 없다. (예: no-unused-vars 같은 문제)
ESLint 설정 파일의 역할
ESLint의 동작 방식은 설정 파일을 기반으로 결정된다.
files: 특정 파일 확장자(.ts, .tsx)에 대한 규칙을 적용.languageOptions.parser: AST(Abstract Syntax Tree) 변환을 위해 @typescript-eslint/parser 사용 (TypeScript 코드 분석용).plugins: @typescript-eslint/eslint-plugin을 추가하여 TypeScript 전용 ESLint 규칙을 활성화.rules: 코드 스타일 및 코드 품질 규칙을 설정.
ESLint의 동작을 깊이 이해하기 위한 추가 개념
1. AST(Abstract Syntax Tree, 추상 구문 트리)를 이용한 코드 분석
ESLint는 AST를 기반으로 동작하므로, AST를 직접 확인하면 규칙 작성이 쉬워진다.
- AST Explorer같은 도구를 활용하여 코드의 AST를 시각적으로 확인할 수 있다.
2. 플러그인과 확장 기능
ESLint는 플러그인을 추가하여 기능을 확장할 수 있다. (내가 본 프로젝트를 통해서 만들고자 하는 요소이다.)
- 예제: React용 플러그인 추가
- 설정 파일에 추가
files: **/*.jsx, **/*.tsx 파일에 React 관련 ESLint 규칙 적용.plugins: eslint-plugin-react을 추가하여 React 관련 규칙 활성화.rules: react.configs.recommended.rules → React 추천 규칙을 그대로 적용(plugin:react/recommended와 동일).
기본 개발 환경 준비
1. Node.js 설치
ESLint는 Node.js 환경에서 동작하므로, Node.js를 설치해야 한다.
Node.js 공식 사이트에서 LTS 버전을 다운로드 받아 설치한다.
2. 프로젝트 디렉토리 생성
ESLint 플러그인을 개발할 프로젝트 디렉토리를 생성한다.
3. NPM 프로젝트 초기화
NPM 프로젝트를 초기화한다.
이렇게 하면 package.json 파일이 생성된다. 이 파일은 프로젝트의 설정과 의존성을 관리한다.

4. ESLint 설치
ESLint를 설치한다.
--save-dev 옵션을 사용하여 개발 의존성으로 설치한다. (개발 환경에서만 사용하겠다는 의미이며, 빌드 시에는 포함하지 않겠다는 의미이다.)
5. ESLint 초기화
ESLint를 초기화한다.
이 명령어를 실행하면, ESLint 초기화 설정이 시작된다.
- "How would you like to use ESLint?" → "To check syntax, find problems, and enforce code style" 선택.
- "What type of modules does your project use?" → "JavaScript modules (import/export)" 선택.
- "Which framework does your project use?" → "None of these" 선택 (React는 나중에 필요하면 추가할 수 있다).
- "Does your project use TypeScript?" → "No" 선택 (초반은 JavaScript로 진행하기 때문).
- "Where does your code run?" → "Node" 선택.
- "Would you like to install them now?" → "Yes" 선택.
- "Which package manager do you want to use?" → "npm" 선택.

Prettier 설치
Prettier는 코드 스타일을 자동으로 포멧팅해주는 도구이다.
ESLint와 함께 사용하면 코드 품질과 스타일을 모두 관리할 수 있다.
이에 대해서는 제일 마지막에 다룰 것이므로 우선은 설치만 해두자.
디렉토리 구조 설계
초기에 작업할 디렉토리 구조는 단순하게 유지한다.
eslint-fsd-plugin/
├── node_modules/
├── .eslintrc.js
├── package.json
├── package-lock.json
├── src/
└── README.md
2단계: ESLint 규칙의 기본 구조 이해
목표
- ESLint가 코드를 분석하는 방식(파싱, AST 활용)을 이해한다.
- 간단한 ESLint 규칙을 직접 만들어 본다.
- 규칙을 적용하고 실행하는 방법을 익힌다.
ESLint 규칙 다시 한번 상기하기
앞에서 이미 ESLint가 어떻게 동작하는지를 자세히 다루었지만, 프로젝트 진행을 위해서는 중요한 내용이니 다시한번 상기해보자.
ESLint는 단순한 문자열 비교가 아니라 AST(Abstract Syntax Tree, 추상 구문 트리)를 기반으로 동작한다.
AST란?
AST는 코드의 구조를 트리 형태로 표현한 것이다.
예를 들어서 아래의 콛르르 살펴보자.
이 코드의 AST는 대략 다음과 같이 표현된다.
Program
├── VariableDeclaration (const message = "Hello, world!")
│ ├── Identifier (message)
│ └── Literal ("Hello, world!")
└── ExpressionStatement (console.log(message))
├── MemberExpression (console.log)
└── Identifier (message)
이렇게 AST를 통해 코드의 구조를 트리 형태로 표현하게 된다.
이 트리를 이용해서 ESLint는 코드를 분석하고 규칙을 적용한다.
간단한 ESLint 플러그인 만들기
원리는 이해했는데, 이제 활용해서 어떻게 규칙을 만들 수 있는지 감이 잡히지 않는다.
그래서, 간단한 규칙을 만들어보면서 실습해보고자 한다.
앞선 예제에서 서술한 console.log를 사용하지 못하도록 하는 규칙을 만들어보자.
1. 규칙 디렉토리 생성
먼저, 규칙을 저장할 디렉토리를 생성한다.
여기서 -p 옵션은 부모 디렉터리를 자동으로 생성하는 옵션이다.
예를 들어, src/rules 폴더를 만들려고 하는데 src가 존재하지 않는다면, mkdir -p를 사용하면 src부터 자동으로 생성해 준다.
2. 규칙 파일 생성
규칙을 저장할 파일을 생성한다.
3. 규칙 코드 작성
이제, no-console-log.js 파일에 규칙 코드를 작성한다.
이 코드는 다음과 같은 역할을 수행한다.
- ESLint는
CallExpression노드를 찾는다. -> 이는 함수 호출을 의미한다. - 만약
console.log를 호출한 경우:node.callee.object.name === "console"node.callee.property.name === "log"
- 위 조건을 만족하면 ESLint가 경고 메세지를 띄운다.
4. 규칙을 바탕으로 플러그인 만들기
이제 eslint.config.mjs에서 우리가 만든 규칙을 등록해볼 차례이다.
Flat Config에서는 기존 .eslintrc.js 방식과 다르게 플러그인을 명시적으로 등록하고, 네임스페이스를 사용하여 규칙을 호출해야 한다.
위와 같이 plugins 속성에 플러그인을 등록하고 [플러그인명]/[규칙명] 형식으로 규칙을 호출한다.
그래서 no-console-log.js와 같은 규칙을 만들었다고 하더라도, 플러그인은 별도로 설정해주어야 한다.
현재는 규칙만을 만들었을 뿐 플러그인을 만들지는 않았다.
ESLint는 커스텀 규칙 적용을 위해서는 플러그인을 요구한다.
따라서, 플러그인을 만들어서 규칙을 적용해야 한다.
(1) 플러그인 만들기
index.js에서 규칙들(rules)을 플러그인 형식으로 감싸야 한다.
Flat Config에서는 플러그인 내에서 rules 객체를 정의하는 것이 필수이다.
이때 파일명은 index.js가 아니어도 된다. (예: plugin.js)
그러나, 관례상 플러그인의 시작점이기도 하기에, index.js라는 명칭을 사용했다.
다음과 같이 만들어보자.
위와 같이 rules 객체를 만들어서 규칙을 등록한다.
여기서 중요한 점은 다음과 같다.
rules객체를 만들어서 규칙을 등록한다.rules객체를 포함하여 내보내야 한다.export default { rules: { "no-console-log": noConsoleLog } };형태로 설정해야 Flat Config에서 올바르게 인식한다.
5. Flat Config에서 만든 플러그인 사용하기
이제 eslint.config.mjs에서 플러그인을 명확히 등록하고 네임스페이스를 설정해야 한다.
파일을 열어서 다음과 같이 추가한다.
여기서 중요한 점
- plugins 객체 내에 fsd: eslintFsdPlugin 형태로 플러그인을 등록해야 한다.
- rules에서 반드시 "fsd/no-console-log"처럼 네임스페이스(fsd/)를 붙여서 호출해야 한다.
- "no-console-log" → ❌ 에러 발생
- "fsd/no-console-log" → ✅ 올바른 방식
위와 같이 이미 내부에 값이 들어있을 수 있다.
이는 위쪽에서 결과물 설명 항목에서 서술했던 내용으로,
Node.js 환경이나 기본적인 JavaScript 규칙을 적용하는 설정이다.
이 설정은 그대로 두고, 새로 만든 규칙을 추가하면 된다.
Flat Config는 배열 방식으로 여러 속성을 연결해서 적용할 수 있다.
그렇기에 위와 같이 적용시켜주면 된다.
이때 참고로 위 코드는 TypeScript의 타입 정의를 의미한다.
이를 통해서 TypeScript가 해당 변수가 Linter.Config[] 타입이라는 걸 알 수 있다.
타입스크립트를 사용하지 않는다면 지워도 되지만, 어떻게 될 지 모르니 가급적 그냥 두는 것을 추천한다.
5. ESLint 실행 테스트
이제 console.log가 포함된 파일을 만들어 실행해보자.
(1) 테스트용 파일 생성
그리고 test.js 파일에 다음의 코드를 추가한다.
(2) ESLint 실행
(3) 실행 결과 확인

정상적으로 에러가 출력되는 것을 확인할 수 있다.
6. npx eslint 실행 시 디버깅
모든 설정을 올바르게 적용한 후, ESLint가 플러그인을 정상적으로 불러오는지 확인해야 한다.
디버깅 결과에서 확인할 점
- Loaded plugin: fsd
Loaded plugin: fsd로그가 있어야 플러그인이 정상적으로 로드된 것이다.
- Applying rule: fsd/no-console-log
Applying rule: fsd/no-console-log로그가 있어야 규칙이 적용되고 있는 것이다.
- ESLint 실행 결과
test.js 1:1 error console.log는 사용하지 마세요. fsd/no-console-log- 위와 같이 오류가 발생해야 정상적으로 동작하는 것이다.
7. 정리
이번 단계에서는 다음과 같은 내용을 진행했다.
| 단계 | 수정 사항 | 설명 |
|---|---|---|
| 1️⃣ 규칙 정의 수정 | rules/no-console-log.js에서 meta.messages와 context.report() 올바르게 설정 | Flat Config에서는 meta 정보가 필수 |
| 2️⃣ 플러그인 설정 | index.js에서 rules 객체를 포함하여 내보냄 | rules: { "no-console-log": noConsoleLog } 형태로 정의 |
| 3️⃣ Flat Config 적용 | eslint.config.mjs에서 plugins.fsd로 등록하고, "fsd/no-console-log" 형태로 규칙 적용 | Flat Config에서는 plugins 객체가 필수 |
| 4️⃣ ESLint 디버깅 | npx eslint --debug src/test.js 실행 | Loaded plugin: fsd 및 Applying rule: fsd/no-console-log 확인 |
- Flat Config에서는 ESLint 플러그인을 네임스페이스로 등록해야 한다.
- "no-console-log" → ❌ 오류 발생
- "fsd/no-console-log" → ✅ 올바른 방식
- rules 객체를 index.js에서 플러그인으로 내보내야 한다.
export default { rules: { "no-console-log": noConsoleLog } };→ ✅ 올바른 방식
- ESLint 실행 전에 npx eslint --debug로 플러그인이 정상적으로 로드되는지 확인해야 한다.
- Loaded plugin: fsd 로그를 반드시 확인해야 함.
3단계: Feature-Sliced Design(FSD) 기반 ESLint 규칙 설계 및 구현
지금까지 간단한 ESLint 규칙을 만들어보았다.
지금부터는 Feature-Sliced Design(FSD)를 이해하고, 이에 대한 규칙 설계를 진행해보자.
목표
- Feature-Sliced Design(FSD)의 개념을 다시 한번 정리한다.
- FSD 구조에서 ESLint 규칙이 어떤 역할을 해야 하는지 설계한다.
- 기본적인 FSD 규칙을 직접 구현해본다.
Feature-Sliced Design(FSD)이란?

FSD는 프로젝트를 기능(feature) 중심으로 구조화하여 확장성과 유지보수성을 높이는 아키텍처이다.
프론트엔드 프로젝트를 기능 중심으로 구성하여 유지보수성과 확장성을 극대화하는 아키텍쳐 패턴이다.
기존의 pages, components, services 같은 폴더 기반 구조와 달리, 비즈니스 도메인과 기능(feature)에 따라 구조화하는 것이 핵심이다.
FSD의 핵심 원칙
1. 기능 중심의 구조 (Feature-Oriented Architecture)
- 기존의 Layer-Based 구조(예:
components/,services/폴더에 모든 컴포넌트와 서비스가 모이는 구조)와 다르게, 각 기능 단위로 폴더를 나누고 해당 기능과 관련된 모든 요소(UI, 상태, API 호출, 로직 등)를 한곳에 배치하는 방식을 따른다. - 예를 들어, 인증(Authentication)과 관련된 모든 요소를
features/auth/내부에 배치하고, 결제(Payment) 관련 요소는features/payment/에 배치하는 방식이다. - 기능 단위로 그룹화하면 재사용성이 증가하고 유지보수가 쉬워진다.
REST API 처럼 디렉토리 자체로 코드가 무엇을 하는 지 명확하게 의미를 드러내는 효과를 가진다.
가령 features/auth/ 디렉토리는 인증 처리와 관련된 모든 코드를 포함하고, features/payment/ 디렉토리는 결제 처리와 관련된 모든 코드를 포함한다.
entities/auth/디렉토리의 경우는 인증 정보와 관련된 로직을 처리와 관련된 코드를 포함한다.
이처럼 디렉토리 이름만 보고도 어떤 기능을 담당하는 지 쉽게 파악할 수 있다는 장점이 있다.
2. 레이어 기반 아키텍쳐 (Layered Architecture)
- FSD는 7개의 레이어로 구성되며, 상위 레이어는 하위 레이어를 참조할 수 있지만, 반대로 하위 레이어가 상위 레이어를 참조하는 것은 금지된다. (상향식 의존성 방지)
- ✅ 올바른 참조 관계
App → Process → Pages → Widgets → Features → Entities → Shared - ❌ 잘못된 참조 예시
Features → Pages (X) // Features 레이어가 Pages를 참조하면 안 됨 Entities → Features (X) // Entities가 Features를 참조하면 안 됨
이러한 레이어 구조 덕분에 FSD는 코드 간 결합도를 줄이고, 유지보수성을 극대화할 수 있다.
3. 올바른 의존성 관리 (Dependency Rules)
- FSD에서는 명확한 의존성 규칙을 준수해야 한다.
- 특히, import할 때 상위 요소를 참조할 수 없도록 강제하는 것이 핵심이다.
규칙: 상위 레이어를 참조할 수 없음
- features/ 내부에서는 pages/ 또는 widgets/의 코드를 import할 수 없다.
- entities/ 내부에서는 features/ 또는 widgets/을 import할 수 없다.
- ✅ 올바른 import 예시
- ❌ 잘못된 import 예시
이 원칙을 지키면, 하위 모듈이 상위 모듈에 의존하지 않으므로 결합도가 낮아지고, 기능을 독립적으로 유지보수하기 쉬워진다.
4. 도메인 중심 설계 (Domain-Driven Design, DDD)
- 각 도메인(예: 사용자, 상품, 결제 등)을 기능 단위로 그룹화하여 설계한다.
- 기존 MVC 패턴에서는 모델과 서비스가 별도로 존재하지만, FSD에서는 기능(feature) 중심으로 관련된 모든 요소(UI, 상태, API, 로직 등)를 한곳에 모아 배치하여 도메인 주도 설계와 유사한 접근법을 취한다.
이렇게 하면 코드 탐색이 쉬워지고, 기능 단위로 유지보수가 간편해지는 장점이 있다.
5. 재사용성 및 모듈화 증가
Shared레이어에 공통적으로 사용될 유틸리티, 스타일, API 함수, 기본 컴포넌트 등을 배치하여 중복을 방지하고, 프로젝트 전반에서 쉽게 재사용할 수 있도록 한다.- 컴포넌트, 상태관리, API 요청을 Features 단위로 모듈화하여 독립적인 개발이 가능해진다.
- 예제:
/shared/ ├── api/ ├── ui/ ├── utils/ - 예를 들어, 모든 페이지에서 재사용할 수 있는
Button컴포넌트는shared/ui/Button.tsx에 위치해야 한다.
6. 유지보수성 및 확장성 증가
- FSD의 구조를 따르면 프로젝트가 커져도 유지보수가 쉬워진다.
- 새로운 기능을 추가할 때, 기존 코드에 영향을 주지 않고 독립적으로 개발할 수 있다.
- 리팩토링이 용이함 → 특정 기능만 수정해도 전체 프로젝트에 영향을 주지 않는다.
- 팀 협업이 쉬워짐 → 기능 단위로 작업을 나눠 진행 가능하다.
특히, FSD는 문서와 관련 예제가 굉장히 자세하다.
만약 이를 도입한다고 하면 팀 내에서 아키텍쳐와 관련해서 생각의 동기화를 위한 비용을 많이 줄일 수 있다.
자세한 문서를 기반으로 판단을 내리면 되고, 애매한 것들만 합의를 하면 되기 때문이다.
7. 정리
| 장점 | 설명 |
|---|---|
| 코드의 가독성과 유지보수성이 높아짐 | 기능별로 코드가 정리되어 있어서 찾기 쉬움 |
| 확장성이 뛰어남 | 프로젝트가 커질수록 FSD의 장점이 극대화됨 |
| 의존성이 명확해짐 | 하위 레이어에서 상위 레이어를 참조하지 못하도록 강제함으로써 결합도를 낮춤 |
| 도메인 중심 개발이 가능해짐 | 애플리케이션이 실질적인 비즈니스 로직과 잘 맞아떨어짐 |
| 리팩토링이 쉬워짐 | 기능(feature) 단위로 수정할 수 있어 안정적인 변경이 가능 |
| 팀원 간 협업이 용이함 | 각 기능 단위로 작업을 나눠 진행할 수 있음 |
만약 더 자세한 정보를 원한다면 다음의 글을 추천한다.
FSD에서 ESLint 규칙이 필요한 이유
FSD는 기능 중심으로 프로젝트를 구조화하여 확장성과 유지보수를 높이는 방식이다.
그렇지만, 개발자가 이를 일관되게 유지하지 않으면 오히려 혼란을 초래할 수 있다.
| 문제 유형 | 예제 | 문제점 |
|---|---|---|
| ❌ 잘못된 레이어 import | features/에서 app/을 import | features는 app을 몰라야 하지만, 의존성이 생김 |
| ❌ 잘못된 경로 import | import Button from "../../shared/ui/Button"; | 상대 경로(../../)로 import하면 유지보수 어려움 |
| ❌ 잘못된 Slice 간 의존성 | features/auth가 features/payment를 직접 import | 서로 독립적이어야 하지만 강한 의존성이 생김 |
| ❌ 잘못된 전역 상태 접근 | features/에서 app/store.ts를 import | feature는 global store를 직접 몰라야 함 |
| ❌ 잘못된 UI 요소 import | entities/에서 widgets/을 import | 비즈니스 로직(entities)이 UI를 몰라야 함 |
위와 같은 문제들이 개발자가 자주 유발할 수 있는 실수들이다.
나는 어떤 규칙을 만들 것인가?
FSD의 원칙을 깨는 코드를 감시하는 규칙을 만들 것이다.
그리고, 당연하게도 이 규칙은 Flat Config를 기준으로 만들 것이며, 플러그인을 만들어서 적용할 것이다.
옵션들은 기본적으로 error로 설정할 것이다.
그렇지만, 당연하게도 rules는 사용자 마음대로 수정할 수 있으니, 사용자가 필요에 따라서 이를 warn 하거나 off 할 수 있도록 구현하고자 한다.
내가 만들 ESLint 규칙 목록
| 규칙 이름 | 설명 | 예제 (잘못된 코드) |
|---|---|---|
forbidden-imports | 상위 레이어 import 및 cross-import 금지 | features/ → app/ import 금지 |
no-relative-imports | 상대 경로 import 금지, import 시 @shared/ui 같은 alias를 사용하도록 강제 | import Button from "../../shared/ui/Button"; ❌ |
no-public-api-sidestep | public API(index.ts) 우회 import 금지 | import { authSlice } from "../../features/auth/slice.ts"; ❌ |
no-cross-slice-dependency | feature slice 간 import를 금지 (features/auth → features/payment ❌) | import { processPayment } from "../payment"; ❌ |
no-ui-in-business-logic | 비즈니스 로직에서 UI import 금지 (entities → widgets 방지) | import { Header } from "../../widgets/Header"; ❌ |
no-global-store-imports | 전역 store 직접 import 금지 | import store from "../../app/store.ts"; ❌ |
ordered-imports | import 정렬 (레이어별 그룹화) | import { Button } from '@shared/ui'; import { User } from '@/entities/user'; ❌ |
- 규칙 이름은 FSD의 린터인 steiger를 참고해서 작성했다.
forbidden-imports를 제외하면 중복되는 요소 없이steiger에서 지원하지 않는 부분에 대해서 린팅을 지원하도록 설계했다.- 추후
steiger에 있는 내용을 추가해볼 예정이다.
각 규칙의 필요성 설명
1. forbidden-imports: 올바른 레이어 간 의존성 적용
- 상위 레이어에서 하위 레이어를 import하는 것을 금지한다.
- 예를 들어,
features/에서app/을 import하는 것을 방지한다. - 이 규칙을 통해 FSD의 레이어 구조를 지키도록 유도한다.
2. no-relative-imports: 경로 alias 사용 강제
- 상대 경로(
../../)를 사용하는 것을 금지하고, alias(@shared/ui)를 사용하도록 강제한다. - 코드의 가독성을 높이고 유지보수를 쉽게 만들기 위함이다.
3. no-public-api-sidestep: public API를 통한 import 강제
- 각 slice(
features,entities,widgets)의 내부 파일을 직접 import하지 않도록 강제한다. - 항상
index.ts를 통해 import하도록 제한한다.
4. no-cross-slice-dependency: Slice 간 의존성 제한
- feature slice 간의 import를 금지한다.
features/auth에서features/payment를 import하는 것을 방지한다.- 각 feature가 독립적으로 동작할 수 있도록 하기 위함이다.
5. no-ui-in-business-logic: UI 레이어 간 import 제한
- 비즈니스 로직(
entities)이 UI(widgets)를 import하지 못하도록 제한한다.
6. no-global-store-imports: 전역 상태 관리 접근 제한
- feature, widgets, entities 등에서 전역 상태(store)를 직접 import하는 것을 금지한다.
- 전역 상태는
useStore,useSelector같은 상태 관리 훅을 통해 접근해야 한다. - Redux, Zustand 등 다양한 상태 관리 라이브러리를 고려한 일반적인 규칙을 적용한다.
features/auth/AuthForm.tsx에서 store를 직접 import하고 있다.- 전역 상태는 store 객체를 직접 접근하는 방식이 아니라, 훅을 통해 접근해야 한다.
7. ordered-imports: import 구문 그룹화 강제
import구문을 그룹화하여 코드 가독성을 높이도록 강제한다.- 예를 들어, import 구문을 같은 레이어끼리 그룹화하여 코드 가독성을 높이는 것을 목표로 한다.
import순서가 랜덤하게 섞여 있는 경우 이를 정리할 수 있도록 강제한다.
import정렬이 엉망인 것을 볼 수 있다.- 한눈에 보기 어렵고 유지보수가 어렵다는 문제가 존재한다.
ESLint 규칙 구현하기
이제 위에서 설계한 규칙들을 구현해볼 것이다.
no-relative-imports 규칙 구현
no-public-api-sidestep 규칙 구현
no-cross-slice-dependency 규칙 구현
no-ui-in-business-logic 규칙 구현
no-global-store-imports 규칙 구현
전역 상태를 어떤 디렉토리에 넣을 지는 프로젝트에 달려있다. 따라서 프로젝트 별로 이게 고려가 되어야 하며, FSD에서 추구하는 전역상태 관리 요소를 기준으로도 이게 고려가 되어야 한다.
다만, 여기서는 정말 보편적인 상황을 가정해서 app/store와 shared/store를 금지하는 규칙을 만들었다.
store 요소가 model에 들어가는 경우도 있고 해서, 이 규칙은 추후 수정할 예정이다.
forbidden-imports 규칙 구현
| 레이어 | import 가능 대상 |
|---|---|
| app | processes, pages, widgets, features, entities, shared ✅ |
| processes | pages, widgets, features, entities, shared ✅ |
| pages | widgets, features, entities, shared ✅ |
| widgets | features, entities, shared ✅ |
| features | entities, shared ✅ |
| entities | shared ✅ |
| shared | 아무 레이어도 import하지 않음 ✅ |
이 규칙은 위 구조를 지켜주게 하는 규칙이다.
ordered-imports 규칙 구현
4단계: ESLint 플러그인 개발 및 패키징
이제 ESLint 플러그인을 개발하고 패키징하는 방법을 알아보자.
목표
- 개별 규칙을 하나의 ESLint 플러그인 패키지로 묶는다.
- NPM에서 배포 가능한 eslint-plugin-fsd 형태로 정리한다.
- 테스트 및 디버깅을 통해 정상적으로 작동하는지 확인한다.
ESLint 플러그인 구조 정리
지금까지 우리는 rules/ 폴더에 개별 규칙을 구현했다.
플러그인 패키지로 정리하기 위해 아래와 같은 디렉토리 구조를 만들어주자.
테스트를 위해서 포함시켰던
no-console-log.js는 지우자.
eslint-fsd-plugin/
├── src/
│ ├── rules/
│ │ ├── forbidden-imports.js
│ │ ├── no-relative-imports.js
│ │ ├── no-public-api-sidestep.js
│ │ ├── no-cross-slice-dependency.js
│ │ ├── no-ui-in-business-logic.js
│ │ ├── no-global-store-imports.js
│ │ ├── ordered-imports.js
│ ├── index.js <-- 플러그인 엔트리 포인트
├── tests/ <-- ESLint 규칙 테스트 코드
├── package.json
├── eslint.config.mjs (테스트용 Flat Config)
├── README.md <-- 문서화
위와 같은 디렉토리가 되는 것을 확인할 수 있다.
ESLint 플러그인 패키지화
지금부터는 내가 만들어둔 ESLint 규칙을 패키지화하고 NPM에 배포하기 위한 준비 과정이다.
1. package.json 설정
ESLint 플러그인을 하나의 패키지로 만들기 위해서 package.json을 설정해야 한다.
아래와 같은 내용을 package.json에 추가하자.
name: eslint-plugin-fsd-lint로 설정한다. (eslint-plugin-xxx 형태)main:src/index.js를 플러그인 엔트리 포인트로 지정한다.type:module로 설정한다.scripts:lint와test스크립트를 추가한다.lint: ESLint 실행.test: 테스트 실행.
keywords: ESLint, plugin, feature-sliced-design, fsd, lint 키워드를 추가한다.author: 작성자를 추가한다. (내 깃허브 아이디를 적었다.)license: MIT 라이선스를 적용한다.dependencies: ESLint가 필수적으로 있어야 하므로 추가한다.peerDependencies: ESLint가 필수적으로 있어야 하므로 추가한다.
2. index.js 플러그인 엔트리 포인트 작성
- 모든 규칙을
rules객체에 등록하여 플러그인에서 사용할 수 있도록 한다.
플러그인 테스트
1. ESLint 설정 파일 수정
테스트를 위해 eslint.config.mjs를 수정한다.
- ESLint 플러그인을 테스트할 수 있도록
eslint.config.mjs에서fsdPlugin을 등록한다.
2. 테스트 파일 추가
tests/ 디렉토리에 fsd-rules.test.js 파일을 추가한다.
- ESLint 실행 후, 규칙 위반 메세지를 출력하여 테스트를 진행한다.
3. 테스트 실행
패키지 NPM 배포 준비
NPM에 배포하려면 .npmignore 파일을 추가하자.
tests/
node_modules/
.eslintrc.js
eslint.config.mjs
.idea/
.vscode/
.DS_Store/5단계: 플러그인 문서화 및 NPM 배포
사실 이미 배포는 다 진행되었다.
이제는 배포된 요소를 다듬는 작업을 진행할 차례이다.
목표
README.md파일을 작성하여 플러그인의 사용 방법 및 규칙 설명을 정리한다.- GitHub 및 NPM 페이지에서 사용자가 쉽게 설정할 수 있도록 가이드를 제공한다.
- 패키지 배포 완료 후, 버전 관리 및 업데이트 전략을 수립한다.
코드 로컬라이징
공식적으로 코드를 다루기 위해서 문서화 및 로컬라이징 작업을 해야한다.
우리는 지금까지 모국어인 한글을 기반으로 주석 및 에러메세지 등을 작업했었다.
지금부터는 한글을 걷어내고 영어로 바꾸어야 하며, 모두 영어로 다뤄질 예정이다.
README.md를 비롯한 문서 작성
플러그인의 사용 방법 및 규칙 설명을 README.md에 작성하자.
영어만 지원하는 것이 아니라, 한국어도 지원할 것이기 때문에 둘 모두 작성하는 방향으로 진행하고자 한다.
이에 대한 요소는 사진으로 첨부한다.

NPM 배포
작성된 문서를 바탕으로 다시금 NPM에 배포하자.
버전 1.0.0에서 1.0.1로 올려준다.
NPM과 Github 연동
NPM과 Github를 연동하면, Github에 코드를 푸시하면 자동으로 NPM에 배포되는 기능을 사용할 수 있다.
package.json에 Github 저장소 정보를 추가
GitHub과 NPM을 연결하려면 package.json에 repository 및 bugs 정보를 추가해야 해야 한다.
버전을 1.0.1에서 1.0.2로 올려준다.
"repository"→ GitHub 저장소 주소 연결"bugs"→ 이슈 트래킹 URL 추가"homepage"→ GitHub의 README.md 링크 추가
pakcage.json 업데이트 후 커밋 & 푸쉬
수정된 package.json을 Github에 반영한다.
NPM 패키지 업데이트
업데이트된 정보를 포함한 패키지를 다시 배포한다.
이제 마지막으로 릴리즈 노트를 업데이트하면 관련 작업은 마무리가 된다.
CI/CD 설정
GitHub에서 새로운 태그를 푸시하고 Release를 생성하면 자동으로 NPM에 배포되도록 만들어보자.
이를 위해 GitHub Actions를 설정하여 CI/CD(Continuous Integration/Continuous Deployment)를 구성할 것이다.
GitHub Actions에서 NPM에 배포하려면 NPM Access Token이 필요하다.
아래 단계를 따라 NPM에서 Token을 발급받으면 된다.
1️⃣ NPM 공식 홈페이지로 이동 2️⃣ 오른쪽 상단 프로필 클릭 → "Access Tokens" 선택 3️⃣ "Generate New Token" 클릭 4️⃣ "Automation" 타입의 Token 생성 (자동화 배포에 필요하다) 5️⃣ 발급된 Token을 복사해둠 (나중에 GitHub에 추가할 예정이다)
✅ 이제 NPM Access Token이 준비된다. (Token은 노출되지 않도록 잘 보관해야 해야 한다.)
6단계 : 실제 프로젝트에 적용해보기
이제 만들어진 Lint를 실제 프로젝트에 적용해보자.
다행히도, 항해플러스를 진행하면서 FSD를 적극적으로 활용한 프로젝트가 있었다.
여기에는 별 다른 FSD 관련 Lint가 존재하지 않았기 때문에 딱 알맞는 요소였다.
FSD는 사용했지만.. 린트는 없는... 아주 최적의 조건이었다.
프로젝트에 ESLint 설치
이미 배포해둔 요소기 때문에 다운받아서 사용만 하면 되었다.
해당 프로젝트는 pnpm을 사용하고 있었기 때문에 이를 바탕으로 설치해주었다.

정상적으로 잘 설치된 것을 볼 수 있다.
프로젝트에 ESLint 설정
위와 같이 설정을 추가해주었다.
프로젝트에 ESLint 실행
위와 같이 설정을 해두어서 린트를 실행시켰다.
그 결과는 다음과 같다.

정상적으로 잘 동작하는 것을 확인할 수 있었다.

fix 역시도 잘 동작하는 것을 확인할 수 있었다.
정리
지금까지 ESLint 플러그인을 만들어보았다.
처음에 불편함에서 시작된 일이 생각보다 재미있는 일이 되었다.
스케일이 많이 커지기도 했고.
이를 통해 ESLint가 어떻게 동작하는지, Flat Config가 무엇인지를 보다 자세하면서 명료하게 이해할 수 있었다.
또한, 처음으로 NPM 패키지를 배포하고, Open Source를 만들어보았는데, 이것 또한 새로운 경험이었다.
단순히 배포하고 장땡이 아니라, 관련해서 설정할 것도 있었고, 실제로 배포가 이루어지다보니 버그에 민감해질 수 밖에 없었다.
현재는 규모가 작지만, 규모가 커진다면 테스트도 좀 더 엄격해져야 할 것 같고, 린트 규칙도 좀 더 엄격하게 해야할 것 같다.
설 연휴 동안 기존 항해에 대한 내용 복습 겸, 인턴으로 있는 실무에 필요해질 수 있겠다는 생각이 들어서 만들었는데 잘한 것 같다.
부스트캠프가 끝나고 최근들어서 뭔가 개발 실력이 많이 늘어난 것 같다. 정확히는 도전 정신이랄까?
불편함을 마다하지 않고 해결하려는 노력이 많아진 것 같다.
당분간 이런 상태를 유지하면서 더 많은 것을 배워나가야겠다.
앞으로의 계획
Steiger에서 지원하는 내용들을 조금씩ESLint에 포팅을 해볼까 한다.- 또한,
Prettier도 설정을 커스텀할 수 있던데 이에 대해서도 만들까 싶긴 한데.. 다른 해야할 것도 많아서 조금 더 고민해보고 시도할까 한다.