DevInsight

개발의 판단과 맥락을 기록하는 곳

Tools
조회 0약 4분 읽기

ESLint Flat Config 마이그레이션 실패 일지와 살아남는 체크리스트

ESLint 9의 flat config로 넘어가면서 extends가 사라지고, 플러그인 호환성 문제, VS Code ESLint 확장과의 설정 불일치, 글로벌 변수 선언 방식 변화 등 현장에서 마주치는 장애물을 해결 순서대로 정리한다. 삽질을 줄이는 실전 체크리스트. 2025년 4월, ESLint 9가 정식 릴리스되면서 파일은 deprecated 경고를 넘어 아예 무시되기 시작했다.

DevInsight 편집팀 발행

AI 보조 초안과 편집 검수를 거쳐 발행했습니다.

#ESLint#flat config#eslintrc#마이그레이션#TypeScript#ESLint 9#VS Code#린트 설정#JavaScript 도구#개발 환경

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-reacteslint-plugin-react/configs 경로로 flat config를 제공 중이고, @typescript-eslinttypescript-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.useFlatConfigtrue로 강제하고, eslint.nodePath를 프로젝트의 node_modules로 지정한다. 그래도 안 되면 VS Code를 워크스페이스 루트에서 재시작한다. ESLint 서버가 캐싱한 이전 설정을 물고 있으면 설정 파일을 바꿔도 반영되지 않는다.

또 하나. .eslintignore가 flat config에서는 무시된다. 대신 eslint.config.mjsignores 배열을 글로벌 스코프에 선언해야 한다. 이걸 놓치면 node_modulesdist까지 전부 린트 대상이 되어 VS Code 확장이 CPU를 100% 치는 상황이 연출된다. ignores: ['**/node_modules/**', '**/dist/**']를 가장 먼저 선언하지 않으면 다른 설정이 먼저 매칭돼서 ignores가 씹힌다.

globalsenv의 소멸이 만드는 사소한 폭발

.eslintrcenv: { 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이면 그냥 넘어가면 안 된다.

  1. npx eslint가 종료 코드 0으로 끝나는가? → CI에서 통과해도 플러그인 일부가 무시된 상태일 수 있으니 --print-config로 최종 병합 결과를 반드시 확인한다.
  2. npx eslint --print-config src/index.ts | grep "react/" 같은 식으로 플러그인별 규칙 존재 여부를 검사했는가? → 출력이 없으면 해당 플러그인은 마이그레이션에 실패한 거다.
  3. VS Code ESLint Output Channel에 "Invalid config"나 "Failed to load plugin" 메시지가 없는가? → 터미널과 달리 VS Code 확장은 설정 오류를 조용히 삼키는 경우가 많다.
  4. .eslintignore.gitignore에 흡수된 줄 알고 ignores 설정을 안 한 건 아닌가? → ignores는 반드시 최상단 독립 객체로 선언한다.
  5. globals 임포트를 했는가? → npx eslint --print-config | grep globals로 전역 변수가 주입됐는지 확인한다.
  6. 모든 플러그인이 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하는 세 단계를 습관화하지 않으면, 규칙이 절반만 적용된 채 몇 달을 지나칠 수 있다.

댓글

댓글을 읽어오는 중입니다.

같이 읽으면 좋은 글

방금 읽은 주제와 이어지는 글을 골랐습니다.

Tools 전체 보기

이전 글

1년 동안 DB 테이블로 큐 대신 쓰면서 내가 놓친 것들

다음 글

당신의 RLS 정책은 조용히 거짓말을 하고 있다

DevInsight Digest

새 글이 쌓이면, 피드에서 바로 이어 읽으세요.

과장된 알림 대신 발행한 글 전체를 RSS로 제공합니다.

RSS 구독