# Configuring ESLint > 그동안 ESLint 설정하느라 보낸 시간을 다 합치면 적어도 10시간, 최대 100시간은 될텐데 아직도 ESLint 설정 파일이 어떻게 작동하는지 정확히 모른다. 어떤 일에 100시간을 썼는데 쌓인 게 없으면 뭔가 잘못하고 있다는 뜻이다. 2시간 정도 진득하게 공부를 해봤다. 그동안 설정하느라 보낸 시간을 다 합치면 적어도 10시간, 최대 100시간은 될텐데 아직도 ESLint 설정 파일이 어떻게 작동하는지 정확히 모른다. 어떤 일에 100시간을 썼는데 쌓인 게 없으면 뭔가 잘못하고 있다는 뜻이다. 2시간 정도 진득하게 공부를 해봤다. (<2025년 8월 17일> v9.33.0 "Flat Config" 기준) ## 공부 환경 갖추기 뭘 고쳤을 때 어떻게 바뀌는지에 대한 빠른 피드백을 받는 게 중요하다. [설정 파일을 디버깅하는 방법](https://eslint.org/docs/latest/use/configure/debug)에 대한 공식 문서를 읽어보니 `npx eslint --inspect-config`라는 명령이 있다. 실행하면 현재 적용된 설정을 일목요연하게 보여주는 웹 페이지가 열린다. 설정 파일을 수정하면 페이지의 내용이 자동으로 갱신된다. ## 빈 설정에서 시작하기 기본 설정이 뭔지 알아보자. ```js import { defineConfig } from "eslint/config" export default defineConfig([]) ``` 이렇게 했을 때의 기본 설정은 다음과 같다. ```js [ { files: ["**/*.js", "**/*.mjs"], ignores: [".git/", "**/node_modules/"], languageOptions: { sourceType: 'module', ecmaVersion: 'latest' }, linterOptions: { reportUnusedDisableDirectives: 1 } }, { files: ["**/*.cjs"], languageOptions: { sourceType: 'commonjs', ecmaVersion: 'latest' } } ] ``` 모듈 JS(`*.js` 및 `*.mjs`)와 커먼 JS(`*.cjs`)의 소스 타입을 별도로 지정한 게 눈에 띈다. 아직은 아무런 규칙도 없다. ## tseslint.config `typescript-eslint` 패키지를 쓰려면 `defineConfig()` 대신에 `tseslint.config()`를 쓰라고 안내하고 있다. 시키는대로 교체를 해도 설정에는 아무 변화가 없다. [찾아보니](https://github.com/typescript-eslint/typescript-eslint/issues/10899) 기능엔 차이가 없고 `defineConfig()`로 인해 발생하는 타입 문제를 잡아주기 위해 필요하다고 한다. ```js import tseslint from "typescript-eslint" // eslint.defineConfig() has a types incompatibility issue export default tseslint.config([]) ``` 여전히 아무런 규칙도 없는 상황. ## Global ignores 소스코드 전체에 걸쳐 추가로 무시하고 싶은 파일들이 있다면 아무런 다른 키는 없고 오로지 `ignores`만 있는 설정을 추가해야 한다고 하는데, `name`은 추가해도 괜찮았다. (`s4`는 현재 작업 중인 프로젝트 이름이다. 이런 식으로 "/" 기호를 써주면 ESLint Config Inspector가 예쁘게 렌더링을 해준다.) ```js import tseslint from "typescript-eslint" // eslint.defineConfig() has a types incompatibility issue export default tseslint.config([ { name: "s4/global ignores", ignores: [".cursor/", ".github/", "dist/", "coverage/"], // do not add anything else here to make `ignores` apply globally }, ]) ``` `name` 이외의 다른 키(예: `rules`)를 추가하면 `ignores`는 더이상 글로벌로 작동하지 않는다. 실수하기 딱 좋다. 그래서 의도를 더 명확히 드러내고 실수 방지를 도와주기 위한 [함수를 제공한다](https://eslint.org/docs/latest/use/configure/configuration-files#globally-ignoring-files-with-ignores). ```js import { globalIgnores } from "eslint/config"; export default tseslint.config([ globalIgnores([".cursor/", ".github/", "dist/", "coverage/"]), ]) ``` ## ESLint recommended 이제 규칙을 추가해보자. `@eslint/js`에는 `all`과 `recommended` 설정이 담겨 있다. `recommended`에 뭐가 있는지 `console.log()`로 찍어보면 아래와 같다. ```js { rules: { 'constructor-super': 'error', 'for-direction': 'error', // ...중략... 'use-isnan': 'error', 'valid-typeof': 'error' } } ``` `rules`만 정의하고 있는 자바스크립트 객체다. 이걸 아래와 같이 ESLint 설정에 추가하면 무슨 일이 벌어지나? ```js import js from "@eslint/js" export default tseslint.config([ // ...중략... js.configs.recommended, ]) ``` 인스펙터에서는 61개의 규칙이 "모든 파일"에 적용된다고 나온다. 왜 그럴까? [문서](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-objects)를 읽어보니 `files`를 생략하면 다른 설정에서 지정한 모든 `files`에 적용된다고 한다. ESLint 기본 설정이 `*.js`, `*.mjs`, `*.cjs`를 명시하고 있으니 이 새 패턴의 파일들에 적용된다고 보면 되겠다. 한편, 문서에서 권장하는 방식은 아래와 같다. ```js [ // ...중략... { files: ["**/*.js"], plugins: { js }, extends: ["js/recommended"] }, ] ``` 다만 `extends`에 문자열을 쓰는 방식은 `eslint-typescript-plugin`의 `config()`에서는 사용할 수 없으므로 아래와 같이 쓰면 된다. ```js [ // ...중략... { files: ["**/*.js"], extends: [js.configs.recommended] }, ] ``` 사실 `js.config.recommended`는 `{rules: […]}` 형태의 객체이므로 아래와 같이 Spread 연산자 `…`를 써도 된다. ```js [ // ...중략... { ...js.configs.recommended, files: ["**/*.js"] }, ] ``` 다만 규칙을 추가하거나 덮어쓰고 싶을 때 이런 식으로 번거로워질 수 있다. ```js { ...js.configs.recommended, rules: { ...js.configs.recommended.rules, "max-params": ["error", { "max": 5 }], } } ``` `extends`를 쓰면 아래처럼이 깔끔하게 된다. ```js { files: ["**/*.js"], extends: [js.configs.recommended], rules: { "max-params": ["error", { "max": 5 }], } } ``` ## eslint-typescript-plugin: strictTypeChecked `eslint-typescript-plugin`의 "Typed Linting" 기능을 써서 타입 시스템을 바짝 조여보자. `recommended` 대신 `strictTypeCheck` 설정을 쓰면 된다. 다만 이걸 쓰려면 아래처럼 `languageOptions`를 추가로 지정해줘야 한다. ```js { extends: [ts.configs.strictTypeChecked], languageOptions: { parserOptions: { projectService: true } }, rules: { '@typescript-eslint/restrict-template-expressions': 'off', "@typescript-eslint/switch-exhaustiveness-check": "error", } } ``` ## 최종본 최종본. AI 에이전트가 만드는 코드의 품질을 강제할 목적으로 좀 과하게 조였다. 다만, 린터만으로는 한계가 있고 다른 수단들이 더 필요하다. 예(`jscpd`: 중복 코드 감지; `dependency-cruise`: 모듈 간 의존 구조 강제; `knip`: 안쓰는 코드 감지. 자세한 내용은 [에이전트 기반 코딩 실험 3](https://wiki.g15e.com/pages/Agentic%20coding%20experiment%203.txt) 참고) ```js import js from "@eslint/js" import { globalIgnores } from "eslint/config" import importPlugin from "eslint-plugin-import" import jsdoc from "eslint-plugin-jsdoc" import sonarjs from "eslint-plugin-sonarjs" import ts from "typescript-eslint" // eslint.defineConfig() has a types incompatibility issue export default ts.config([ // global ignores globalIgnores([".cursor/", ".github/", "dist/", "coverage/", ".dependency-cruiser.cjs", "eslint.config.js"]), // check for typescript files { name: "s4/ts", files: ["src/**/*.ts"] }, // js/recommended with custom rules { name: "s4/js-recomm-mod", extends: [js.configs.recommended], rules: { "no-undef": "off", "max-params": ["error", { max: 5 }], "max-statements": ["error", { max: 15 }], }, }, // jsdoc { name: "s4/jsdoc-recomm-mod", extends: [jsdoc.configs["flat/recommended-error"]], rules: { "jsdoc/require-jsdoc": ["error", { publicOnly: true }], "jsdoc/require-param-type": "off", "jsdoc/require-returns-type": "off", "jsdoc/require-returns-check": "error", }, }, // import { name: "s4/import-recomm-mod", extends: [importPlugin.flatConfigs.recommended], ignores: ["eslint.config.js"], rules: { "import/max-dependencies": ["error", { max: 8, ignoreTypeImports: false }], }, }, // typescript-eslint { name: "s4/ts-strict-type-checked-mod", extends: [ts.configs.strictTypeChecked], languageOptions: { parserOptions: { projectService: true } }, rules: { "@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/switch-exhaustiveness-check": "error", "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/no-magic-numbers": [ "error", { ignore: [-2, -1, 0, 1, 2, 10, 16, 24, 32, 42, 60, 100, 255, 256, 512, 1024], ignoreEnums: true, ignoreNumericLiteralTypes: true, ignoreReadonlyClassProperties: true, ignoreTypeIndexes: true, }, ], }, }, // sonarjs { name: "s4/sonarjs-recomm-mod", extends: [sonarjs.configs.recommended], rules: { "sonarjs/todo-tag": "off", "sonarjs/pseudo-random": "off", "sonarjs/no-os-command-from-path": "off", "sonarjs/prefer-regexp-exec": "off", "sonarjs/cognitive-complexity": ["error", 6], "sonarjs/max-lines": ["error", { maximum: 200 }], "sonarjs/elseif-without-else": "error", "sonarjs/no-collapsible-if": "error", "sonarjs/no-inconsistent-returns": "error", "sonarjs/slow-regex": "off", "no-useless-escape": "off", "no-magic-numbers": "off", }, }, // tests (overrides previous rules) { name: "s4/test", files: ["src/**/*.test.ts"], rules: { "max-statements": ["error", { max: 20 }], "sonarjs/cognitive-complexity": ["error", 3], "sonarjs/max-lines": ["error", { maximum: 300 }], }, }, ]) ``` 몇 가지 재미난(?) 설정들: - 테스트 케이스는 좀 길어져도 괜찮지만 [인지복잡도](https://wiki.g15e.com/pages/Cognitive%20complexity.txt)는 프로덕션 코드에 비해 더 낮아야 함 - 전체 파일에 대해서는 `max-lines`를 제야하고, 개별 함수에 대해서는 `max-statements`를 제약