DevInsight

개발하며 겪은 판단을 적어요

Tools
조회 1약 4분 읽기

eslintrc를 버리고 flat config로 갈아탈 때 조용히 무너지는 지점들

eslintrc에서 flat config로 전환할 때 가장 많이 무너지는 지점은 규칙이 아니라 플러그인 호환성과 설정 병합 순서다. 문자열 기반 extends가 사라지고 객체 배열로 바뀌면서 플러그인 등록 방식, ignores 처리, 에디터 연동이 모두 달라진다. 이 글은 실제 마이그레이션에서 조용히 실패하는 지점을 짚고, 무엇을 먼저 옮기고 무엇을 버려야 하는지 판단 기준을 제시한다.

DevInsight에서 발행해요

#ESLint#flat config#마이그레이션#린트#플러그인#개발도구#eslintrc#빌드설정

ESLint 9로 올린 뒤 CI는 초록불인데 편집기에는 빨간 줄이 남아 있는 상황이 있다. 반대도 흔하다. 편집기는 조용한데 커맨드라인에서 돌린 린트가 새로운 에러를 쏟아낸다. 규칙이 깨져서가 아니다. 같은 린트를 서로 다른 두 세계에서 실행하고 있기 때문이다. eslintrc와 flat config는 파일 이름만 다른 게 아니라 설정을 읽고 조립하는 경로 자체가 다르다. 그래서 이 마이그레이션은 규칙 목록을 옮기는 작업이 아니라, 설정이 만들어지는 순서를 다시 그리는 작업에 가깝다.

규칙이 죽는 순간은 대개 조용하다

가장 위험한 실패는 에러를 내는 실패가 아니다. 플러그인이 로드되지 않으면 ESLint는 아무 경고 없이 그 규칙들을 건너뛴다. react-hooks/rules-of-hooks가 빠져도 린트는 통과한다. 조건문 안에서 훅을 호출해도, 의존성 배열을 비워도 아무 일이 없다. 린트가 초록이니 안심하고 넘어가기 쉽다.

이 침묵을 확인하는 방법은 규칙 개수를 세는 것이다. npx eslint --print-config src/App.tsx로 설정을 덤프하면 특정 파일에 실제로 적용된 규칙이 전부 나온다. 이전 버전의 출력과 diff를 뜨면 어느 규칙이 사라졌는지 바로 보인다. 일부러 규칙을 어기는 파일을 하나 만들어 잡히는지 보는 방법도 빠르다. 마이그레이션 첫 단계의 기준은 "린트가 도는가"가 아니라 "의도한 규칙이 실제로 걸리는가"다.

extends 문자열이 하던 일

eslintrc에서는 extends: ["airbnb", "plugin:react/recommended"]처럼 이름을 나열하면 나머지는 ESLint가 해석했다. flat config에는 이 키가 없다. 설정 객체 배열을 직접 나열하고, 공유 설정은 그 배열에 펼쳐 넣는다. plugin:react/recommended 같은 문자열은 통하지 않는다.

플러그인이 flat 대응을 내놨다면 react.configs.flat.recommended 같은 객체를 배열에 넣는다. 아직 대응이 없으면 @eslint/eslintrc 패키지의 FlatCompat으로 예전 설정을 변환해 끼워 넣는 우회로가 있다. 다만 FlatCompat은 만능 어댑터가 아니다. overridesignorePatterns가 뒤섞인 설정을 변환하면 파일 매칭이 미묘하게 어긋난다. 변환을 마친 뒤에도 --print-config로 특정 파일의 규칙 집합을 다시 확인하는 이유가 여기에 있다. 대응이 끊긴 공유 설정은 이 경로에 오래 머물게 되므로, 유지보수되는 대안으로 바꾸는 편이 낫다.

플러그인은 이름이 아니라 객체로 들어온다

eslintrc의 plugins: ["react"]는 문자열 목록이었다. flat config에서는 플러그인 모듈을 import해서 객체로 넣는다. 문제는 네임스페이스다. plugins: { react }로 등록했으면 규칙 참조도 react/jsx-...가 된다. 키 이름을 바꾸면 규칙 id도 통째로 달라진다. @typescript-eslint처럼 스코프가 붙은 패키지도 같다. 플러그인이 제공하는 flat 프리셋을 그대로 쓰면 이 등록이 자동으로 되지만, 규칙을 손으로 나열할 때는 키와 규칙 접두사를 눈으로 맞춰야 한다.

파서 위치도 이동했다. eslintrc의 parser, parserOptions는 flat config에서 languageOptions 아래로 들어간다. ecmaVersion, sourceType, globals도 같은 곳에 모인다. env 키는 사라졌다. env: { browser: true }languageOptions.globalsglobals.browser를 직접 펼치는 방식으로 바뀐다. 이 변환을 빠뜨리면 documentwindow 같은 전역이 no-undef에 걸린다. 타입 인식 린팅을 켠 프로젝트라면 languageOptions.parserOptions.projectService 같은 옵션 이름도 버전에 따라 달라졌으니, typescript-eslint 문서의 flat 예시를 기준으로 맞추는 편이 안전하다.

배열의 순서가 곧 병합 순서

flat config에서 설정은 위에서 아래로 누적되고, 같은 규칙을 뒤에서 다시 정의하면 뒤가 이긴다. eslintrc의 extends가 암묵적으로 정하던 우선순위를 이제는 배열 순서로 직접 표현한다. 베이스 설정을 먼저, 그다음 프레임워크·타입 설정, 마지막에 프로젝트 고유 오버라이드를 둔다. 순서를 잘못 잡으면 recommended가 내가 명시한 규칙을 덮어쓴다.

