이번 글은 코드 린팅을 효율적으로 처리하기 위해 자동화하는 문제 해결을 담고 있습니다.
ESLint, Husky, lint-staged에 대해 다룹니다.
ESLint는 이제 프론트엔드 개발 과정에서 때려야 땔 수 없는 코드 린팅 도구이다. 자바스크립트 정적 코드 분석 도구로서 코드에서 오류나 버그를 찾아주고, 코딩 스타일도 정의해서 이를 유지하게 도와준다. 다양한 플러그인과 커스터마이징도 지원하는 이 강력한 도구를 자동화 측면에서 더 유용하게 쓰기 위한 경험을 이번 아티클에서 소개하고자 한다.
ESLint의 설정 파일(e.g. .eslintrc.json
)에서 원하는 룰을 정의하고 npx eslint src
라는 간단한 명령어만으로 src 파일 내부의 모든 코드에 대해 정의한 룰대로 코드 린팅을 할 수 있다.
이를 통해서 발견되는 error와 warning을 볼 수 있고, --fix
를 붙이면 간단한 수정으로 해결 가능한 문제를 해결해 준다.
예를 들어, 프로젝트에서 import 문을 정렬 및 분리하고 싶어서 eslint-plugin-import
플러그인 패키지를 설치하고 다음과 같이 ESLint 설정 파일에 import 관련 룰을 정의했다.
// .eslintrc.json
// 일부 생략
{
"plugins": ["import"],
"rules": {
"import/order": [
"error",
{
"groups": ["builtin", "external", "internal"],
// builtin : node.js 내장 모듈 (e.g. fs, path)
// external : node_modules에 설치된 외부 패키지 (e.g. react, axios)
// internal : 절대 경로 (e.g. @/const.ts)
"alphabetize": { "order": "asc", "caseInsensitive": true },
"newlines-between": "always"
}
]
}
}
그룹 내의 import 문들은 각기 뭉쳐있으며 그룹끼리는 한 줄 띄우고, import 문은 알파벳순으로 정렬하도록 했다.
이 룰대로 npx eslint src --fix
를 할 시, 룰대로 수정이 가능하다면 별도 에러 메시지도 뜨지 않고 알아서 수정해 준다.
터미널에 한 줄 입력한 것만으로 자동으로 파일을 수정해 주니 삶이 풍족해졌다.
하지만 매번 커밋 하기 전에 npx eslint src --fix
를 직접 실행하는 것은 비효율적이었다.
이에 대한 대응으로 vsc의 settings.json에서 아래처럼 설정을 해두면 개발하며 코드가 저장될 때, lint 룰대로 고쳐준다.
// settings.json
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always",
}
하지만 다른 개발자가 프로젝트에 참여했을 때를 가정하면, 이를 수동으로 세팅해야 했기에 좀 더 시스템화 하고 싶었다.
커밋 하기 전에 자동으로 린팅이 실행되고 커밋이 되면 좋을 것이라는 생각 아래, 찾아본 결과 pre-commit을 지원하는 서비스로 Husky와 pre-commit이 있었다.
결과적으로 나는 Husky를 선택했다.
Husky는 JS 환경에 최적화되어있고, JS 커뮤니티에서 활발하며 설정도 간단하기 때문에 현재 프로젝트에 더 적합하다고 판단했다.
Husky 도입 과정은 간단했다.
devDependencies에 깔아주고 package.json
의 scripts에 prepare 한 줄을 추가해 준다.
// package.json
"scripts": {
"prepare": "husky install",
"lint": "npx eslint src --fix"
}
이렇게 해야 npm install을 했을 때, husky도 설치되며 다른 개발자들이 별도의 처리 없이 husky의 기능을 사용할 수 있다.
.husky 폴더에 pre-commit 파일을 만들어주고, 실행할 동작인 npm run lint
를 넣어주며 마무리했다.
// .husky/pre-commit
npm run lint
이렇게 며칠을 써본 결과 여러 문제점이 느껴졌다.
첫 번째, 매번 전체 파일에 대해 린팅을 하다 보니 커밋 할 때마다 시간이 오래 걸린다는 것이었다. 대략 10초 정도가 걸렸으니 기존 즉각적인 커밋보다 상당히 답답했다.
두 번째, 완전한 자동화가 아니라는 점. staged 된 파일에 대해 --fix
를 처리하고 나면 해당 커밋에 그대로 반영되는 게 아니라 고쳐진 코드는 unstaged로 바뀌어버린다. 즉 바뀐 파일에 대해 다시 staging 해주고 커밋을 한 번 더 해줘야 했다. fix: lint
라는 커밋이 자꾸 추가되는 게 마음에 들지 않았다.
세 번째, 여러 파일을 수정하고 코드 별로 커밋을 분리하고 싶을 때가 있다. git add -p
를 통해서 별도로 staged 된 코드만 커밋 하고 싶은데, unstaged 코드에서 에러가 발생하면 pre-commit에서 강제 종료되어 커밋이 안된다는 점이 문제였다.
위의 문제들을 해결하고자 lint-staged라는 솔루션을 도입했다.
lint-staged는 커밋 되기 전 staged 상태의 코드만을 대상으로 린트 작업을 수행한다.
나는 js, jsx, ts, tsx 파일만을 대상으로 lint를 수행하도록 설정했다. 추가로 --debug
옵션을 추가하면 더욱 자세히 각 작업당 몇 ms를 소요했는지도 나오고, 최종 시간도 나오니 한 번씩 써보면 유용하다.
// package.json
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix"
]
}
lint-staged를 도입하면서 위의 문제점들 중 1, 2번은 해결할 수 있었다.
이젠 전체 파일이 아닌 staged 된 파일에 대해서만 fix 해주면 되니 속도가 빨라졌고 (staged만 --fix
하면 1000ms보다 적게 나온다)
두 번째 문제였던 반자동화도 lint-staged에선 해당 커밋에 바로 반영되어서 별도의 후처리가 없어졌기에 완전 자동화되었다.
세 번째 문제인 커밋 별 분리는 lint-staged만을 쓴다면 이 또한 간단히 해결된다. 하지만 staged 파일 이외의 다른 파일에서 발생한 에러는 볼 수 없게 된다.
이를 나는 좀 더 커스터마이징을 하고 싶었다.
pre-commit으로 전체 파일을 검사하되, 에러가 있어도 중단되지 않고 error와 warning은 출력하도록 하고, staged 된 코드에서 에러 발생 시 강제 종료되도록 하고 싶었다.
위의 요구사항을 처리하기 위해서 강제 종료 없이 전체 린팅 검사하는 스크립트를 하나 만들어서 Husky pre-commit에 넣어두었다. 이제 커밋을 시도하면 먼저 전체 코드에 대해 error와 warning을 출력하고 난 후 staged 된 코드들에 대해 린팅하며 fix 해줄 것이다.
// .husky/pre-commit
npm run lint-no-exit
npm run lint-staged
// package.json
"scripts": {
"lint-no-exit": "node scripts/lint-no-exit.js",
"lint-staged": "lint-staged",
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix"
]
}
// lint-no-exit.js
import { execSync } from "child_process";
try {
// --cache를 통해 린팅 속도 가속
execSync("npx eslint src --cache", { stdio: "inherit" });
} catch (error) {
console.log("전체 lint 검사에서 문제가 발견되었습니다. 이후 스테이징 된 코드를 검사합니다.");
}
process.exit(0);
lint-no-exit.js
의 전체 린팅에선 --fix
를 제거했다. 어차피 pre-commit의 다음 줄인 lint-staged 로직에서 --fix를 실행하기 때문이다.
또한 --cache를 통해서 전체 린팅 속도를 가속했다. --debug
옵션으로 확인해 본 결과, 기존 10초에서 cache 도입 후 3~4초로 전체 코드 린팅 시간을 단축했다. 바뀐 부분이 없다면 1초 내외로 끝나는 것도 확인할 수 있었다.
빌드 할 때는 여전히 cache 없이 전체 검사를 하기 때문에 vercel의 CI/CD에선 캐싱 없이 전체 코드에 대해 린팅을 수행하게 될 것이다.
위의 문제 해결 과정을 통해서
- 커밋 할 때마다 코드 린팅 수행을 자동화했으며
- 전체 코드 린팅과 staged 코드 린팅을 함께 수행할 수 있게 되었고
- 적절한 위치에
--fix
와--cache
옵션을 넣으면서 수행 시간도 줄일 수 있었다.
결과적으로 코드 린팅 자동화를 통해 개발 환경과 앞으로의 코드 품질을 개선했다고 생각한다. 이 글을 보는 독자분들에게도 코드 린팅 자동화에 대해 유용한 글이었길 바란다.
바쁘게 요구사항을 구현하다 보면 DX를 놓치는 경우가 많은데, 이렇게 한 번씩 개선할 때마다 신선하고 재밌는 것 같다. 다음 DX 개선 관련해서는 commitlint에 대해 알아보고자 한다.
위에서 언급한 파일들은 깃허브 링크에서 전체 코드를 확인할 수 있다.