코드 리뷰 할 때마다 보이는 지켜지지 않은 컨벤션… 심지어 타입 오류로 빌드도 안되는 상태… 이를 해결하기 위한 husky 와 lint-staged 도입 계기와 설정 방법을 소개합니다.
개요
익히 husky 와 lint-staged 사용은 프로젝트의 코드 품질과 일관성을 유지하기 위해 사용되는 글이 많습니다.
물론 그것을 중점으로 도입되는 것은 맞지만 다른 관점으로 코드 리뷰어로써 도입하게 된 배경을 소개해 보려고 합니다.
도입 배경
당시 저는 TS + Next.js + MUI 를 이용한 프로모션 사이트 의 메인 코드 리뷰어 Role으로 활동 중이었는데 처음에는 큰 문제가 없었지만 점차 문제가 발생하기 시작했습니다.
나만 열심히 사용하는 Prettier , ESLint
친절히 개인이 사용하는 코드 Editer (like. IDE) 에 설정을 하고 쓰면 좋겠지만 그렇지 않은 경우가 꽤 있었습니다.
사실 prettier 까지는 포멧팅 정도이기에 그냥 넘어갈 수 있지만 (눈물을 흘리며 내가 npx prettier --write . 실행하기…)
ESLint 의 검사를 하지 않는 것은 문제가 발생했습니다.
개발을 위해 MUI 를 사용하고 있는데 import { Button } from '@mui/material'; 와 같이 최상위에서 가져오면 아직 사용하지 않을 Component 까지 번들링되어서 전체적인 DX 경험과 Build 시간이 길어지는 문제가 발생했습니다. (가뜩이나 MUI 느린데 말이죠!)
이를 위해서는 MUI 에서 가이드 하는 방법 중 하나로 import Button from '@mui/material/Button'; 와 같이 use path imports 를 해야 했습니다.
개발하다 보면 나도 모르는 사이에 최상위에서 import 를 하고 있을 수 있고 이는 전체적인 DX 와 build 퍼포먼스 저하로 이뤄지는데 시스템적으로 개선이 필요했습니다.
Type Error!
TS + Next.js 을 사용했는데 npm run dev 으로 수행된 development 상태에서는 프로젝트의 특정 파일 중 타입 에러가 발생하더라도 내가 수정하고 있는 페이지에 대해서 문제가 없다면 화면에 잘 표시되었기에 마치 프로젝트가 문제가 없는 것처럼 보이는 문제가 있었습니다.
note
이것은 예시로 보고 있는 페이지에서도 타입 에러가 발생하더라도 npm run dev 에서는 정상적으로 보이지만 npm run build 시 type error 이 발생하는 것을 볼 수 있습니다.
주로 리펙토링 혹은 타입을 확장하면서 많이 발생했는데 이런 경우 build 를 하면 Type error 으로 프로젝트가 망가지기 십상이었습니다.
애써 PR 이 올라와서 열심히 코드를 보고 있는데 수행이 되면 안 되는 코드들이 보이고, 그런 것을 놓치고 승인을 눌렀더니 build 에 실패해서 다시 코드가 Push 되고 리뷰어로써는 괴로운 순간이었습니다.
정적 검사를 도입하자
코드 리뷰 를 위해 PR (like. 코드 리뷰) 가 올라올 때마다 코드를 리뷰하는 것이 아닌 숨어있는 문제와 숨바꼭질 을 늘 하게 되니 리뷰의 피로도는 높았습니다.
이러하여 리뷰어 로써의 불편함도 개선하면서 프로젝트의 코드 품질과 일관성을 지키기 위해서 중간에 정적 검사 를 추가하기로 하였습니다.
개발 프로세스 중 어느 이벤트에 추가하느냐는 여러 방법이 있겠지만 프로세스 상 가장 검증이 필요한 git push 단계에 넣는 것이 좋다고 생각했고 이와 관련된 Node.js 계열의 라이브러리 로 husky , lint-staged 를 사용하게 되었습니다.
husky와 lint-staged는?
왜 두 개나 사용해야 하나 싶은데 차이점은 husky 는 git hook 을 이용해서 git 의 특정 단계에 대해서 원하는 스크립트 를 수행할 수 있도록 해주고 lint-staged 는 모든 파일에 대해서 정적 검사 를 하면 느리기에 git staging area 만 검사할 수 있도록 해주는 라이브러리입니다.
husky 는?
git push 단계에서 특정 스크립트 를 실행시켜서 작업하고 싶은 기능을 제공하는 것은 git hook 입니다.
근데 git hook 이 .git 안으로 들어가는 형태라서 열심히 hook 을 정의해도 .git 디렉터리는 버전 관리 대상 이 아니므로 공유가 어렵습니다.
그래서 husky 를 사용하여 손쉽게 공유 및 관리를 달성하는 것입니다.
husky 도 까보면 .git/hooks 에 있는 스크립트를 .husky 으로 돌리는 것이라고 합니다.
lint-staged 는?
모든 파일에 대해서 정적 검사 를 해도 좋은 경우와 그렇지 않은 경우가 있을 것입니다.
예를 들어 prettier 는 전체 프로젝트에 대해서 한번 진행이 되었다면 수정되는 파일에 대해서만 진행하는 것이 경제적입니다.
이제 이럴 때 lint-staged 을 이용해서 commit 하려고 하는 (수정이 된 파일) 에 대해서만 추가 작업을 진행할 수 있는 패키지입니다.
헷갈리면 안 될 것이 lint-staged 는 스테이징 영역에 있는 파일 리스트에 대해서 추가 작업을 정의할 수 있도록 해주는 도구 입니다.
스테이징 영역 으로 들어갔을 때 hook 으로 실행해 주는 것이 아니므로 왜 스테이징 으로 들어갔는데 실행 안되지? 라고 햇갈리지 않도록 합니다.
원리 및 적용 방법
시간에 지남에 따라 적용 방법은 바뀔 수 있기에 공식 문서 를 보는 것을 추천드립니다.
적용 방법은 심플하게 설명하고 적용하면서 알게 된 원리와 주의점에 대해서 설명을 해보겠습니다.
husky Automatic 방법으로 설치
공식 문서에서 권장하는 방법인 Automatic 을 실행합니다.
그러면 아래와 같이 설치가 될 텐데
prepare를 이용해서 프로젝트 설치 전husky내용을 설치하도록 하는 것을 볼 수 있습니다.package.json{
"scripts": {
"prepare": "husky install",
"format": "prettier --write",
"lint": "eslint --fix",
"type-check": "tsc --pretty --noEmit",
"lint:full-inspection": "next lint --fix"
},
"devDependencies": {
"husky": "^8.0.0"
}
}또한, 샘플로
pre-commit가 생기는 것을 볼 수 있습니다.pre-commit이외도 다른git hook을 이용한 파일을 생성해서hook을 정의할 수 있습니다..husky/pre-commit#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm test이외
_내부에 있는husky.sh를 보면 git hook과 관련된 것은 모두husky에서 사용할 수 있도록Overwrite하고 있는 것을 볼 수 있습니다.
pre-commit hook에 lint-staged 지정
아까 생성된 pre-commit 파일 하단에 스크립트 를 작성하면 commit 을 실행하기 전에 실행 때 원하는 작업을 Local 에서 수행할 수 있습니다.
여기서는 commit 시 git staging area 에 올라온 파일에 대해서만 특별한 작성을 하기 위해서 lint-staged 를 사용하기 때문에 아래와 같이 명시했습니다.
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# `.lintstagedrc.js` 규칙에 맞게 검사
npx lint-staged
lint-staged 설치 및 지정
lint-staged를 설치합니다.npm install lint-staged --save-dev.lintstagedrc.js을 생성하여 어떤 파일을 어떻게 체크할 것인지 정의합니다..lintstagedrc.jsmodule.exports = {
// script 실행만을 위함이므로 기본 패키지 매니저인 npm으로 하는 것이 깔끔합니다
"*": "npm run format",
"*.@(js|jsx|ts|tsx)": "npm run lint",
};
수동으로 lint-staged 를 수행하는 방법은 npx lint-staged 가 있습니다.
위에서 pre-commit 에 npx lint-staged 를 지정했기 때문에 commit 시 npx lint-staged 가 실행되어 스테이징 에만 있던 파일은 .lintstagedrc.js 에 정의된 대로 작업이 수행될 것입니다.
pre-push hook으로 type 검사 진행
왜, type 검사는 pre-push 에서 하는지 의문일 수 있습니다.
pre-commit 에서 하기에는 매번 commit 마다 확인하기에는 부담스러운 작업이고, lint-staged 에서 진행하기에는 스테이징 에만 올라온 파일만 검사하기 때문에 문제가 발생합니다.
lint-staged 에서 진행하면 아래의 문제가 발생하는데
- 예시로
a.ts는b.ts의 파일을import합니다. - 여기서
a.ts만 수정되어스테이징에 올라가면 어떻게 될까요? - 타입 검사는 에러가 발생합니다. 이유는
a.ts만 검사를 하기 때문에b.ts가 없다고 말이죠
때문에 Push 으로 Remote Server 으로 보내기 전 pre-push 으로 검사하는 것이 깔끔합니다.
기호에 맞게 수정하면 되는데 아래와 같이 구성할 수 있습니다.
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 타입 검사의 경우 모듈 의존성 때문에 모든 파일을 대상으로 진행되어야 합니다. 때문에 `lint-staged` 으로 타입 검사 시 스테이징에 올라온 파일만 검사하다 에러가 발생하므로 push 이전에 확인하도록 합니다.
npm run type-check
# next에서 검사하는 lint도 `pages` 디렉터리가 있냐 없냐와 같은 전체 파일을 대상으로 검사하기 때문에 `lint-staged` 으로 검사가 어려워 push 이전에 확인하도록 합니다.
npm run lint:full-inspection
Insight
lint-staged 에서 Prettier 이나 ESLint 실행 시 . 으로 전체 파일에 대한 경로를 지정하면 의미가 없습니다.
lint-staged 를 사용하면서 가장 중요한 부분이라고 생각되는데 ESLint 를 예로 든다면 eslint --fix . 시 .eslintignore 를 제외한 파일에 대해 모두 검사한다는 것 입니다.
우리가 lint-staged 를 사용해서 얻는 이점은 스테이징 에 올라온 파일만 검사하는 것인데 위와 같이 아예 . 을 옵션으로 넘겨주는 순간 의미가 없어집니다.
고로 아래와 같이 정의하면 js|jsx|ts|tsx 에 해당하는 파일만 옵션 으로 전달되어 검사하게 되어 스테이징 에 있는 파일만 지정하여 검사할 수 있는 것입니다.
module.exports = {
"*.@(js|jsx|ts|tsx)": "eslint --fix",
};
Windows 에서 husky 생성한 경우 Linux 에서 동작하지 않는 문제
프로젝트 에서 타입 에러 가 발생하고 있는데 Push 가 된 것을 보고 Windows 에서 잘 되던 husky 가 WSL 에서 동작하지 않음을 감지했습니다.
아무 파일이나 수정하고 coomit 하니까 아래와 같은 메시지가 출력되었는데 이는 Push 도 마찬가지로 git hook 이 동작하지 않는 것을 볼 수 있었습니다.
hint: The '.husky/pre-push' hook was ignored because it's not set as executable.
hint: You can disable this warning with `git config advice.ignoredHook false`.
저와 같은 고민을 하는 이슈 ‣ 를 보고 Stackoverflow 를 통해서 아래의 명령어를 통해 권한을 줘서 문제를 해결했습니다.
chmod ug+x .husky/*
husky init을 Windows 에서 해서 Linux 에서는 파일 실행 권한 이 없어서 발생한 문제이었는데 나름 여러 환경 에서 개발하다 만난 이슈 라서 신기했습니다.
마무리
이렇게 husky 와 lint-staged 를 왜 도입했는지, 어떻게 설정하는지를 알아보았습니다.
코드의 규모가 커질수록 일관적 인 코드 품질은 유지보수 성 및 가독성 을 향상시킬 수 있습니다.
해당 페이지에서 소개한 도구를 사용해서 더 좋은 DX 으로 즐거운 개발을 해나가보세요!
읽어주셔서 감사합니다.