eslint의 flat config가 추구하는 단순함

2025년 6월 21일

얼마 전에 회사 리포지토리의 eslint의 버전을 업그레이드하면서 flat config를 도입했다. eslint v9부터는 이제 flat config가 기본값이 되었다고 하는데, 왜일까?

무엇이 달라졌을까?

flat config의 요점은 하나의 파일에 모든 규칙을 적는 것이다. 크고 복잡한 코드베이스를 운영하다보면 각각의 코드 파일들에 다른 lint 규칙을 적용하고 싶을 때가 많다. 이 요구에 부응하기 위해, 기존의 eslint는 대상 파일의 상위 디렉토리로 거슬러 올라가며 모든(root 설정을 만날 때 까지) 설정 파일을 탐색하고 규칙을 적용한다. 따라서 개발자는 프로젝트 최상단에 기본 규칙을 명시한 후, 각 디렉토리 별로 세분화된 설정을 추가하는 것으로 규칙을 커스터마이징할 수 있다.마치 객체 상속 같다.

flat config는 대신 하나의 파일에서, 규칙 객체의 배열을 선언하는 것으로 문제를 해결한다. 각각의 객체는 적용/무시 규칙, lint 규칙, 파싱 옵션, 사용할 플러그인들을 독립적으로 지니고 있으며, 위에서부터 순차적으로 대상 파일들에 적용된다. 서로 다른 규칙들은 모두 적용되고, 겹치는 부분은 덮어쓰기된다. flat cascade라는 방식이라고 한다.

Goodbye extends, hello flat cascade -- ESLint 공식 블로그 포스팅

// 어머 깔끔해
export default [
  {
    // 적용 범위
    files: ["**/*.{ts,tsx}"], 
    
    // 규칙들
    extends: [
      js.configs.recommended,
      ...tseslint.configs.strictTypeChecked,
      ...tseslint.configs.stylisticTypeChecked,
    ],
    
    // 플러그인
    plugins: {
      "simple-import-sort": simpleImportSort,
    },
  }
]

왜 바뀌었을까?

공식 문서를 읽어본 것은 아니고, 직접 사용해보면서 든 생각이다. 나중에 이런 문서들을 읽으면서 생각을 구체화해볼 것.

트리 구조는 참 직관적이라서 복잡하다

두 가지 이유로 eslint는 단순해졌다. 하나는 여러 디렉토리를 돌아다니며 설정 파일을 찾지 않아도 된다는 점에서고, 다른 하나는 설정 객체의 의미와 동작 방식이 명확해졌다는 점에서다.

기존에는 파일 디렉토리에 의존한 트리 구조로 설정들이 관계 맺고 있었다. 반대로 flat config는 하나의 위계만을 갖고 있다. flat config에 의미를 부여하는 것은 트리의 부모-자식 관계가 아니라 "위에서 아래"라는 하나의 흐름이다. 위에서 아래로 규칙들을 순차적으로 적용하는 흐름. 모든 규칙이 적용되고 나면(마치 이벤트 소싱처럼) 결과물은 반드시 하나의 결정론적인 트리가 될 텐데, 이를 1차원 배열로 서술하는 것은 결과물의 복잡도를 많은 경우에 해소해준다.

트리 구조는 참 직관적이라서 복잡하다. 여기에 대해서는 새로운 글을 쓸 것.

glob 매칭의 자유로움

개발자들은 참 창의적인 방식으로 본인의 리포지토리를 관리하기 때문에, 디렉토리 구조에 따라 린트 규칙을 세분화하는 방식은 큰 제약으로 다가온다. 때로는 특정 확장자나 파일명, 또는 디렉토리와 파일명의 복잡한 조합으로 코드 스타일 정책이 명시될 수도 있다. 이제는 설정 파일이 디렉토리에 구애받지 않기 때문에, 오직 glob 매칭에만 의존하여 마음 편히 적용 범위를 커스터마이징할 수 있다.

참고로 glob 매칭에는 minimatch 라이브러리를 사용한다고 한다. 반드시 공부할 것.

정책의 추가 / 삭제가 쉽다

린트 정책이 추가 / 삭제될 일은 많지 않지만, 분명 존재한다. 특히 모노리포와 같은 환경에서는 더 그렇다. 이럴 때 flat config는 기존 규칙을 크게 건드리지 않고, 단순히 객체를 추가로 작성하는 것으로 정책의 변경을 가능하게 한다.

그리고 린트를 쉽게 부분 도입할 수 있는 것도 매력적이다. 이번 기회에 import-sort 플러그인을 회사의 프론트엔드 모노리포에 추가해보았는데, 효용이 입증되지도 않은 상황에서 전체 코드에 거대한 diff를 남기는 리팩토링을 진행할 수는 없었다. 그래서 비교적 규모가 작으면서 활발히 개발되고 있는 웹뷰 프로젝트에 먼저 플러그인을 도입해보았다. 아래와 같은 객체 하나를 배열의 끝에 추가하는 것으로 말이다.

  // 더도 말고 덜도 말고 딱 import-sort만 
  {
    files: ["mobile/**/*.{ts,tsx}"],
    plugins: {
      "simple-import-sort": simpleImportSort,
    },
    rules: {
      "simple-import-sort/imports": "error",
      "simple-import-sort/exports": "error",
    },
  },

아쉬운 점

기존의 디렉토리 별 매칭이 가진 이점도 있었다. 바로 패키지 의존성 분리이다.

예컨데 NextJS 프로젝트를 위한 eslint-config-next는 next에 의존하는데, 다양한 프로젝트를 관리하는 환경에서 린팅을 위해 특정 패키지를 루트에 설치해야만 하는 것은 아쉽다. 물론 flat config에서도 파일 간 상속을 구현하는 방법이 있을 것 같지만 flat config의 디자인 철학과 맞지는 않다. 사실 eslint-config-next가 next에 의존하는 것 자체가 문제인 것 같기도.

또한 tsconfig 파일이 여전히 extend 기반의 트리 구조로 동작하는 한, tsconfig 파일에 기반한 린팅 방식(typescript-eslint 등이 사용하는)이 무엇인가 문제를 일으킬 것 같은 강한 예감이 든다.

여전히 고민은 남는다

트리와 1차원 배열 간의 경쟁은 참 흥미로운 고민거리이다.

1차원 배열의 가장 큰 이점은 구조에 큰 변경 없이도 내용을 변경하기가 좋다는 것이다. 그렇다보니 큰 변경도 아주 간단하게 처리할 수 있는데, 그러다보면 배열이 지저분해지기 쉽상이다. 배열만을 보았을 때, 이로써 결정되는 트리를 상상하기가 어려워지는 것이다.

보통 사용자에게 더 많은 자유도와 권한을 주는 기술은 쓰기 어렵기 마련이다. 린트 규칙을 명확히 서술할 노하우와 정책이 뒷받침되어야 flat config의 유용함이 비로소 느껴지지 싶다.


  • 글을 쓰는 데 한시간 반 걸림. 힘이 너무 들어갔다.
  • strapi 에디터
    • 볼드 단축키 버그가 있다. cmd+b 해도 볼드 적용되지 않음.
    • 사용성은 그냥저냥. 헤더에 기본 볼드 처리가 되어 있지 않은 부분이 매력적이다.