ESLint 완벽 가이드 | JavaScript Linter·Rules
이 글의 핵심
ESLint로 코드 품질을 관리하는 완벽 가이드. Espree·AST, 규칙 엔진, Autofix, 플러그인 내부, 프로덕션 패턴까지. Rules, Prettier, TypeScript, React 실전 예제.
ESLint는 처음 접했을 때 “빨간 줄”만 늘어나는 스트레스 도구로 보이기 쉽습니다. 하지만 팀 규모가 커질수록 규칙이 없는 코드베이스가 훨씬 더 위험하다는 사실을 체감하게 됩니다. 이 글에서는 ESLint가 실제로 어떤 일을 하는지, 왜 팀에 도입해야 하는지, 그리고 도입 과정에서 흔히 겪는 시행착오와 해결 방향을 정리합니다.
”ESLint 규칙 싸움”이 시작된 순간
한 팀에서 any를 습관적으로 쓰는 사람, ==를 고수하는 사람, console.log를 커밋에 그대로 남기는 사람이 한 코드베이스에 섞여 있는 경우를 자주 봅니다. 누군가 엄격한 규칙 셋을 PR에 올리는 순간 CI가 일제히 빨갛게 변하고, 슬랙에는 “이거 왜 갑자기 에러 나요?”라는 질문이 쏟아집니다. 그러면 eslint-disable을 남발하는 사람도 생기고, 로컬 설정에서만 규칙을 몰래 꺼버리는 사람도 나타납니다.
이런 혼란을 겪고 나서 얻은 결론은 “규칙 자체가 문제가 아니라, 한꺼번에 강하게 적용하려는 방식이 문제”라는 것입니다. 실무적으로는 다음 세 가지 원칙이 효과적입니다. 첫째, 처음부터 전부 error로 설정하지 말고 warn으로 시작해 점진적으로 강도를 높입니다. 둘째, 자동으로 고칠 수 있는 규칙은 --fix에 맡겨 사람이 신경 쓸 부분을 줄입니다. 셋째, 코드 포맷팅은 ESLint가 아니라 Prettier에 위임합니다. Prettier와의 역할 분담은 아래에서 자세히 다룹니다.
ESLint가 하는 일
ESLint는 자바스크립트(또는 TypeScript) 코드를 읽고, 팀이 합의한 규칙에 위반되는 부분을 찾아 표시해 주는 정적 분석 도구입니다. 동작 원리를 단계별로 보면, 먼저 파서(기본값은 Espree)가 소스 코드를 파싱해 AST(추상 구문 트리)를 만듭니다. 이후 각 규칙이 이 트리를 순회하면서 조건에 맞는 노드를 발견하면 context.report를 호출해 진단(diagnostic)을 발생시킵니다. --fix 옵션을 지원하는 규칙은 fixer 객체를 통해 해당 위치의 텍스트를 직접 수정하며, 지원하지 않는 규칙은 개발자가 직접 코드를 고쳐야 합니다.
여기서 중요한 점은 ESLint가 프로그램의 의미를 완벽하게 증명하는 컴파일러가 아니라는 것입니다. ESLint는 정적 힌트를 제공하는 도구에 가깝기 때문에, “이 코드가 왜 문제인지”를 팀의 룰북과 함께 이해하는 것이 훨씬 실용적입니다. 즉, ESLint 경고 자체를 목적으로 삼기보다는, 그 경고가 가리키는 팀의 합의를 이해하는 데 초점을 맞춰야 합니다.
처음 설정하기
ESLint를 프로젝트에 도입하는 가장 빠른 방법은 다음과 같습니다.
npm install -D eslint
npx eslint --init
npx eslint --init을 실행하면 대화형 프롬프트를 통해 모듈 시스템, 프레임워크, TypeScript 사용 여부 등을 물어보고 그에 맞는 초기 설정 파일을 생성해 줍니다. 직접 작성한다면 .eslintrc.json은 대략 다음과 같은 형태가 됩니다.
{
"env": { "browser": true, "es2021": true, "node": true },
"extends": ["eslint:recommended"],
"parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
"rules": {
"no-console": "warn",
"no-unused-vars": "error",
"prefer-const": "error"
}
}
extends에 eslint:recommended를 넣는 것은 거의 표준적인 시작점입니다. extends와 plugins 배열의 값은 모두 JSON 문자열이므로 따옴표를 빠뜨리지 않도록 주의해야 합니다. package.json의 스크립트에는 eslint src와 eslint src --fix 정도만 등록해도 일상적인 개발 루프는 충분히 돌아갑니다.
규칙의 심각도는 문자열("off" | "warn" | "error") 대신 숫자(0, 1, 2)로도 표현할 수 있습니다. 팀이 “이건 에러까지는 아니다”라고 판단한 규칙은 warn으로 설정해 두는 것이 정신 건강에 좋습니다. 경고는 빌드를 막지 않으면서도 코드 리뷰에서 눈에 띄게 남기 때문입니다.
TypeScript, React와 함께 쓰기
TypeScript 프로젝트에서는 @typescript-eslint/parser와 @typescript-eslint/eslint-plugin을 함께 설치해야 하고, React 프로젝트에서는 eslint-plugin-react와 eslint-plugin-react-hooks를 추가하는 것이 일반적입니다. 실무에서는 문서를 참고해 플러그인 이름과 설정 키를 정확히 맞추는 작업이 절반 이상을 차지합니다.
no-floating-promises처럼 타입 정보를 요구하는 엄격한 규칙을 쓰려면 parserOptions.project로 타입스크립트 프로젝트를 ESLint에 연결하는 “type-aware” 린트 모드로 전환해야 합니다. 이 모드는 타입 체커를 함께 구동하기 때문에 속도가 눈에 띄게 느려질 수 있습니다. 전체 파일에 무조건 적용하기보다는 overrides로 대상 범위를 좁히거나, CI에서만 실행되는 별도 스크립트로 분리하는 팀도 많습니다. 속도 저하의 원인을 파악하려면 TIMING=1 환경 변수를 붙여 실행해 어느 규칙이 시간을 많이 쓰는지 먼저 확인하는 것이 좋습니다.
Prettier와 함께 쓰기
여기서부터는 개인적인 의견을 조금 덧붙이겠습니다. 코드 포맷(줄바꿈, 세미콜론, 따옴표 스타일)은 Prettier에 맡기고, ESLint는 버그·코드 스멜·팀이 정한 논리적 제약에 집중하는 역할 분담이 장기적으로 훨씬 덜 피곤합니다. 두 도구가 같은 영역(예: 들여쓰기)을 각자의 방식으로 강제하면 저장할 때마다 서로 충돌하는 경험을 하게 되기 때문입니다.
따라서 eslint-config-prettier로 ESLint 쪽의 포맷 관련 규칙을 비활성화하는 것은 거의 필수적인 설정입니다. 이때 extends 배열의 맨 마지막에 prettier를 넣어야 앞선 설정들의 포맷 규칙을 확실히 덮어쓸 수 있습니다. 반대로 eslint-plugin-prettier를 사용해 “Prettier를 ESLint 규칙 하나로 실행”하는 방식은 실행 속도가 느리고 에러 메시지가 직관적이지 않은 경우가 있어, 개인적으로는 prettier CLI와 eslint를 완전히 분리해서 실행하는 조합을 선호합니다. 에디터에서는 저장 시 두 도구를 각각 실행하도록 설정하면 됩니다.
npm install -D prettier eslint-config-prettier
설정은 extends 배열 끝에 prettier만 추가하면 됩니다(플러그인 이름은 프로젝트 셋업에 맞게 조정합니다).
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
]
}
.prettierrc는 팀의 취향(세미콜론 사용 여부, 인용부호 종류, printWidth 등)만 정하고, 그 이후로는 포맷에 대해 더 이상 논쟁하지 않는 것이 규칙 싸움을 끊는 가장 확실한 방법입니다.
ignore 설정, 에디터 통합, pre-commit 훅
node_modules/, dist/, coverage/처럼 린트할 필요가 없는 디렉터리는 .eslintignore(또는 최신 flat config의 ignores 필드)로 제외해야 합니다. VS Code에서는 저장 시 source.fixAll.eslint를 활성화해 두면 자동으로 고칠 수 있는 문제들이 저장과 동시에 정리됩니다. husky와 lint-staged를 조합해 커밋 직전에 변경된 파일만 eslint --fix와 prettier --write를 실행하는 패턴도 널리 쓰입니다.
이렇게 로컬에서는 변경된 파일만 가볍게 검사하고, 전체 코드베이스에 대한 린트는 CI에서 수행하도록 나누면 개발자 체감 속도와 코드 품질을 동시에 확보할 수 있습니다. CI에서는 eslint . --max-warnings 0으로 경고까지 실패 조건으로 삼을지, 아니면 warn은 통과시키고 error만 막을지를 팀 정책으로 명확히 정해 두는 것이 좋습니다.
규칙 엔진, AST, Autofix 내부 동작
조금 더 깊이 들어가 보면, ESLint 규칙은 create(context) 함수가 반환하는 visitor 객체로 구성됩니다. ESLint는 AST를 한 번 순회하면서 각 노드 타입에 맞는 visitor 함수를 호출하고, 그 안에서 조건을 검사해 문제를 발견하면 context.report를 실행합니다. fix 함수는 AST를 직접 재작성하는 것이 아니라, 소스 코드의 특정 범위(range)를 문자열 패치로 교체하는 방식으로 동작합니다. 이 차이를 이해하면 왜 일부 Autofix가 “완벽하지 않은” 결과를 낼 수 있는지도 짐작할 수 있습니다.
Autofix가 일부 문제만 고치고 나머지를 남기는 경우는, 여러 규칙의 수정 범위가 겹쳐서 충돌이 발생했거나, 해당 규칙 자체가 fix를 구현하지 않은 경우입니다. 이런 상황에서 eslint-disable-next-line으로 예외를 남길 때는 단순히 규칙만 끄지 말고, 이유와 관련 이슈 트래커 번호를 주석으로 함께 남기는 것이 좋습니다. 나중에 코드베이스에서 해당 예외를 검색(grep)할 때 맥락을 훨씬 빠르게 파악할 수 있기 때문입니다.
커스텀 규칙이나 플러그인을 직접 작성하는 단계까지 가게 된다면, package.json의 peerDependencies에 지원하는 ESLint 버전 범위를 정확히 명시해야 합니다. TypeScript 기반 규칙을 작성할 때는 @typescript-eslint/utils가 AST 노드 타입과 유틸리티 함수를 제공해 개발 경험을 크게 개선해 줍니다.
마무리
ESLint는 팀이 “이건 지양하자”고 합의한 내용을 코드 차원에서 강제하는 도구이고, Prettier는 코드의 “겉모습”을 통일하는 도구입니다. 이 둘을 올바르게 조합하면 리뷰에서 발생하는 빨간 줄이 줄어들고, “스타일 때문에 리뷰가 30분씩 걸리는” 상황도 크게 줄어듭니다.
물론 react-hooks/exhaustive-deps 규칙과 useEffect의 의존성 배열 때문에 골머리를 앓는 순간은 여전히 존재합니다. 그럴 때는 규칙을 바로 끄기보다는 먼저 의존성 배열을 실제로 맞춰보는 시도를 해보고, 정말 예외적인 상황이라면 이유를 주석으로 남기고 넘어가는 것을 권장합니다. 함께 읽으면 도움이 될 글로는 Prettier 완벽 가이드, Babel 완벽 가이드, Bun 완벽 가이드가 있습니다. 검색 키워드로는 ESLint, Espree, AST, Linter, TypeScript, Prettier, React 정도를 함께 기억해 두면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Prettier 완벽 가이드 | 코드 포맷터·ESLint 통합·설정·실전 활용
- Babel 완벽 가이드 | 파서·AST·파이프라인·트랜스파일러·Presets·Plugins·실전
- Bun 완전 가이드 | Node.js를 뛰어넘는 초고속 JavaScript 런타임