본문 바로가기

프로그래밍

ESLint v9 Flat Config 마이그레이션 가이드: typescript-eslint 연동 단계별 설명

반응형

왜 지금 Flat Config로 옮겨야 할까

기존 .eslintrc 계열은 플러그인 해석 순서와 상속 체인이 복잡해 프로젝트마다 규칙 충돌이 잦았습니다. ESLint v9의 Flat Config는 단일 배열 기반 합성으로 규칙 적용 과정을 명시화해, 설정 비용은 늘지만 예측 가능성과 성능(캐싱·해석 비용 감소)을 제공합니다.

타입스크립트를 쓰는 팀은 typescript-eslint의 파서·플러그인 조합과 타입 정보(프로젝트 참조) 전달이 핵심 과제입니다. Flat Config 전환 시 “파일 매칭 -> 파서/플러그인 바인딩 -> 타입 체크 규칙 분리” 흐름을 코드로 구조화해, tsconfig 경로·모노레포 워크스페이스 같은 엣지 케이스를 줄일 수 있습니다.

다만 전환에는 리스크가 있습니다. 규칙 이름 변경, 공유 설정 호환성 저하, 빌드 도구(ESM/CJS) 혼재로 인한 실행 컨텍스트 오류를 감수해야 합니다.

이 글은 마이그레이션 순서와 안티패턴을 짚어, “동작은 하지만 느리거나 불안정한 설정”을 피하는 실무 기준을 제공합니다.

> 안티패턴: 기존 eslintrc 설정을 자동 변환만 하고 전면 적용하는 방식은 추천하지 않습니다. 단계별로 JS/TS, 타입 의존 규칙, 테스트 파일 범위를 분리 적용해 성능 회귀와 오탑지(불필요한 경고 폭증)를 먼저 확인하세요.

“파일 매칭 → 파서 → 규칙”으로 이해하는 Flat Config

Flat Config는 배열을 위에서 아래로 적용합니다. 즉 `파일 매칭 → 파서/플러그인 바인딩 → 규칙 병합`이 순차 실행되고, 앞에서 누적·뒤에서 덮어쓰기가 일어납니다.

React+TS 모노레포라면 `packages/*/src//*.ts(x)`에는 typescript-eslint와 타입 규칙, `scripts//*.ts`에는 문법 위주 규칙처럼 레이어를 분리합니다.

- 단일 배열로 상속 해석 비용은 줄지만, 순서가 곧 우선순위라 위치 설계가 핵심입니다.
- 경로별 다른 파서를 지정해 이질성을 흡수하되, 패턴 누락 시 검사 공백이 생깁니다.
- 타입 규칙은 tsconfig I/O로 느려질 수 있어 빌드 스크립트는 비타입 프로파일이 유리합니다.

- 소스: packages/*/src/**/*.tsx -> parser: @typescript-eslint/parser -> rules: stylistic + type-aware
- 스크립트: scripts/**/*.ts -> parser: @typescript-eslint/parser -> rules: stylistic only
- JS: **/*.js -> parser: espree -> rules: import/order 등 공통

> 안티패턴: 전 파일에 타입 규칙을 일괄 적용하면 CI 시간이 급증합니다. 테스트·빌드 스크립트는 비타입으로 분리하세요.

단계별 TS 마이그레이션: Flat Config 압축 가이드

가장 안전한 흐름은 “JS 규칙 → TS 문법 → TS 타입” 3단 분리입니다. 파일 패턴·파서·규칙 고정 후, 타입 규칙을 마지막에 올려 강도를 점진적으로 높입니다.

- 1단계: ESLint v9, @eslint/js, typescript-eslint 설치·ESM 확인
- 2단계: eslint.config.js 생성, JS 추천 규칙 적용
- 3단계: src/**/*.ts(x)에 TS 파서/플러그인 바인딩
- 4단계: 타입 규칙 블록 분리, tsconfig 경로 지정
- 5단계: scripts/**/*.ts는 비타입 프로파일로 분리
- 6단계: 경로 겹침은 뒤 블록 우선 정렬

아래 예시는 구조 이해용이며, 모노레포는 패키지 루트 기준으로 패턴·tsconfig를 조정하세요.

// eslint.config.js (ESM)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';

