ESLint Flat Config 마이그레이션 실패 일지와 살아남는 체크리스트
ESLint 9의 flat config로 넘어가면서 extends가 사라지고, 플러그인 호환성 문제, VS Code ESLint 확장과의 설정 불일치, 글로벌 변수 선언 방식 변화 등 현장에서 마주치는 장애물을 해결 순서대로 정리한다. 삽질을 줄이는 실전 체크리스트. 2025년 4월, ESLint 9가 정식 릴리스되면서 파일은 deprecated 경고를 넘어 아예 무시되기 시작했다.
DevInsight에서 발행해요
2025년 4월, ESLint 9가 정식 릴리스되면서 .eslintrc 파일은 deprecated 경고를 넘어 아예 무시되기 시작했다. 마이그레이션 가이드를 읽고 npx @eslint/migrate-config .eslintrc.cjs를 돌렸다. 결과물은 겉보기에 그럴듯했다. 터미널에서 npx eslint .을 치자 통과. 문제는 VS Code에서부터 시작됐다.
빨간 줄이 전혀 뜨지 않았다.
eslintrc가 죽었다고 extends가 사라진 건 아니다
flat config에서 가장 먼저 혼란을 주는 지점은 extends 키워드의 소멸이다. 기존 설정이 이랬다면:
// .eslintrc.cjs module.exports = { extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], };
마이그레이션 도구는 이걸 eslint.config.mjs로 풀어내는데, extends 체인이 사라지면서 발생하는 부작용이 하나 있다. extends는 단순히 규칙을 복사하는 게 아니라, 플러그인이 제공하는 configs 객체를 참조해 프로세서, 언어 옵션, 파서까지 함께 주입한다. flat config에서 이걸 직접 ...tseslint.configs.recommended처럼 스프레드해야 하는데, 여기서 languageOptions.parser가 누락되는 경우가 흔하다.
마이그레이션 도구가 생성한 설정을 보면 @typescript-eslint 플러그인을 객체 임포트로 바꾸면서 파서 참조를 plugin.configs.recommended 안에 묻어두는데, 이게 다른 플러그인과 충돌하면 파서가 날아간다. 실제로 eslint-plugin-react와 조합했을 때 VS Code에서 JSX 파싱이 깨지는 현상을 겪었다. languageOptions.parserOptions.ecmaFeatures.jsx가 true로 설정됐음에도 파서가 espree로 롤백된 탓이다.
해결은 명시적이다. tseslint.configs.recommended를 스프레드한 뒤에도 languageOptions.parser를 직접 한 번 더 지정한다. 중복이 아니라 보험이다. flat config는 마지막에 적용된 값이 이긴다.
플러그인은 plugins에 넣었는데 왜 규칙은 못 찾을까
flat config 이전에는 plugins: ['react']라고 선언한 뒤 rules: { 'react/jsx-boolean-value': 'error' }라고 쓰면 규칙을 찾았다. 플러그인이 글로벌 네임스페이스에 등록됐기 때문이다. flat config에서는 플러그인을 직접 임포트해서 plugins: { react: reactPlugin }처럼 객체로 넘겨야 하는데, 공식 문서의 코드 샘플을 그대로 베꼈는데도 규칙을 못 찾는 상황이 생긴다.
범인은 대부분 eslint-plugin-react 같은 전통적인 플러그인을 flat config 호환 래퍼 없이 그냥 가져오는 경우다. import reactPlugin from 'eslint-plugin-react'로 가져온 객체에 configs 프로퍼티가 없다면, 해당 플러그인의 flat/recommended 같은 flat config 전용 엔트리포인트를 대신 써야 한다. eslint-plugin-react는 eslint-plugin-react/configs 경로로 flat config를 제공 중이고, @typescript-eslint는 typescript-eslint 패키지의 config() 헬퍼를 권장한다.
문제는 이 사실을 모르고 plugins에 넣기만 하면 규칙 네임스페이스는 살아있지만 configs 체인이 끊긴 상태가 된다는 점이다. CLI에서는 조용히 통과하지만 VS Code ESLint 확장(2.x)은 이 불일치를 잡아내지 못한다. 파일을 열 때마다 "규칙을 찾을 수 없음"이라는 경고를 표시하거나, 더 나쁜 경우 모든 규칙을 무시하고 아무 오류도 표시하지 않는다.
검증 방법은 간단하다. npx eslint --print-config src/index.ts를 실행해서 최종적으로 병합된 설정을 덤프해본다. rules 섹션에 react/jsx-boolean-value가 없으면 플러그인 연동이 실패한 거다.
VS Code ESLint 확장이 말썽인 진짜 이유
마이그레이션 후 터미널에서는 잘 되는데 에디터에서만 조용해지는 패턴. 이건 설정 문제가 아니라 ESLint 확장의 내부 동작 방식 차이다.
VS Code ESLint 확장 2.x는 작업 디렉터리에서 ESLint 인스턴스를 로드하는데, eslint.config.mjs가 있으면 자동으로 flat config 모드로 전환한다. 여기서 함정은 확장이 사용하는 eslint 버전과 프로젝트의 로컬 eslint 버전이 다를 때 발생한다. 확장이 번들링한 ESLint 8.x를 쓰면서 flat config를 인식하지 못하는 경우, 아무 경고 없이 린트가 비활성화된다.
확인 순서는 이렇다. 먼저 cmd+shift+p → "ESLint: Show Output Channel"을 열어서 ESLint 서버 로그를 본다. "Flat config is not supported with ESLint v8.x" 같은 메시지가 있으면 확장이 오래된 ESLint를 사용 중이다. VS Code 설정에서 eslint.useFlatConfig를 true로 강제하고, eslint.nodePath를 프로젝트의 node_modules로 지정한다. 그래도 안 되면 VS Code를 워크스페이스 루트에서 재시작한다. ESLint 서버가 캐싱한 이전 설정을 물고 있으면 설정 파일을 바꿔도 반영되지 않는다.
또 하나. .eslintignore가 flat config에서는 무시된다. 대신 eslint.config.mjs의 ignores 배열을 글로벌 스코프에 선언해야 한다. 이걸 놓치면 node_modules와 dist까지 전부 린트 대상이 되어 VS Code 확장이 CPU를 100% 치는 상황이 연출된다. ignores: ['**/node_modules/**', '**/dist/**']를 가장 먼저 선언하지 않으면 다른 설정이 먼저 매칭돼서 ignores가 씹힌다.
globals와 env의 소멸이 만드는 사소한 폭발
.eslintrc의 env: { browser: true, node: true } 한 줄은 사실 엄청난 양의 전역 변수를 주입한다. window, document, process, Buffer까지 수십 개의 예약어를 룰에서 제외해주는 역할이다. flat config에서는 env 키가 사라졌고, globals 패키지를 직접 임포트해서 languageOptions.globals에 넣어야 한다.
import globals from 'globals'; export default [ { languageOptions: { globals: { ...globals.browser, ...globals.node, }, }, }, ];
이걸 빠뜨리면 'process' is not defined나 'window' is not defined가 파일 열 때마다 수십 개씩 튀어나온다. 테스트 파일에서 describe, it, expect를 못 찾는 것도 같은 이유다. globals.jest 또는 globals.mocha를 테스트 전용 config 객체에만 선택적으로 주입하는 패턴으로 해결한다.
files 필드로 대상을 좁히는 게 flat config의 장점이지만, 동시에 실수하기 쉬운 지점이기도 하다. files: ['**/*.test.ts']로 테스트 전역 변수를 제한했는데 .spec.ts 파일은 방치하는 식이다. .test.ts와 .spec.ts를 모두 커버하려면 files: ['**/*.{test,spec}.ts']처럼 써야 한다. Glob 매칭에서 중괄호가 지원되지 않는 줄 알고 따로따로 객체를 만들었다가, 병합 순서가 꼬여서 한쪽이 덮어씌워지는 버그도 흔하다.
마이그레이션 직후 검증 루틴
체크리스트 하나로 정리한다. 각 항목이 false이면 그냥 넘어가면 안 된다.
npx eslint가 종료 코드 0으로 끝나는가? → CI에서 통과해도 플러그인 일부가 무시된 상태일 수 있으니--print-config로 최종 병합 결과를 반드시 확인한다.npx eslint --print-config src/index.ts | grep "react/"같은 식으로 플러그인별 규칙 존재 여부를 검사했는가? → 출력이 없으면 해당 플러그인은 마이그레이션에 실패한 거다.- VS Code ESLint Output Channel에 "Invalid config"나 "Failed to load plugin" 메시지가 없는가? → 터미널과 달리 VS Code 확장은 설정 오류를 조용히 삼키는 경우가 많다.
.eslintignore가.gitignore에 흡수된 줄 알고 ignores 설정을 안 한 건 아닌가? →ignores는 반드시 최상단 독립 객체로 선언한다.globals임포트를 했는가? →npx eslint --print-config | grep globals로 전역 변수가 주입됐는지 확인한다.- 모든 플러그인이 flat config 호환인가? →
eslint-plugin-import처럼 아직 flat config를 지원하지 않는 플러그인은compat레이어(@eslint/compat)의fixupPluginRules로 감싸거나,eslint-plugin-import-x같은 포크로 교체한다.
마지막 항목은 특히 함정이다. compat 레이어는 플러그인 내부의 context.getScope() 호출 같은 레거시 API를 flat config 모델에 맞게 중계해주는데, 이게 100% 동작한다는 보장은 없다. fixupPluginRules로 감싼 뒤에도 특정 규칙이 조용히 실패하는 경우가 있다. 이때는 포크로 갈아타든지, 아니면 ESLint 8을 유지할 기간을 정하고 점진적으로 옮기는 수밖에 없다.
마이그레이션의 진짜 적은 CLI가 아니라 조용한 실패다. 터미널에서 초록불이 떴다고 다 된 게 아니다. --print-config로 병합 결과를 확인하고, VS Code Output Channel을 열어 ESLint 서버 로그를 읽고, 플러그인별 규칙 존재 여부를 grep하는 세 단계를 습관화하지 않으면, 규칙이 절반만 적용된 채 몇 달을 지나칠 수 있다.
댓글
댓글을 읽어오는 중입니다.
같이 읽으면 좋은 글
방금 읽은 주제와 이어지는 글을 골랐습니다.
eslintrc를 버리고 flat config로 갈아탈 때 조용히 무너지는 지점들
eslintrc에서 flat config로 전환할 때 가장 많이 무너지는 지점은 규칙이 아니라 플러그인 호환성과 설정 병합 순서다. 문자열 기반 extends가 사라지고 객체 배열로 바뀌면서 플러그인 등록 방식, ignores 처리, 에디터 연동이 모두 달라진다. 이 글은 실제 마이그레이션에서 조용히 실패하는 지점을 짚고, 무엇을 먼저 옮기고 무엇을 버려야 하는지 판단 기준을 제시한다.
2,731개의 타입 에러와 마주했던 한 주, strict 옵션을 끄고 켜는 사이의 현실
TypeScript strict 계열 옵션을 한 번에 켜면 수천 개의 타입 에러가 쏟아져 작업이 멈추기 십상이다. 실제 프로젝트 사례를 바탕으로 옵션별 변경 난이도와 영향 범위를 비교해 어떤 순서로 켜는 것이 안전한지, 어디까지 자동으로 고치고 어디를 수동으로 봐야 하는지 단계적 로드맵으로 정리했다.
any 지옥에서 strict로 승급한 어느 프로젝트의 사투기
느슨하게 태어난 TypeScript 프로젝트에서 strict를 하루아침에 켜면 수백 개의 에러가 쏟아진다. 이 글은 신규 기능 개발과 버그 수정 틈새에서 strict 계열 옵션을 하나씩 켠 실제 진행 기록으로, 옵션별 난이도와 효과를 따져 최적의 적용 순서를 정리했다. any 소탕 순서부터 strictNullChecks, noImplicitAny까지 실무에서 겪은 장애와 회귀를 함께 담았다.
이전 글
1년 동안 DB 테이블로 큐 대신 쓰면서 내가 놓친 것들
다음 글
당신의 RLS 정책은 조용히 거짓말을 하고 있다
DevInsight Digest
새 글이 쌓이면, 피드에서 바로 이어 읽으세요.
과장된 알림 대신 발행한 글 전체를 RSS로 제공합니다.