본문으로 건너뛰기

마음 편한 코드 리뷰를 위한 선택: husky와 lint-staged 도입기

 · reading-time-plural · 

코드 리뷰 할 때마다 보이는 지켜지지 않은 컨벤션… 심지어 타입 오류로 빌드도 안되는 상태… 이를 해결하기 위한 huskylint-staged 도입 계기와 설정 방법을 소개합니다.

개요

익히 huskylint-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 를 하고 있을 수 있고 이는 전체적인 DXbuild 퍼포먼스 저하로 이뤄지는데 시스템적으로 개선이 필요했습니다.

Type Error!

TS + Next.js 을 사용했는데 npm run dev 으로 수행된 development 상태에서는 프로젝트의 특정 파일 중 타입 에러가 발생하더라도 내가 수정하고 있는 페이지에 대해서 문제가 없다면 화면에 잘 표시되었기에 마치 프로젝트가 문제가 없는 것처럼 보이는 문제가 있었습니다.

note

이것은 예시로 보고 있는 페이지에서도 타입 에러가 발생하더라도 npm run dev 에서는 정상적으로 보이지만 npm run buildtype error 이 발생하는 것을 볼 수 있습니다.

주로 리펙토링 혹은 타입을 확장하면서 많이 발생했는데 이런 경우 build 를 하면 Type error 으로 프로젝트가 망가지기 십상이었습니다.

애써 PR 이 올라와서 열심히 코드를 보고 있는데 수행이 되면 안 되는 코드들이 보이고, 그런 것을 놓치고 승인을 눌렀더니 build 에 실패해서 다시 코드가 Push 되고 리뷰어로써는 괴로운 순간이었습니다.

정적 검사를 도입하자

코드 리뷰 를 위해 PR (like. 코드 리뷰) 가 올라올 때마다 코드를 리뷰하는 것이 아닌 숨어있는 문제와 숨바꼭질 을 늘 하게 되니 리뷰의 피로도는 높았습니다.

이러하여 리뷰어 로써의 불편함도 개선하면서 프로젝트의 코드 품질과 일관성을 지키기 위해서 중간에 정적 검사 를 추가하기로 하였습니다.

개발 프로세스 중 어느 이벤트에 추가하느냐는 여러 방법이 있겠지만 프로세스 상 가장 검증이 필요한 git push 단계에 넣는 것이 좋다고 생각했고 이와 관련된 Node.js 계열의 라이브러리husky , lint-staged 를 사용하게 되었습니다.

husky와 lint-staged는?

왜 두 개나 사용해야 하나 싶은데 차이점은 huskygit 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 방법으로 설치

  1. 공식 문서에서 권장하는 방법인 Automatic 을 실행합니다.

  2. 그러면 아래와 같이 설치가 될 텐데 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"
    }
    }
  3. 또한, 샘플로 pre-commit 가 생기는 것을 볼 수 있습니다. pre-commit 이외도 다른 git hook 을 이용한 파일을 생성해서 hook 을 정의할 수 있습니다.

    .husky/pre-commit
    #!/usr/bin/env sh
    . "$(dirname -- "$0")/_/husky.sh"

    npm test
  4. 이외 _ 내부에 있는 husky.sh 를 보면 git hook과 관련된 것은 모두 husky 에서 사용할 수 있도록 Overwrite 하고 있는 것을 볼 수 있습니다.

pre-commit hook에 lint-staged 지정

아까 생성된 pre-commit 파일 하단에 스크립트 를 작성하면 commit 을 실행하기 전에 실행 때 원하는 작업을 Local 에서 수행할 수 있습니다.

여기서는 commitgit staging area 에 올라온 파일에 대해서만 특별한 작성을 하기 위해서 lint-staged 를 사용하기 때문에 아래와 같이 명시했습니다.

.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# `.lintstagedrc.js` 규칙에 맞게 검사
npx lint-staged

lint-staged 설치 및 지정

  1. lint-staged 를 설치합니다.

    npm install lint-staged --save-dev
  2. .lintstagedrc.js 을 생성하여 어떤 파일을 어떻게 체크할 것인지 정의합니다.

    .lintstagedrc.js
    module.exports = {
    // script 실행만을 위함이므로 기본 패키지 매니저인 npm으로 하는 것이 깔끔합니다
    "*": "npm run format",
    "*.@(js|jsx|ts|tsx)": "npm run lint",
    };

수동으로 lint-staged 를 수행하는 방법은 npx lint-staged 가 있습니다.

위에서 pre-commitnpx lint-staged 를 지정했기 때문에 commitnpx lint-staged 가 실행되어 스테이징 에만 있던 파일은 .lintstagedrc.js 에 정의된 대로 작업이 수행될 것입니다.

pre-push hook으로 type 검사 진행

왜, type 검사는 pre-push 에서 하는지 의문일 수 있습니다.

pre-commit 에서 하기에는 매번 commit 마다 확인하기에는 부담스러운 작업이고, lint-staged 에서 진행하기에는 스테이징 에만 올라온 파일만 검사하기 때문에 문제가 발생합니다.

lint-staged 에서 진행하면 아래의 문제가 발생하는데

  • 예시로 a.tsb.ts 의 파일을 import 합니다.
  • 여기서 a.ts 만 수정되어 스테이징 에 올라가면 어떻게 될까요?
  • 타입 검사는 에러가 발생합니다. 이유는 a.ts 만 검사를 하기 때문에 b.ts 가 없다고 말이죠

때문에 Push 으로 Remote Server 으로 보내기 전 pre-push 으로 검사하는 것이 깔끔합니다.

기호에 맞게 수정하면 되는데 아래와 같이 구성할 수 있습니다.

.husky/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 에 해당하는 파일만 옵션 으로 전달되어 검사하게 되어 스테이징 에 있는 파일만 지정하여 검사할 수 있는 것입니다.

.lintstagedrc.js
module.exports = {
"*.@(js|jsx|ts|tsx)": "eslint --fix",
};

Windows 에서 husky 생성한 경우 Linux 에서 동작하지 않는 문제

프로젝트 에서 타입 에러 가 발생하고 있는데 Push 가 된 것을 보고 Windows 에서 잘 되던 huskyWSL 에서 동작하지 않음을 감지했습니다.

아무 파일이나 수정하고 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 에서는 파일 실행 권한 이 없어서 발생한 문제이었는데 나름 여러 환경 에서 개발하다 만난 이슈 라서 신기했습니다.

마무리

이렇게 huskylint-staged 를 왜 도입했는지, 어떻게 설정하는지를 알아보았습니다.

코드의 규모가 커질수록 일관적 인 코드 품질은 유지보수 성 및 가독성 을 향상시킬 수 있습니다.

해당 페이지에서 소개한 도구를 사용해서 더 좋은 DX 으로 즐거운 개발을 해나가보세요!

읽어주셔서 감사합니다.