export default [
  js.configs.recommended,
  {
    files: ['packages/*/src/**/*.{ts,tsx}'],
    languageOptions: {
      parser: tseslint.parser,
      parserOptions: { ecmaFeatures: { jsx: true }, sourceType: 'module' },
    },
    plugins: { '@typescript-eslint': tseslint.plugin },
    rules: { ...tseslint.configs.recommended.rules },
  },
  {
    files: ['packages/*/src/**/*.{ts,tsx}'],
    languageOptions: {
      parser: tseslint.parser,
      parserOptions: {
        project: ['./packages/*/tsconfig.json'],
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      ...tseslint.configs['recommended-type-checked'].rules,
      '@typescript-eslint/no-misused-promises': 'warn',
    },
  },
  {
    files: ['scripts/**/*.ts'],
    languageOptions: { parser: tseslint.parser },
    rules: { ...tseslint.configs.recommended.rules },
  },
];

핵심은 블록별 “files → parser → rules” 순서 유지입니다. 타입 블록을 뒤에 둬 앞선 규칙을 필요한 범위만 덮어 성능과 정확도를 균형화합니다.

> 안티패턴
> - 문법·타입 규칙 혼합: 캐시 불안정, 실행 시간 증가
> - files 누락: 검사 공백 발생 → CI에서 “lint 대상 파일 수” 검증
> - 단일 루트 project: 모노레포에서 타입 I/O 과증가 → 패키지 단위 분리

흔한 함정과 선택 기준: 타입 규칙, ESM, 모노레포

Flat Config 전환의 병목은 타입 규칙과 실행 컨텍스트입니다. 타입 인지 블록을 분리하고 tsconfig 경로를 고정하지 않으면 캐시 불안정, 성능 저하, 오탐이 동시에 생깁니다.

아래 체크리스트는 충돌·누락을 줄이는 최소 구성이며, 모노레포일수록 순서와 범위를 엄격히 고정합니다.

- ts 파일 매칭 블록과 타입 규칙 블록을 분리하고, 타입 블록은 배열 뒤쪽에 둡니다.
- parserOptions.project는 실제 참조 tsconfig(예: tsconfig.eslint.json)로 지정합니다.
- 모노레포는 패키지마다 root:true 대신 패턴/tsconfig 경로를 패키지 기준으로 고정합니다.
- ESM 실행 시 eslint.config.js도 ESM으로 두고 CJS 혼용을 피합니다.
- scripts, config 폴더는 비타입 프로파일로 제외하거나 약하게 둡니다.

> 안티패턴
> - 전 파일에 타입 규칙 일괄 적용 후 속도 문제 방치
> - js·ts 규칙을 같은 블록에 섞어 우선순위 혼탁
> - 겹치는 경로 패턴에 대한 블록 순서 미고려로 의도치 않은 덮어쓰기
> - parser만 바꾸고 플러그인/extends를 누락해 규칙이 비활성화

Flat Config는 상속이 아닌 위→아래 합성이라 가시성은 높지만 설계 실수의 비용이 큽니다. 파일 매칭 → 파서·플러그인 바인딩 → 규칙·프로젝트 I/O를 분리해야 캐싱과 I/O 경계를 안정화할 수 있습니다.

타입 규칙은 코드베이스 규모와 CI 비용으로 판단합니다. 대규모·느린 CI라면 PR은 비타입 빠른 프로파일, 메인은 타입 포함 이중 프로파일로 비용과 신뢰도 균형을 잡습니다.

지금 할 일: 최소 설정으로 먼저 굴리고, 범위를 넓히세요

Flat Config 전환은 “파일 매칭 → 파서 바인딩 → 규칙 분리” 순서만 지키면 예측 가능성이 커집니다. 먼저 비타입 프로파일로 안전하게 통과시킨 뒤 타입 규칙을 뒤에 얹는 것이 비용을 줄입니다.

바로 실행할 체크리스트입니다. 한 번에 모두 바꾸기보다 작은 단위로 저장-실행-수정 루프를 돌리세요.

- eslint.config.js를 ESM으로 만들고 @eslint/js 기본 규칙만 통과시킵니다.
- src/**/*.ts(x)에 @typescript-eslint/parser/플러그인을 연결합니다.
- scripts, config 폴더는 비타입 규칙으로 별도 블록에 둡니다.
- 마지막에 parserOptions.project로 tsconfig.eslint.json을 지정해 타입 규칙 블록을 추가합니다.
- 모노레포면 패키지별 패턴/tsconfig 경로를 명시하고, 타입 블록을 배열의 끝으로 보냅니다.

> 안티패턴
> - 전 파일에 타입 규칙을 한 번에 켜서 캐시·속도·오탐을 동시에 악화시키는 구성
> - CJS/ESM 혼용으로 플러그인 로딩 실패가 나는 실행 컨텍스트 불일치
> - 패턴 누락으로 검사 공백이 생기는데도 통과로 오인하는 상황

마이그레이션은 JS 규칙 통과 → TS 문법 적용 → 타입 규칙 추가의 3단 흐름으로 진행하세요. “변경 → lint --cache 실행 → 느려지면 타입 블록 범위 축소”처럼 A -> 실행 -> 피드백 사이클을 짧게 가져가면 리스크를 통제할 수 있습니다.

경로 겹침 정렬: 뒤에서 덮어쓰기 전략

파일 패턴이 겹치면 Flat Config는 뒤에 오는 블록이 규칙을 덮어씁니다. 성능이 무거운 타입 규칙은 항상 마지막에 배치해 충돌과 중복 파싱을 줄이세요.

대표 시나리오는 src//*.ts(x)와 scripts//*.ts가 교차할 때입니다. 아래처럼 범용 TS 문법 블록 → 예외(스크립트) → 타입 규칙 순으로 정렬하면 누락 없이 의도대로 병합됩니다.

- 1) TS 문법 블록: src/**/*.ts(x) + @typescript-eslint/parser, no-unused-vars(비타입)
- 2) 스크립트 예외: scripts/**/*.ts + Node/ES 규칙, 타입 규칙 비활성
- 3) 타입 규칙 블록: src/**/*.ts(x) + parserOptions.project, strict-boolean-expressions 등

> 안티패턴
> - 타입 규칙을 앞에 두고 뒤에서 비타입 블록으로 다시 덮어쓰기
> - src와 tests가 겹치는데 tests 전용 완화 규칙을 앞에 배치
> - 광범위한 패턴(“**/*.ts”)을 뒤에 둬 세부 패턴을 무력화

왜 이런 정렬이 필요한지 간단히 보면, 매칭 → 파서/플러그인 → 규칙 병합이 위→아래로 누적됩니다. 즉 src -> scripts -> types 블록 순서로 두면 A(src) -> B(scripts) -> C(types) 흐름에서 C가 최종 우선권을 가져 타입 검사 범위를 정확히 고정합니다.

필요하다면 다음과 같이 주석으로 의도를 문서화하세요. 나중에 규칙을 추가할 때도 순서를 보존하는 기준점이 됩니다.

반응형