pre-commit, lint-staged와 타입체크
타입 체크를 실행하는 단계에 대한 고민
Overview
보통 팀 컨벤션을 ESLint, Prettier를 통해서 정의한다.
이 도구들을 도입했더라도 강제적인 장치가 없다면 문제가 있는 코드들이 PR에 올라갈 수 있다.
이러한 최소한의 신뢰성을 위해서 husky
와 lint-staged
가 많은 곳에서 활용된다.
나도 마찬가지로 프로젝트들 모두 commit을 할 떄마다 lint-staged
를 실행하여 코드 품질 검사를 하고있다.
그런데 eslint
나 prettier
, stylelint
는 별 문제가 없었는데 typescript의 타입 체크가 한번씩 말썽이었다.
다음 작업으로 넘어가기 위해 이전 작업을 커밋해두려고 할 때였다.
stage 영역에 올라가지 않은 Modified 영역 코드에서 tsc
단계에 에러가 발생했다.
lint-staged
는 스테이징 영역에 있는 파일들에만 작업을 해주는게 아니었나?
pre-commit, husky 그리고 lint-staged
조금 성가신 문제였지만 타입스크립트 에러가 발생한 부분을 수정한 뒤 다시 커밋을 하여 문제를 넘길 수 있었다.
하지만 이유가 뭔지 궁금하고 해결책을 찾고자 husky
와 lint-staged
에 대해서 정리를 해보았다.
Husky, pre-commit
Git은 어떤 이벤트가 생겼을 때 자동으로 특정 스크립트를 실행하도록 할 수 있다.
기본 hook 디렉토리 위치는 .git/hooks
이고 ls ~/.git/hooks
를 입력하면 여러 샘플 스크립트를 확인 할 수 있다.
이 위치에 스크립트를 작성하여 커밋이나 다양한 git 이벤트들에 대하여 사전 작업을 설정 할 수 있다.
그런데 .git
디렉토리에 git hook 스크립트를 넣어도 git 작업 영역에 잡히지 않는다.
그럼 같은 git hook
설정을 어떻게 공유할까?
husky
는 바로 이 Git hook
을 적용시켜주는 라이브러리다.
즉, husky
는 git hook
스크립트를 .git/hook
의 외부 위치에서 사용할 수 있게 도와준다.
lint-staged
git으로 관리되는 디렉토리에 있는 모든 파일들은 관리 대상(Tracked)과 비관리 대상(Untracked) 두 가지 상태로 구분된다.
.gitignore
로 관리되는 파일들은 비관리 대상이다.
그리고 관리 대상은 또 Unmodified
, Modified
, Staged
의 세 가지 상태로 나뉜다.
lint-staged
는 정확하게 Tracked
파일들 중 Staged
상태의 파일들에 대해서 특정 작업을 실행시켜주는 역할을 한다.
husky와 lint-staged의 조합을 정리하자면
husky를 통해 pre-commit 단계에 lint-staged를 실행하여 사전 코드 검사를 진행한다.
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged
tsc가 정상 동작하지 않는 문제
tsc
를 통한 타입 검사의 경우에는 모듈 의존성을 확인한다.
예를 들어서 Children.tsx
를 import하고 있는 Parent.tsx
가 있을때, Parent.tsx
파일만 스테이징 영역에 올리면 문제가 발생한다.
그런데 내가 겪었던 문제는 스테이징의 파일들에 아무 문제 없고, modified 영역의 파일에 문제가 있을때에도 에러가 발생했다.
문제를 파헤쳐보자.
우선 husky
를 통한 pre-commit
단계에서 tsc
를 실행하고 있는지 확인해야한다.
만약 그렇다면 lint-staged
단계에 실행되지 않기 때문에 모든 파일에 대해 검사를 수행하게 된다.
따라서 스테이징 환경은 문제가 없더라도 타입 검사를 실패하게 된다.
이 작업들을 왜 하는지 다시 생각해보자. 코드 컨벤션을 포함하여 타입 체크도 완료된 최소한의 신뢰성을 보장하는 코드인지 commit 직전에 검사하는 장치가 필요해서이다. 그 대상은 커밋하려고 하는 코드이다.
내가 직면한 문제는 커밋하려고 하는 코드 검사 중에 modified 영역의 코드 영향을 받는다는 것이다.
lint-staged 단계에 tsc 사용하기
pre-push
가끔 불편한 이 문제를 그냥 감수하거나 타입 체크를 하지 않을 수 있다.
혹은 pre-commit
단계가 아니라 pre-push
단계에서만 tsc
를 실행하는 것도 하나의 방법이다.
local에서 Remote server로 보낼때만 확인해도 협업 시 코드 안정성을 챙길 수 있기 때문이다.
나는 평소 커밋 단위도 신경써서 하는 편이다.
커밋이 너무 많은 경우 interactive rebase
도 활용하여 조절하곤 한다.
특정 시점의 코드마저도 타입 검사에 문제가 없었으면 좋겠다.
따라서 pre-push
는 나쁘지 않은 방법이지만 뭔가 아쉬웠다.
stash 활용하기
git에는 pre hook
이나 submodule
처럼 특정 상황에 쓸만한 기능들이 정말 많다고 느꼈다.
이번 문제의 경우에도 나는 git의 기능을 통해서 해결할 수 있었다.
git을 사용하는 사람이라면 당연히 알고 있을 stash
를 활용하는 방법이다.
우선 lint-staged 단계 이전에 git stash -k
를 실행한다.
-k
옵션에 의해서 작업 영역의 modified 코드들만 스태시에 저장한다.
그러면 린트 단계에서는 정확하게 스테이징 된 파일들에 대해서만 tsc를 포함한 코드 검사를 수행할 수 있다.
이 방법을 적용한 설정 코드는 아래와 같다.
git stash -ku
yarn lint-staged
git stash pop
{
"scripts": {
"prepare": "husky install",
"format": "prettier --write",
"lint": "eslint --fix",
"type-check": "tsc --pretty --noEmit",
"lint:full-inspection": "next lint --fix"
},
"lint-staged": {
"*.{ts,tsx}": ["yarn type-check", "yarn lint"]
}
}
git stash -ku
의 k
옵션은 스테이징 영역의 변경 사항은 유지된 채로 작업 디렉토리의 변경 사항만을 스태시에 저장한다.
또한 u
옵션은 새로 추가된 파일도 스태시에 포함시키고 싶은 경우에 사용한다.
이후 lint 실행 단계에 정확히 staging 영역의 변경 사항들에게만 코드 검사가 적용된다.
추가 사항
lint-staged가 실패한 경우
stash
를 활용한 방법에도 문제가 있었다.
lint-staged 단계에서 문제가 발생하면 git stash pop은 실행되지 않아서 stash에 들어간 코드를 직접 꺼내야하는 불편함이 있다.
쉘 스크립트를 조금 더 개선시켰다.
#!/bin/sh
git stash -ku
lint-staged_result=$(yarn lint-staged)
if [ $? -ne 0 ]; then
echo "lint-staged failed."
exit 1
fi
git stash pop
yarn lint-staged
의 결과는 lint-staged_result에 저장되고, if
문의 조건 $? -ne 0
은 직전 명령의 종료 상태를 확인한다.
이처럼 lint-staged 결과에 상관없이 git stash pop
이 실행되도록 설정하여 직접 pop
을 하지 않아도 된다.