ESLint v9.19.0 버전을 기준으로 작성되었습니다.
Intro
프로젝트 개발 환경을 구성할 때 무엇을 먼저 하시나요? 저는 먼저 코어 라이브러리(React, Vite 등)를 설치한 후 ESLint나 Prettier와 같은 도구를 설치합니다. 그중 ESLint는 린트 도구로 런타임 이전에 잠재적인 문제를 발견해 프로그램의 완성도를 높일 수 있습니다.
하지만 한 번 설정하고 나면 ESLint 설정은 잘 바꾸지 않습니다. 거기다가 보통 이전 프로젝트에서 사용했던 config 파일을 그냥 다시 사용해 설정 방법을 더 쉽게 까먹곤 합니다.
그 근본적인 원인은 ESLint에 대해서 제대로 알고 있지 않은 상태로 사용했기 때문이라고 생각했습니다. 그래서 ESLint가 무엇인지, 어떻게 동작하는지 살펴보고 이해해 보려고 합니다.
Lint
ESLint를 살펴보기 전에 먼저 린트(lint) 가 무엇인지 알아볼까요?
원래 린트(lint)는 옷에 일어나는 보풀을 말합니다. 보풀이 일어난 옷을 못 입는 것은 아니지만, 보기에 좋지 않고 시간이 지나면 옷이 상하는 원인이 됩니다. 이처럼 코드에서도 당장 프로그램이 동작하는 데 문제는 없지만 보풀처럼 프로그램의 품질을 떨어뜨리는 코드가 있습니다. 개발에서 린트(lint) 혹은 린터(linter)는 소스 코드를 분석하여 코드의 오류나 버그, 스타일 오류 등을 표시하는 정적 코드 분석 도구를 말합니다. 즉, 코드에 일어난 보풀을 미리 제거할 수 있도록 돕는 도구인 셈이죠.
* 개발에서 lint라는 표현은 Stephen C. Johnson이 처음 사용하였습니다.
린트를 사용하여 보풀 같은 코드를 사전에 점검하고 수정하여 코드의 품질을 높일 수 있습니다. 추가적으로 여러 개발자가 함께 개발할 때 정해진 코드 규칙과 코드 스타일을 지키도록 도와 코드의 가독성을 높일 수도 있습니다.
ESLint란?
ESLint는 현대 Javascript 진영에서 가장 많이 사용하는 린트 도구로, ECMAScript/JavaScript 코드를 정적으로 분석하여 잠재적 런타임 오류, best practice를 따르지 않는 경우 등에 대한 문제를 찾고 더 나은 코드로 정정하는 것을 돕는 도구입니다. (ESLint의 공식 문서)
What is ESLint?
ESLint is a configurable JavaScript linter. It helps you find and fix problems in your JavaScript code. Problems can be anything from potential runtime bugs, to not following best practices, to styling issues.
출처: ESLint Docs - Core Concepts
즉, 코드를 실행하지 않고도 다양한 문제를 파악하고 Best Practice를 따르도록 도와주는 도구입니다.
- Linter와 Code Formatter
ESLint는 팀원간의 코딩 컨벤션을 맞추기 위해서 코드 포매터인 Prettier와 함께 사용하는 것이 좋습니다. 이전에는 ESLint가 포맷팅 기능을 포함하고 있었지만 8.53 버전 이후 해당 기능을 공식적으로 deprecate 하였고, 현재는 코드 포매터와 함께 사용하는 것을 권장합니다.
ESLint에서는 더이상 코드 포맷팅 규칙을 지원하지 않습니다.
ESLint는 8.53 마이너 버전에서 포맷팅 규칙을 공식적으로 deprecate 하였으며, 코드 포맷팅을 위해 ESLint 대신 소스 코드 포매터 사용을 권장합니다. 또한, 소스 코드 포맷팅을 위한 라이브러리를 추가로 설치하고 싶지 않다면 @stylistic/eslint-plugin-js 혹은 @stylistic/eslint-plugin-ts를 사용하는 것을 권장합니다. 자세한 내용은 Deprecation of formatting rules 혹은 (번역)포맷팅 규칙이 ESLint에서 사라집니다 by @Saetbyeol 글을 참고하세요.
ESLint 구성요소
그렇다면 ESLint는 어떻게 구성되어 있을까요?
Rule
Rule은 ESLint의 가장 핵심 구성요소로 특정 조건을 충족하는지, 충족하지 못할 경우 어떻게 해야 하는지 검증하는 규칙이며, 각 규칙에 대해 다음과 같은 옵션을 설정할 수 있습니다.
"off"
or0
- 규칙을 사용하지 않음"warn"
or1
- rule을 경고로 설정(규칙을 위반해도 exit code 발생하지 않음)."error"
or2
- rule을 오류로 설정 (규칙 위반 시 exit code 1 발생).
ESLint는 기본적으로 수백 개의 규칙을 제공하고 있으며, 사용자 지정 규칙을 만들거나 다른 사람이 만든 플러그인으로 만든 규칙을 사용할 수도 있습니다. 각각의 rule은 fix 혹은 suggestion 두 가지 형태가 있습니다.
Rule fix
- 발견된 규칙 위반에 대해서 수정 가능 (어플리케이션 로직은 변경하지 않아 안전)
- 커맨드 라인 옵션에
--fix
를 추가 / 코드 에디터의 확장 프로그램으로 수정 사항 반영
Rule Suggestion
- fix와 함께 혹은 fix 대신 제안 사항을 제공할 수 있다.
- 어플리케이션 로직을 변경할 수 있어 자동으로 적용 불가능 / CLI로 적용 불가 / 코드 에디터 확장 프로그램을 통해서만 사용 가능
Configuration file
Configuration file은 프로젝트에서 ESLint에 대한 구성을 저장하는 파일로, 기본 제공 rule으로 어떻게 코드를 검사할지 구성된 옵션, 사용자 지정 규칙이 있는 플러그인, sharable configuration 등을 포함하고 있습니다.
- configuration file은 다음과 같은 이름을 사용합니다.
- JS:
eslint.config.js
,eslint.config.mjs
,eslint.config.cjs
- TS:
eslint.config.ts
,eslint.config.mts
,eslint.config.cts
- JS:
- eslint 9버전부터는 기존에 사용하던 구성 형식을 버리고 flat config 방식을 사용하여 configuration을 구성합니다.
Sharable configuration
Sharable configuration은 npm을 통해 공유되는 ESLint configuration을 sharable configuration이라고 하며, 대표적으로 에어비앤비의 JavaScript 스타일 가이드인 eslint-config-airbnb
가 있습니다.
Plugins
Plugin은 ESLint Rule, Configuration, processor, environment에 대한 정의를 포함하는 npm 모듈을 말합니다. 사용자 정의 rule을 포함하며, 스타일 가이드를 적용하고 JavaScript extension(TypeScript 등), 라이브러리(React 등), frameworks(Angular 등)을 지원하는 데 사용할 수 있습니다. 가장 대표적인 예시는 React 관련 Eslint 설정을 제공하는 eslint-plugin-react
가 있습니다.
Parsers
ESLint는 코드를 AST(abstract syntax tree)로 변환하여 평가합니다. 이때, parser는 코드를 AST로 변환하는 역할을 합니다. ESLint는 기본적으로 Espree를 parser로 사용합니다.
커스텀 parser를 사용하면 표준이 아닌 Javascript 구문을 분석할 수 있습니다. 커스텀 parser의 경우 sharable config 혹은 plugin의 일부로 포함되어 직접 사용할 필요는 없습니다.
현대 웹 개발에서 많이 사용하는 타입스크립트는 @typescript-eslint/parser
를 사용하여 ESLint가 TypeScript 코드를 파싱할 수 있도록 합니다.
etc
그 외에도 ESLint는 다음과 같은 요소들로 구성되어 있습니다.
- Custom Processors
- 프로세서를 사용하여 다른 종류의 파일에서 자바스크립트 코드를 추출하여 린트 처리할 수 있음. 또는 프로세서를 사용하여 ESLint 구문 분석 전 자바스크립트 코드를 조작할 수 있음
eslint-plugin-markdown
에는 마크다운 코드 블록 내에서 자바스크립트 코드를 린트할 수 있는 커스텀 프로세서가 포함되어 있음.
- Formatters
- 포맷터는 CLI에서 린팅 결과의 모양을 어떻게 표시할지 제어함.
- Integrations
- ESLint 통합 에코시스템 제공.
- 자세한 내용은 공식 문서 참조
- CLI
- 터미널에서 린팅을 실행할 수 있는 인터페이스
- Node.js API
- Node.js 코드에서 프로그래밍 방식으로 ESLint 사용.
- 플러그인, integration 등 ESLint 관련 도구 개발 시에만 사용
ESLint 동작 원리
ESLint의 GitHub에서는 ESLint에 대해서 아래와 같이 설명합니다.
- ESLint uses Espree for JavaScript parsing.
- ESLint uses an AST to evaluate patterns in code.
- ESLint is completely pluggable, every single rule is a plugin and you can add more at runtime.
ESLint는 Espree(혹은 다른 Parser)를 사용해서 코드를 파싱하여 AST를 생성하고 코드 패턴을 평가하고, 그 후 AST를 플러그인 형태의 룰을 기반으로 검사를 진행하여, 해당하는 결과를 출력하거나 코드를 수정하는 방식으로 동작한다는 것이죠.
동작 방식을 간단하게 나타내보면 다음과 같이 표현할 수 있습니다.
다음과 같은 간단한 코드에 대해서 no-var
, no-console
룰을 적용하여 ESLint가 동작하는 방식을 살펴보겠습니다.
var x = 10;
console.log(x);
1. 파일 읽기 및 파싱(Parsing)
먼저 ESLint는 코드 파일을 문자열 형태로 읽은 후, 이를 이해하기 위해 Parser를 사용합니다. 기본적으로 Espree(ESLint의 기본 JavaScript 파서)를 사용하지만, 다른 파서(대표적으로 타입스크립트를 사용할 경우 @typescript-eslint/parser)도 설정할 수 있습니다. 파싱 과정에서 코드를 토큰(Token) 단위로 쪼개고, 이를 기반으로 AST (Abstract Syntax Tree, 추상 구문 트리) 를 생성합니다. AST Explorer에서 여러 Parser를 사용하여 코드가 파싱된 결과를 확인할 수 있습니다.
2. AST(Abstract Syntax Tree) 생성
파싱 결과로 코드의 구조를 트리 형태로 표현한 AST가 생성되었습니다. 위에서 작성한 코드를 AST로 변환하면 다음과 같습니다. 실제 파싱 결과는 json으로 되어있지만 쉬운 이해를 위해 json의 type
과 name
, value
등 의 프로퍼티를 기반으로 다음과 같이 표현하였습니다.
Program
├── VariableDeclaration
│ └── VariableDeclarator
│ ├── Identifier (x)
│ └── Literal (10)
└────── ExpressionStatement
└── CallExpression
├── MemberExpression
│ └── Identifier (console)
│ └── Identifier (log)
└── Identifier (x)
실제 Espree 결과 형태
{
"type": "Program",
"start": 0,
"end": 27,
"range": [0, 27],
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 11,
"range": [0, 11],
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 10,
"range": [4, 10],
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"range": [4, 5],
"name": "x"
},
"init": {
"type": "Literal",
"start": 8,
"end": 10,
"range": [8, 10],
"value": 10,
"raw": "10"
}
}
],
"kind": "var"
},
{
"type": "ExpressionStatement",
"start": 12,
"end": 27,
"range": [12, 27],
"expression": {
"type": "CallExpression",
"start": 12,
"end": 26,
"range": [12, 26],
"callee": {
"type": "MemberExpression",
"start": 12,
"end": 23,
"range": [12, 23],
"object": {
"type": "Identifier",
"start": 12,
"end": 19,
"range": [12, 19],
"name": "console"
},
"property": {
"type": "Identifier",
"start": 20,
"end": 23,
"range": [20, 23],
"name": "log"
},
"computed": false,
"optional": false
},
"arguments": [
{
"type": "Identifier",
"start": 24,
"end": 25,
"range": [24, 25],
"name": "x"
}
],
"optional": false
}
}
],
"sourceType": "module"
}
3. Rule 적용 및 검사 (Linting)
이렇게 Parser를 통해 AST가 생성되면, ESLint는 정의된 룰(rule) 을 기반으로 코드를 검사합니다.
ESLint의 룰은 기본적으로 트리 탐색 방식 으로 적용됩니다.
예를 들어, 예시 코드의 첫 번째 줄은 다음과 같은 방식으로 탐색하게 됩니다. 노드 탐색 시작 시 enter, 종료 시 exit을 사용하며 기본적으로는 enter를 사용하고 exit이 필요할 경우 명시적으로 작성합니다. (모든 단계에서 enter와 exit이 가능합니다.)
Program:enter
VariableDeclaration:enter
Identifier
Literal
VariableDeclaration:exit
Program:exit
위와 같은 방식으로 AST를 탐색하면서 특정 노드(Node) 에 적용될 수 있는 룰을 확인합니다. 예를 들어, no-var룰의 경우 "VariableDeclaration:exit"
로 정의되어 있으므로 변수 선언 노드 탐색이 끝날 때 var
으로 선언한 변수가 있는지 체크합니다.
위에서 언급했던 두 가지 rule의 코드를 간략하게 살펴보면 다음과 같습니다.
- no-var rule 코드
- VariableDeclaration(변수 선언) 단계가 끝날 때, 노드의 종류가 var일 경우 report를 합니다.
module.exports = {
create(context) {
// ...
function report(node) {
context.report({
node,
messageId: 'unexpectedVar',
fix(fixer) {
const varToken = sourceCode.getFirstToken(node, { filter: (t) => t.value === 'var' });
return canFix(node) ? fixer.replaceText(varToken, 'let') : null;
},
});
}
return {
'VariableDeclaration:exit'(node) {
if (node.kind === 'var') {
report(node);
}
},
};
},
};
- no-console rule 코드
- Program이 끝날 때(전체 노드의 분석이 끝날 때), 스코프 내의 console 변수를 찾고, 변수가 shadowing되지 않은 경우를 찾아 검사하고 allow되지 않은 console 메서드들을 report합니다.
module.exports = {
create(context) {
// ...
function report(reference) {
const node = reference.identifier.parent;
const propertyName = astUtils.getStaticPropertyName(node);
context.report({
node,
loc: node.loc,
messageId: allowed.length ? 'limited' : 'unexpected',
data: { allowed: allowed.join(', ') },
suggest: canProvideSuggestions(node)
? [
{
messageId: 'removeConsole',
data: { propertyName },
fix(fixer) {
return fixer.remove(node.parent.parent);
},
},
]
: [],
});
}
return {
'Program:exit'(node) {
const scope = sourceCode.getScope(node);
const consoleVar = astUtils.getVariableByName(scope, 'console');
const shadowed = consoleVar && consoleVar.defs.length > 0;
const references = consoleVar ? consoleVar.references : scope.through.filter(isConsole);
if (!shadowed) {
references.filter(isMemberAccessExceptAllowed).forEach(report);
}
},
};
},
};
예시 코드를 기준으로 no-var와 no-console 룰의 동작 방식을 살펴봅시다.
AST를 탐색하며 var x = 10;
의 변수 선언 노드를 빠져나올 때, var
으로 선언된 변수 선언을 감지하고 context.report()
를 호출하여 오류를 기록합니다. 또한 프로그램 분석이 종료될 때, 코드 전체에서 사용한 console
을 찾고, allow되지 않은 method는 context.report()
를 호출하여 오류를 기록합니다.
4. 결과 출력 및 수정 (Reporting & Fixing)
룰을 적용하면서 report 메서드를 통해 기록한 오류 목록을 출력합니다.
예시 코드에서는 2개의 오류가 출력될 것 입니다.
- line 1에서 no-var 룰에 의해 기록된
var
을 사용하여 선언한 변수 - line 2에서 no-console 룰에 의해 기록된
console.log
이때, suggestion 혹은 fix 옵션을 통해서 수정 방법을 제안해 줍니다. 또한, 만약 —fix 옵션을 사용하면 자동으로 수정이 가능한 코드 스타일 문제들을 수정할 수도 있습니다.
no-var
의 경우, var를 let으로 바꾸어도 코드의 동작에 영향을 주지 않으므로 자동 fix가 가능합니다. 반면에, no-console
의 경우는 console과 관련된 출력 코드가 사라지게 되므로 코드의 동작이 달라져 자동 fix는 불가능하며 suggestion으로 수정 방법에 대한 제안이 나타날 것입니다.
자동 수정이 가능한 룰은 AST를 수정한 후 다시 코드로 변환하는 방식으로 동작합니다. report된 오류들에서 fix가능한 룰은 다음과 같이 수정됩니다.
- AST에서 report된 특정 노드를 변경하거나 제거
- 변형된 AST를 다시 코드 문자열로 변환 (코드 포매터 사용 가능)
- 변경된 코드를 파일에 덮어씀
위에서 살펴본 no-var 룰의 코드를 보면, 해당 노드의 소스코드에서 var
를 let
으로 replace
하는 형태인 것을 확인할 수 있습니다.
정리
정리하면 ESLint는 다음과 같은 단계로 코드를 검사하고 오류를 기록합니다.
- 코드 파싱 → 문자열을 AST로 변환
- AST 생성 → 코드 구조를 트리 형태로 표현
- Rule 적용 → AST를 순회하며 문제 탐지
- 결과 출력 및 수정 → 오류 리포팅 및 자동 수정
마무리
ESLint가 무엇인지, 그리고 또 어떤 요소들로 구성되어 있는지, 어떤 방식으로 동작하는지 살펴보았습니다. 실제로 eslint가 어떤 방식으로 규칙을 적용하는지 이해하고 어떤 규칙은 자동 수정이 가능하지만 어떤 규칙은 불가능한 이유를 알 수 있었습니다.
이후에는 ESLint가 9버전으로 올라오면서 바뀐 config 구성방식인 flat conifg에 대해서 알아보고, 현재 이 블로그의 eslint config를 실제로 구성해보고자 합니다.