ESLint 커뮤니티와 써드파티 라이브러리에 기여하면서도 ESLint가 정확히 어떻게 자바스크립트를 읽어내려가는지 제대로 알고 있지 않았다.
블로그 글을 쓰는 이 기회에 ESLint 멘토님께 배운 내용을 제대로 짚고 가려고 한다.
const hi = 42;
debugger;
어떤 .js파일에 위와 같은 코드가 있다.
ESLint는 이 파일을 const hi = 42;\n\ndebugger; 문자열로 받는다. 하지만 그 문자열을 그대로 검사하지는 않는다. 문자열로 판단할 수 있는게 거의 없기 때문이다.
// const는 예약어이다.
const message = "const로 선언하기";
위의 코드에서 const가 세번 등장한다. 하지만 실제 변수로 선언한건 하나뿐이다.
이 셋을 구분하려면 다른 형태가 필요하다.
해석할 "다른 형태"를 만드는 일은 ESLint가 하지 않는다. 이 일은 파서에 맡긴다.
ESLint의 package.json을 열어보면 espree가 의존성으로 들어 있다.
"dependencies": {
"espree": "^11.2.0",
...
}
ESLint의 기본 파서는 espree지만, 설정을 통해 다른 파서로 교체할 수 있다.

ESLint — Core Concepts: Parsers
교체할 수 있다는 건 애초에 본체 밖에 있다는 뜻이다.
파서를 밖에 둔 덕분에 새 문법이 나오거나 타입스크립트처럼 다른 문법을 쓸 때도 ESLint 본체는 손대지 않아도 된다.
ESLint는 직접 자바스크립트를 해석(파싱)하지 않고, 누군가 해석해 놓은 구조에서 패턴을 찾아 보고한다.
espree는 자바스크립트 문자열을 의미 단위로 자른다(lexing).

사진처럼 총 7개의 토큰이 생성된다. 공백과 줄바꿈(\n)은 토큰이 되지 않는다. 경계 역할만 한다.
토큰 하나하나는 type, value, start, end 같은 속성을 가진 객체다. 이 객체들이 배열에 순서대로 담긴다.
자른 토큰들은 그 다음 단계에서 서로 엮인다(parsing). 여기서 AST(Abstract Syntax Tree)가 만들어진다.

espree는 노드 이름과 형태를 임의로 정하지 않고 ESTree 명세를 따라 트리를 만든다.
// es5.md
interface VariableDeclaration <: Declaration {
type: "VariableDeclaration";
declarations: [ VariableDeclarator ];
kind: "var"; // es2015.md에서 "var" | "let" | "const" 로 확장
}
ESLint는 이 트리를 훑기 위해 visitor-keys에 노드 타입마다 따라갈 키를 적어두었다.
최상위 노드인 Program부터 그 키를 따라 자식 노드로 내려간다. 키가 빈 배열이면 더 내려갈 곳이 없다. 트리의 끝이다.
토큰과 AST 형태는 ESLint Code Explorer에서 직접 확인해 볼 수 있다.
AST를 만들 때 모든 토큰이 노드가 되는 것은 아니다. 7개 토큰 중 노드로 남는 건 hi, 42, debugger 셋뿐이다.
변수 선언 시 사용한 const는 kind에 문자열로 들어간다. =는 id와 init을 나누는 기준이 되었고 ;는 문장이 끝나는 지점이 되었다. Abstract(추상적인)의 의미가 여기서 드러난다. 표기를 버리고 의미만 남긴다.
앞에서 문자열로는 구분할 수 없던 const 세 개도 트리에서는 다른 의미를 갖고 각자의 자리에 놓이게 된다.
앞에서 espree가 문자열을 어떻게 AST로 만드는지 확인해보았다. 이제 만들어진 AST를 기준으로 ESLint는 깊이 우선 탐색으로 노드를 하나씩 탐색한다.

ESLint는 만들어진 AST 노드를 두 번 지나간다. 들어갈 때(Enter) 한 번, 자식을 다 보고 나올 때(Exit) 한 번이다.
Identifier처럼 자식이 없는 노드는 4번에 들어갔다 5번에 바로 나오고, VariableDeclaration처럼 자식이 많으면 2번에 들어가 9번에야 나온다.
ESLint 룰은 이 순회에 직접 참여하지 않는다. "이 타입의 노드를 만나면 이 함수를 불러달라"고 등록해둘 뿐이다.
룰이 각자 트리를 뒤졌다면 룰 개수만큼 순회가 반복됐겠지만, 순회를 ESLint가 맡으면서 룰이 몇 개든 한 바퀴로 끝난다.
// ESLint rule
module.exports = {
// meta: { ... }
create(context) {
return {
DebuggerStatement(node) {
// DebuggerStatement를 만나면 실행
},
};
},
};
반환하는 객체의 키가 노드 타입 이름이다. 위 그림의 10번, DebuggerStatement에 들어가는 순간 이 함수가 실행된다.
위치 정보는 문자열을 자를 때부터 기록된다. 토큰마다 start, end, loc이 붙는다. 토큰들을 엮어 만든 노드는 원본 문자열에서 자기가 차지하는 구간을 좌표로 갖는다.
ESLint는 문제가 된 노드의 loc을 읽어 에러 위치를 표시한다.

멘토님이 담백하고 쉽게 알려주신 덕분에 직접 그려가며 즐겁게 복기하는 시간을 가졌다. 세 가지로 정리된다.
자바스크립트를 해석한 건 espree고, ESLint는 그 결과물의 형식만 알면 됐다.
그래서 파서를 @typescript-eslint/parser로 바꾸면 코어를 손대지 않고도 타입스크립트가 린트된다.
espree, ESTree — es5.md, ESTree — es2015.md
eslint-visitor-keys, no-debugger, ESLint Code Explorer
그럼 ESLint 라이브러리 안에는 기본적으로 espree가 포함되서 사용하게끔 구성이 되어있는건가요?