대상 독자
- 새로바뀐 eslint v9의 config(flat config)를 이해하고 싶으신 분
- eslint 설정 트러블슈팅을 위해 좀 더 자세한 이해가 필요하신 분
기존에 사용하던 .eslintrc.(json|js|yaml)
설정파일은 ESLint v9에서 deprecated 되었습니다.
eslint v9에서는 새롭게 flat config
시스템으로 탈바꿈했습니다.
이제 eslint의 설정은 루트폴더의 eslint.config.(js|mjs|cjs)
파일을 통해 가능합니다.
js, mjs, cjs의 차이는 단순 모듈시스템 차이이므로, 사실상 eslint.config.js하나로 단일화된 셈입니다.
원하면 config파일에 .(ts|mts|cts)를 이용해 타입스크립트도 적용가능하나, 아쉽게도 아직은 unstable flag를 붙여서 사용해야하므로 추천하지 않습니다.
이 글은 설정파일에 대한 이해를 돕기위한 글로,
ESLint v9 공식문서 여러 문서를 참고하면서 개발중에 많이 도움될 것 같은 부분위주로 정리하여 작성하였습니다.
설정파일의 초기설정은 수작업보다는 npm init @eslint/config@latest
자동화 명령을 이용하는 것을 추천합니다.
모듈시스템이므로,
export default [
{
rules: {
semi: "error",
"prefer-const": "error"
}
}
];
.mjs 확장자를 사용하면 module.exports
(CJS) 방식이 아닌
export export default
(ESM)를 사용할 수 있습니다.
flat config 파일은 plugins와 parsers를 JS Object로 표현합니다. 기존에는 문자열로 인식했었습니다.
따라서 이렇게 바뀝니다.
// .eslintrc.js
module.exports = {
plugins: ["jsdoc"],
// eslint.config.js
import jsdoc from "eslint-plugin-jsdoc";
export default [
{ plugins: { jsdoc: jsdoc } }
]
기존엔 eslint-plugin-
이라는 prefix를 머릿속으로 지우고 jsdoc
이라는 문자열을 적어줘야했던 반면,
이제 jsdoc이라는 플러그인을 import하여 JS Object로서 삽입해주고 있습니다.
실수 가능성이 훨씬 줄어드는 좋은 변화라고 생각합니다.
기존엔 모든 설정내용은 global파일에 적용되는 공통 설정이었고,
src/\*\*/*.js
처럼 특정 파일에만 설정을 적용하고 싶은 경우 아래와같이 override에 작성해주어야만 했습니다.
// .eslintrc.js
module.exports = {
// ...global config
overrides: [
{ files: ["src/**/*", "test/**/*"],
rules: { semi: ["warn", "always"] }
},
{ files:["test/**/*"],
rules: { "no-console": "off" }
}
]
};
// eslint.config.js
import js from "@eslint/js";
export default [
js.configs.recommended, // Recommended config applied to all files
{ files: ["src/**/*", "test/**/*"],
rules: { semi: ["warn", "always"] }
},
{ files:["test/**/*"],
rules: { "no-console": "off" }
}
];
형태자체는 비슷해 보이지만,
overrides 내에 쓰는 것보단 배열로 뺌으로써 depth를 하나 낮추고,
좀 더 복잡한 상황에선 얽히고 설키기 쉬운 기존 방식보다 flat 설정 방식이 보기 좋다고 생각됩니다.
// .eslintrc.js
module.exports = {
extends: ["eslint:recommended", "./custom-config.js", "eslint-config-my-config"],
rules: { semi: ["warn", "always"]},
}
// eslint.config.js
import js from "@eslint/js";
import customConfig from "./custom-config.js";
import myConfig from "eslint-config-my-config";
export default [
js.configs.recommended,
customConfig,
myConfig,
{ rules: { semi: ["warn", "always"]}}
];
내용자체는 비슷합니다만,
JS Object로 관리되어 단순 실수가 줄어들고
flat한 설정 덕분에 각 설정들이 서로 다른 배열엔트리에 들어가기때문에 여러 설정이 얽히는 상황에도 좀 덜 꼬이는 구조라 생각됩니다.
라이브러리에서 제공하는 추천 eslint config를 정확한 용어로는 Sharable Configuration Package라 합니다.
이 Sharable Config는 Config Object(Config)로 제공되기도하고, Config Objec의 배열(Config[])로 제공되기도 합니다.
라이브러리 사용시 recommended config를 가져와서 사용할 일이 많을 텐데,
라이브러리에따라 recommend가 configs(config의 배열)을 제공하기도하고, config(단일 config)를 제공하기도 하니 그에 맞춰 사용하면 됩니다.
// eslint.config.js
import js from "@eslint/js";
import customConfigs from "./custom-config.js";
import myConfig from "eslint-config-my-config";
export default [
js.configs.recommended,
...customConfigs, // 라이브러리가 config[]을 제공하는 경우
myConfig, // 라이브러리가 config를 제공하는 경우
{ rules: { semi: ["warn", "always"]}}
];
// eslint.config.js
import js from "@eslint/js";
import customConfigs from "./custom-config.js";
import myConfig from "eslint-config-my-config";
export default [
js.configs.recommended,
...customConfigs,
{...myConfig, files:['src/**/*.js']}, // glob패턴 매칭되는 경우만 검사
{ rules: { semi: ["warn", "always"]}}
];
files 내용을 덮어씌워 config마다 glob패턴을 적용해줄 수 있습니다.
configs의 경우는 아래와 같이 .map()을 활용하여 각 배열마다 files를 넣어주면됩니다.
// eslint.config.js
import js from "@eslint/js";
import customConfigs from "./custom-config.js";
import myConfig from "eslint-config-my-config";
export default [
js.configs.recommended,
...customConfigs.map(config=>({
...config,
files: ["**/src/*.js"], // 개별 config마다 glob 패턴추가
})),
myConfig
{ rules: { semi: ["warn", "always"]}}
];
name
: 설정 객체의 이름을 뜻합니다. optional입니다. config 라이브러리(Sharable Config Package) 제작자라면 넣어주는 게 좋습니다.
files
: 검사를 적용할 glob 패턴 배열을 넣습니다.
ignores
: 마찬가지로 검사에서 제외할 glob 패턴 배열입니다.
plugins
: Config 객체를 갖는 name-value 매핑입니다.
rules
: 설정 규칙을 담습니다.
settings
: 공통세팅(데어터값)을 갖는 name-value 매핑입니다.
그 외 languageOptions
, linterOptions
가 있습니다.
// eslint.config.js
import example from "eslint-plugin-example";
export default [
{
plugins: { example1: example },
rules: { "example1/rule1": "warn"}
}
];
plugins에 Config 객체를 mapping해주면, rules에서 사용할 수 있습니다.
example1이라는 이름으로 설정객체를 매핑해주었으니, rules에서 example1로 사용하고 있습니다.
rules를 설정하는 것은
1. 주석을 사용하는 방법
2. 설정파일을 사용하는 방법이 있습니다.
둘 모두 사용하였을 경우 주석이 항상 우선됩니다.
여기에서는 설정파일을 사용하는 것만 다루겠습니다.
export default [
{
rules: {
eqeqeq: "off",
"no-unused-vars": "error",
"prefer-const": ["error", { "ignoreReadBeforeAssign": true }]
}
}
];
rules 또한 룰이름
과 severities
의 name-value 페어입니다.
serverities에는 "off", "warn", "error"가 있습니다.
"error"로 설정하면 CI, PR머지 등의 상황에 검사가 가능하므로 공식문서에선 가능하면 대부분 "error"을 사용하도록 추천하고 있습니다. "warn"은 점진적으로 "error"를 켜고싶은 과도기에 사용하기 좋다고 소개하고 있습니다.
또 ["error", { "ignoreReadBeforeAssign": true }]
와 같이 규칙에서 선언해둔 option을 제공할 수도 있습니다.
Config 제작자에따라
option에는 object가 들어가기도하고, string이 들어가기도 합니다.
"semi":["error", "always"]
"semi":["error", {"omitLastInOneLineBlock": true}]
옵션은 들어가는 순서가 배열이라는 것이고,
string, object, array 등 다양한 자료형이 들어갈 수 있으며 이는 Config 제작자가 사전정의해둔 것을 따릅니다.
// 다양한 옵션 예시.
// "someRule": ["warn", 15]
// "someRule": ["warn", 7, { }]
// "someRule": ["error", 3, "on"]
export default [
{ rules: { semi: ["error", "never"]} },
{ rules: { semi: "warn" } }
];
기본적으로 같은 rule을 두번 작성하면 아래의 rule이 우선시되는데요.
만약 위와같이 하나는 배열로 string 옵션인 "never"까지 명시해준반면
하나는 "warn" severity만 명시해주었을 경우 어떻게될까요?
이럴 경우, ["warn", "never"]
로 작동하게 됩니다.
겹치는 것이 severity 뿐이므로 "error"만 "warn"으로 교체되는 것입니다.