ESLint는 자바스크립트를 모른다

이가 은·2026년 7월 29일

frontend

목록 보기
3/3
post-thumbnail

코드가 데이터가 되기까지

ESLint 커뮤니티와 써드파티 라이브러리에 기여하면서도 ESLint가 정확히 어떻게 자바스크립트를 읽어내려가는지 제대로 알고 있지 않았다.
블로그 글을 쓰는 이 기회에 ESLint 멘토님께 배운 내용을 제대로 짚고 가려고 한다.

문자열로는 아무것도 판단할 수 없다

const hi = 42;

debugger;

어떤 .js파일에 위와 같은 코드가 있다.

ESLint는 이 파일을 const hi = 42;\n\ndebugger; 문자열로 받는다. 하지만 그 문자열을 그대로 검사하지는 않는다. 문자열로 판단할 수 있는게 거의 없기 때문이다.

// const는 예약어이다.
const message = "const로 선언하기";

위의 코드에서 const가 세번 등장한다. 하지만 실제 변수로 선언한건 하나뿐이다.
이 셋을 구분하려면 다른 형태가 필요하다.

코드를 해석하는 건 ESLint가 아니다

해석할 "다른 형태"를 만드는 일은 ESLint가 하지 않는다. 이 일은 파서에 맡긴다.

ESLint의 package.json을 열어보면 espree가 의존성으로 들어 있다.

"dependencies": {
  "espree": "^11.2.0",
  ...
}

ESLint의 기본 파서는 espree지만, 설정을 통해 다른 파서로 교체할 수 있다.

ESLint 공식 문서의 Parsers 항목 — 기본 파서는 espree이며 설정으로 다른 파서를 지정할 수 있다고 설명한다

ESLint — Core Concepts: Parsers

교체할 수 있다는 건 애초에 본체 밖에 있다는 뜻이다.
파서를 밖에 둔 덕분에 새 문법이 나오거나 타입스크립트처럼 다른 문법을 쓸 때도 ESLint 본체는 손대지 않아도 된다.

ESLint는 직접 자바스크립트를 해석(파싱)하지 않고, 누군가 해석해 놓은 구조에서 패턴을 찾아 보고한다.

토큰에 있던 const가 트리에는 없다

espree는 자바스크립트 문자열을 의미 단위로 자른다(lexing).

const hi = 42; debugger; 가 7개의 토큰으로 쪼개진 모습

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

자른 토큰들은 그 다음 단계에서 서로 엮인다(parsing). 여기서 AST(Abstract Syntax Tree)가 만들어진다.

Program을 루트로 VariableDeclaration과 DebuggerStatement가 뻗어나가고,VariableDeclarator 아래 Identifier와 Literal이 달린 AST 트리

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 셋뿐이다.
변수 선언 시 사용한 constkind에 문자열로 들어간다. =idinit을 나누는 기준이 되었고 ;는 문장이 끝나는 지점이 되었다. Abstract(추상적인)의 의미가 여기서 드러난다. 표기를 버리고 의미만 남긴다.

앞에서 문자열로는 구분할 수 없던 const 세 개도 트리에서는 다른 의미를 갖고 각자의 자리에 놓이게 된다.


ESLint가 그 데이터를 쓰는 방식

ESLint는 트리를 한 번만 훑는다

앞에서 espree가 문자열을 어떻게 AST로 만드는지 확인해보았다. 이제 만들어진 AST를 기준으로 ESLint는 깊이 우선 탐색으로 노드를 하나씩 탐색한다.

AST 노드 순회 그림

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을 읽어 에러 위치를 표시한다.

no-debugger 오류 위치를 알려주는 이미지

마치며

멘토님이 담백하고 쉽게 알려주신 덕분에 직접 그려가며 즐겁게 복기하는 시간을 가졌다. 세 가지로 정리된다.

  • ESLint가 파싱을 espree에 맡기고 있다는 것
  • 룰이 노드 타입에 등록돼 순회 중에 호출되는 방식
  • 노드의 좌표로 에러 위치가 표시되는 과정

자바스크립트를 해석한 건 espree고, ESLint는 그 결과물의 형식만 알면 됐다.
그래서 파서를 @typescript-eslint/parser로 바꾸면 코어를 손대지 않고도 타입스크립트가 린트된다.

참고

espree, ESTree — es5.md, ESTree — es2015.md
eslint-visitor-keys, no-debugger, ESLint Code Explorer

profile
독서와 개발을 좋아하는 프론트 개발자 🍀

2개의 댓글

comment-user-thumbnail
2026년 7월 30일

그럼 ESLint 라이브러리 안에는 기본적으로 espree가 포함되서 사용하게끔 구성이 되어있는건가요?

1개의 답글