Posts
pre-commit, lint-staged와 타입체크

pre-commit, lint-staged와 타입체크

타입 체크를 실행하는 단계에 대한 고민

Overview

보통 팀 컨벤션을 ESLint, Prettier를 통해서 정의한다. 이 도구들을 도입했더라도 강제적인 장치가 없다면 문제가 있는 코드들이 PR에 올라갈 수 있다. 이러한 최소한의 신뢰성을 위해서 huskylint-staged가 많은 곳에서 활용된다.

나도 마찬가지로 프로젝트들 모두 commit을 할 떄마다 lint-staged를 실행하여 코드 품질 검사를 하고있다. 그런데 eslintprettier, stylelint는 별 문제가 없었는데 typescript의 타입 체크가 한번씩 말썽이었다. 다음 작업으로 넘어가기 위해 이전 작업을 커밋해두려고 할 때였다. stage 영역에 올라가지 않은 Modified 영역 코드에서 tsc 단계에 에러가 발생했다. lint-staged는 스테이징 영역에 있는 파일들에만 작업을 해주는게 아니었나?

pre-commit, husky 그리고 lint-staged

조금 성가신 문제였지만 타입스크립트 에러가 발생한 부분을 수정한 뒤 다시 커밋을 하여 문제를 넘길 수 있었다. 하지만 이유가 뭔지 궁금하고 해결책을 찾고자 huskylint-staged에 대해서 정리를 해보았다.

Husky, pre-commit

Git은 어떤 이벤트가 생겼을 때 자동으로 특정 스크립트를 실행하도록 할 수 있다. 기본 hook 디렉토리 위치는 .git/hooks이고 ls ~/.git/hooks를 입력하면 여러 샘플 스크립트를 확인 할 수 있다. 이 위치에 스크립트를 작성하여 커밋이나 다양한 git 이벤트들에 대하여 사전 작업을 설정 할 수 있다.

그런데 .git 디렉토리에 git hook 스크립트를 넣어도 git 작업 영역에 잡히지 않는다. 그럼 같은 git hook 설정을 어떻게 공유할까? husky는 바로 이 Git hook을 적용시켜주는 라이브러리다.

즉, huskygit hook 스크립트를 .git/hook의 외부 위치에서 사용할 수 있게 도와준다.

lint-staged

git으로 관리되는 디렉토리에 있는 모든 파일들은 관리 대상(Tracked)과 비관리 대상(Untracked) 두 가지 상태로 구분된다. .gitignore로 관리되는 파일들은 비관리 대상이다. 그리고 관리 대상은 또 Unmodified, Modified, Staged의 세 가지 상태로 나뉜다.

lint-staged는 정확하게 Tracked 파일들 중 Staged 상태의 파일들에 대해서 특정 작업을 실행시켜주는 역할을 한다.

husky와 lint-staged의 조합을 정리하자면

husky를 통해 pre-commit 단계에 lint-staged를 실행하여 사전 코드 검사를 진행한다.

.husky/pre-commit
#!/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를 포함한 코드 검사를 수행할 수 있다.

이 방법을 적용한 설정 코드는 아래와 같다.

.husky/pre-commit
git stash -ku
yarn lint-staged
git stash pop
package.json
{
  "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 -kuk 옵션은 스테이징 영역의 변경 사항은 유지된 채로 작업 디렉토리의 변경 사항만을 스태시에 저장한다. 또한 u 옵션은 새로 추가된 파일도 스태시에 포함시키고 싶은 경우에 사용한다. 이후 lint 실행 단계에 정확히 staging 영역의 변경 사항들에게만 코드 검사가 적용된다.

추가 사항

lint-staged가 실패한 경우

stash를 활용한 방법에도 문제가 있었다. lint-staged 단계에서 문제가 발생하면 git stash pop은 실행되지 않아서 stash에 들어간 코드를 직접 꺼내야하는 불편함이 있다. 쉘 스크립트를 조금 더 개선시켰다.

.husky/pre-commit
#!/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을 하지 않아도 된다.