import js from "@eslint/js"; import globals from "globals"; import tseslint from "typescript-eslint"; export default [ { ignores: ["dist/**", "coverage/**"] }, js.configs.recommended, ...tseslint.configs.recommended, { files: ["src/**/*.{ts,tsx}"], languageOptions: { globals: globals.browser }, rules: { "no-console": "warn" }, }, ];

...tseslint.configs.recommended처럼 스프레드로 펼치는 프리셋은 내부에 여러 객체를 품고 있어서, 이걸 뒤에 두면 앞선 설정을 통째로 밀어낸다. 프리셋을 먼저, 손으로 쓴 예외를 뒤에 두는 이유다. globals 패키지처럼 예전 env를 대체하는 모듈은 필요한 환경만 골라 넣는다.

ignores는 어디에 두느냐가 전부다

eslintrc의 ignorePatterns는 설정 전체에 일괄 적용됐다. flat config의 ignores는 위치에 따라 의미가 갈린다. files 없이 ignores만 있는 객체는 전역 무시로 동작한다. files와 함께 있으면 그 파일 집합에서만 제외한다. 이 차이를 모르면 검사 대상이 통째로 비거나, 반대로 무시하려던 dist가 어느 설정에서는 여전히 검사된다. .eslintignore 파일도 flat config에서는 기본으로 읽지 않는다. 무시 목록은 설정 배열 앞쪽의 전역 ignores 항목으로 옮겨야 한다.

CLI 쪽도 손봐야 한다. --ext가 없어졌으니 확장자 지정은 files 패턴이 흡수한다. --ignore-path도 사라졌다. package.json 스크립트에 이 플래그들이 남아 있으면 조용히 무시되거나 에러를 낸다.

편집기는 다른 ESLint를 보고 있을 수 있다

VS Code ESLint 확장은 버전에 따라 flat config 지원이 다르다. 확장이 오래되었거나 flat config 사용 플래그가 꺼져 있으면 루트의 eslint.config.js를 찾지 못하고 예전 .eslintrc 경로를 뒤진다. CI와 편집기의 결과가 갈리는 지점이 여기다. 확인할 것은 세 가지다. 확장 버전, eslint.useFlatConfig 설정, 확장이 실제로 로드한 ESLint 버전. 모노레포라면 워크스페이스마다 ESLint가 따로 깔려 있어 편집기가 엉뚱한 버전을 집는 경우도 있다. npx eslint --version과 확장 로그에 찍힌 경로를 나란히 놓고 보면 원인이 빨리 드러난다.

옮기는 순서

한 번에 전부 바꾸면 어디서 어긋났는지 추적이 안 된다. 다음 순서를 권한다.

  1. 예전 설정을 그대로 둔 채 flat 파일 하나를 만든다. FlatCompat으로 기존 eslintrc를 통째로 감싼다. 이 단계의 목표는 규칙 파리티이고, 새 규칙은 추가하지 않는다.
  2. 주요 파일 몇 개에 대해 --print-config 출력을 뽑아 이전 버전과 diff한다. 차이가 나온 규칙이 FlatCompat이 놓친 부분이다.
  3. 플러그인을 하나씩 flat 네이티브 설정으로 교체한다. 각 교체마다 diff를 다시 뜬다.
  4. 마지막에 ignores, 파서, globals를 정리하고 .eslintignore를 지운다.

순서를 뒤집어 새 규칙을 먼저 추가하면, 규칙 파리티가 깨진 건지 새 규칙이 잡은 건지 구분이 안 된다.

버릴 것과 남길 것

판단 기준은 하나다. 대응이 끊긴 플러그인이 그 프로젝트에서 실제로 값을 내는가. 유지보수가 멈춘 eslintrc 전용 플러그인은 flat 전환을 계기로 걷어내는 편이 낫다. FlatCompat으로 억지로 감싸도 변환 과정에서 규칙이 조용히 빠질 위험이 남는다. 반대로 규칙 몇 개만 쓰는 작은 플러그인은 직접 객체로 등록해버리면 FlatCompat 의존 없이 깔끔하게 끝난다.

에디터에서 CI와 같은 결과를 보는 것까지 확인해야 마이그레이션이 끝난다. CI만 초록이면 절반이다.

댓글

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

같이 읽으면 좋은 글

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

Tools 전체 보기
Tools

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

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

#ESLint#flat config#eslintrc#마이그레이션
Tools

2,731개의 타입 에러와 마주했던 한 주, strict 옵션을 끄고 켜는 사이의 현실

TypeScript strict 계열 옵션을 한 번에 켜면 수천 개의 타입 에러가 쏟아져 작업이 멈추기 십상이다. 실제 프로젝트 사례를 바탕으로 옵션별 변경 난이도와 영향 범위를 비교해 어떤 순서로 켜는 것이 안전한지, 어디까지 자동으로 고치고 어디를 수동으로 봐야 하는지 단계적 로드맵으로 정리했다.

#TypeScript#strict 모드#타입 안정성#레거시 코드
Tools

node_modules를 두 번 지운 뒤에야 패키지 매니저를 다시 봤다

패키지 매니저 선택은 설치 속도, 디스크 사용량, 유령 의존성이라는 세 축이 서로 맞물려 움직인다. 단독 저장소에선 대충 선택해도 문제가 드물지만, 모노레포로 넘어가면 hoisting 방식 차이가 CI 시간과 디스크 폭발로 그대로 드러난다. npm·pnpm·yarn이 각각 의존성을 저장하고 공유하는 구조를 비교하고, 팀 규모와 모노레포 성숙도라는 실제 조건에 맞춰 도구를 고르는 판단 흐름을 정리했다.

#패키지매니저#pnpm#npm#yarn

이전 글

검색이 자꾸 엉뚱한 문서를 물어올 때 임베딩보다 먼저 의심해야 할 것

DevInsight Digest

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

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

RSS 구독