이번에 처음으로 Next.js 프로젝트 세팅하면서 husky를 다뤄보았다. 이번에 하면서 최신 자료들을 못찾아서 좀 어려웠어서... 미래의 나와 혹시 나처럼 헤메고 있을 누군가를 위해서 세팅 과정을 적어둘것이다.
나는 yarn create next app으로 미리 Next.js 프로젝트를 생성해두었다. TypeScript, ESLint, Tailwind CSS를 생성하면서 함께 설치했다. 그래서 Next.js 프로젝트를 설치하는건 따로 작성하지 않겠다.
사용된 버전들
{
"dependencies": {
"next": "14.2.2",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"eslint": "^8",
"eslint-config-next": "14.2.2",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"postcss": "^8",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
},
}
그래서 husky는 왜 쓴건데?
작업과정을 작성하기 전에 먼저 왜 husky를 사용하게 되었는지에 대해서 설명하자면 git hook에 대해서 알아야한다.
git hooks란 특정 git 이벤트 실행 전, 또는 후에 추가적인 작업을 자동적으로 수행하는 기능을 제공하는 도구이다. 클라이언트 훅과 서버 훅으로 나뉘고, 차이점은 다음과 같다.
- 클라이언트 훅 : 커밋, Merge가 발생하거나 push가 발생하기 전 클라이언트에서 실행하는 훅
- 서버 훅 : git repository로 push가 발생했을때 서버에서 실행하는 훅
나는 이번에 커밋 전과 커밋 메시지 설정, push 전에 하고싶은 것들이 있기때문에 클라이언트 훅을 사용할 것이다.
클라이언트 훅의 종류
분류 | 훅 | 설명 |
커밋 워크플로 훅 |
pre-commit | commit 을 실행하기 전에 실행 |
prepare-commit-msg | commit 메시지를 생성하고 편집기를 실행하기 전에 실행 | |
commit-msg | commit 메시지를 완성한 후 commit 을 최종 완료하기 전에 실행 | |
post-commit | commit 을 완료한 후 실행 | |
이메일 워크플로 훅 |
applypatch-msg | git am 명령 실행 시 가장 먼저 실행 |
pre-applypatch | patch 적용 후 실행하며, patch 를 중단시킬 수 있음 | |
post-applypatch | git am 명령에서 마지막으로 실행하며, patch 를 중단시킬 수 없음 | |
기타 훅 | pre-rebase | Rebase 하기 전에 실행 |
post-rewrite | git commit –amend, git rebase 와 같이 커밋을 변경하는 명령을 실행한 후 실행 | |
post-merge | Merge 가 끝나고 나서 실행 | |
pre-push | git push 명령 실행 시 동작하며 리모트 정보를 업데이트 하고 난 후 리모트로 데이터를 전송하기 전에 실행. push 를 중단시킬 수 있음 |
husky는 이러한 협업 환경에서 git hooks를 쉽게 공유할 수 있게 도와주는 NodeJS 기반 모듈이다.
내가 husky를 통해 작성하고 싶은 규칙들은 다음과 같다.
- 작성되는 모든 파일에 eslint, prettier 적용하기
- main, dev로 직접 push 방지하기
- 커밋에 원하는 이슈 번호 붙이기
1. ESLint & Prettier 설정
먼저 린터와 포맷터인 ESLint, Prettier를 설정해준다. 나처럼 Next.js 버전 14, 또는 11.0.0 이상의 버전을 설치했고, ESLint 설치에 Yes 했다면 추가로 설치하거나 설정파일인 .eslintrc.json을 따로 생성해주지 않아도 된다. 가장 처음으로 할것은 코드를 더 예쁘게 정리해줄 포매터인 Prettier를 설치해준다.
yarn add --dev prettier
ESLint도 코드 포맷팅을 해주고, Prettier도 코드 포맷팅을 해주기때문에 두개의 규칙이 겹치지 않도록 해야한다. 필요한 플러그인들을 추가로 설치해준다.
yarn add --dev eslint-plugin-prettier eslint-config-prettier
# eslint-config-prettier: Prettier와 충돌이 생길 수 있는 규칙을 꺼주는 플러그인
# eslint-plugin-prettier: eslint에 Prettier 포맷터 규칙을 추가해주는 플러그인
타입스크립트에 필요한 플러그인도 설치해준다.
yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
그 다음 Prettier 설정파일인 .prettierrc 를 생성해주고 내용을 작성해준다. 내용은 자기 취향껏 작성하면 된다. 나는 Next는 세미콜론을 사용하지 않는다고 해서 리액트 프로젝트에서 사용하던 코드에서 그것만 변경해주었다.
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"proseWrap": "preserve",
"endOfLine": "auto"
}
설치한 플러그인들과 Prettier가 ESLint에 적용될 수 있도록 설정파일인 .eslintrc.json에 필요한 규칙들을 추가해준다. 자세한 내용은 주석으로 작성했고, 주석이 없는 버전도 함께 작성한다.
// eslintrc.json
{
"parser": "@typescript-eslint/parser", // ts는 parsing이 필요하기때문에 사용
"plugins": ["@typescript-eslint", "prettier"], // 사용할 ESLint 플러그인. eslint-plugin-prettier가 사용되게 하기 위해 prettier를 추가해준다
"parserOptions": { // TypeScript 파일 분석에 필요한 옵션을 설정
"project": "./tsconfig.json" // project 옵션은 TypeScript 프로젝트의 설정 파일(tsconfig.json)을 지정하여 ESLint가 프로젝트의 구성을 이해하도록 함
},
"extends": [ // ESLint 구성을 확장. Next.js의 코어 웹 바이탈스 및 TypeScript 및 Prettier와 관련된 권장 규칙을 사용
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended", // @typescript-eslint를 적용하고 recommended 규칙으로 확장.
"plugin:prettier/recommended", // eslint-plugin-prettier + eslint-config-prettier 동시 적용.
],
"rules": { // ESLint 규칙을 설정
// 'React' must be in scope when using JSX 에러 해결 (Next.js)
"react/react-in-jsx-scope": "off",
// ts파일에서 tsx구문 허용 (Next.js)
"react/jsx-filename-extension": [1, { "extensions": [".ts", ".tsx"] }],
"no-unused-vars": "off", //타입스크립트 사용시 interface의 변수명을 eslint가 잡지 않도록 함.
"@typescript-eslint/no-unused-vars": "warn" //대신 사용하지 않는 변수는 @typescript/eslint를 통해 잡아줌.
}
}
// eslintrc.json
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier"],
"parserOptions": {
"project": "./tsconfig.json"
},
"extends": [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
"rules": {
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": [1, { "extensions": [".ts", ".tsx"] }],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "warn"
}
}
2. husky(공식문서 링크)
ESLint와 Prettier로 원하는 린터와 포맷터 규칙을 작성했으니, 이것을 프로젝트를 사용하는 모든 사람들에게 적용될 수 있도록 husky를 사용한다. husky는 프로젝트를 세팅하는 사람 1명만 처음에 이렇게 설치해주면 된다.
먼저 husky를 설치해주고, 한번 실행해서 내용을 초기화해준다.
yarn add --dev husky
npx husky install
프로젝트를 사용하는 모든 사람에게 적용되게 하기 위해 package.json에서 scripts를 수정해 yarn install이 될때 husky를 같이 설치할 수 있는 명령어를 추가로 작성한다.
"scripts": {
// ....
"prepare": "husky install",
},
3. lint-staged
husky만 사용해서 커밋 전 코드를 검사하게 할 경우, 프로젝트의 모든 코드를 검사하기 때문에 비효율적이다. 또 커밋 직전에 파일을 포맷팅하면 현재 스테이지된 파일이 수정된다. 따라서 포맷팅 된 수정본은 새 변경사항으로 등록되고, 커밋 되는 것은 포맷팅 전의 파일이다. 내가 원하는 것과는 정반대로 동작하게 된다... 그래서 lint-staged를 사용한다. lint-staged는 먼저 스테이지 된 파일들만 포맷팅 후 다시 스테이징할 수 있게 도와준다. 즉, 변경된 파일들만 린터와 포맷터를 적용할 수 있어서 훨씬 효율적이다.
lint-staged를 설치한다.
yarn add --dev lint-staged
원하는 파일에 린트를 적용할 수 있도록 package.json의 아래에 명령어를 추가한다. 나는 변경된 js, jsx, ts, tsx는 eslint와 prettier를 적용하고, md와 json에는 prettier만 적용되도록 했다. --cache 옵션도 추가해서 이전에 포맷팅/린팅을 진행한 파일을 캐시에 보관하고 별도의 변경사항이 없을 경우 포맷팅을 진행하지 않게 했다.
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --cache --fix",
"prettier --cache --write"
],
"*.{md,json}": [
"prettier --cache --write"
]
}
이때 eslint의 캐시 파일이 생성되기때문에 .gitignore에 내용을 추가해줘서 깃이 무시하게 한다.
.eslintcache
그리고 Next.js에서는 lint-stated를 적용할 경우 .lintstagedrc.js 파일을 만들어야한다고 알려준다. (공식문서 링크)
파일을 루트 폴더에 생성해주고 공식문서의 내용을 복사해서 추가해준다.
// .lintstagedrc.js
const path = require('path');
const buildEslintCommand = (filenames) =>
`next lint --fix --file ${filenames
.map((f) => path.relative(process.cwd(), f))
.join(' --file ')}`;
module.exports = {
'*.{js,jsx,ts,tsx}': [buildEslintCommand],
};
4. git hooks 적용
드디어 git hooks를 적용할 수 있다. husky에 hook을 추가하려면 터미널에서 아래와 같은 명령어로 간단하게 생성할 수 있다.
husky를 설치했다면 프로젝트 내에 /.husky/_ 폴더가 보일것이고 그 안에 여러가지 훅 파일들이 있을것이다. 파일을 옮기면 에러가 나니 반드시 아래처럼 생성해주어야한다! 파일이 생성되면 /.husky 폴더 바로 하위에서 확인할 수 있다.
echo "npm test" > .husky/hook이름
4-1. 작성되는 모든 파일에 ESLint, Prettier 적용하기
포맷터와 린터는 커밋 전에 실행되어야하니 pre-commit에 명령어를 추가해야한다. 명령어로 pre-commit 파일을 생성해준다.
echo "npm test" > .husky/pre-commit
pre-commit 파일에 커밋 전에 실행할 내용을 작성해주면 된다. 수정된 모든 파일에 lint-staged를 적용하기 위해 아래처럼 내용을 작성한다.
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
실제로 커밋을 해보면 아래처럼 린터와 포맷터가 적용되는걸 볼 수 있다.
4-2. main, dev 브랜치로 직접 push 방지하기
push 직전 실행해줘야하기 때문에 pre-push 파일을 생성해준다.
echo "npm test" > .husky/pre-push
생성된 파일에 내용을 추가해주면 된다. 나는 main과 dev, featuer 브랜치들을 사용할 예정이라 배포되는 브랜치인 main과 개발하는 모든 내용이 모이는 dev에는 직접 push 할 수 없도록 했다. 내용은 가비아 블로그를 참고했다.
#!/bin/sh
FORBIDDEN_HTTPS_URL="<https://github.com/moondrop0816/flip.git>" # 레포 https url
FORBIDDEN_SSH_URL="git@github.com:moondrop0816/flip.git" # 레포 ssh url
FORBIDDEN_REF_MAIN="refs/heads/main"
FORBIDDEN_REF_DEV="refs/heads/dev"
remote="$1"
url="$2"
if [ "$url" != "$FORBIDDEN_HTTPS_URL" -a "$url" != "$FORBIDDEN_SSH_URL" ]
then
echo "forked branch can push your commits"
exit 0 # Forked Project 에서는 제한하지 않음
fi
if read local_ref local_sha remote_ref remote_sha
then
echo "현재 푸쉬하는 브랜치는 $local_ref 내부입니다."
if [ "$remote_ref" == "$FORBIDDEN_REF_MAIN" ] || [ "$remote_ref" == "$FORBIDDEN_REF_DEV" ]
then
echo "DO NOT PUSH TO MAIN OR DEV"
exit 1 # 금지된 ref 로 push 를 실행하면 에러
fi
fi
exit 0
만약 내용을 무시하고 dev나 main에 직접 푸시하면 에러가 발생하고 push를 막아준다.
4-3. 커밋에 원하는 이슈 번호 붙이기
나는 브랜치를 feature/기능-#이슈번호 형태로 작성해서 브랜치의 이슈 번호를 커밋의 뒤에 붙여주려고 한다.
husky에서 prepare-commit-msg를 생성해준다.
echo "npm test" > .husky/prepare-commit-msg
그리고 필요한 내용을 파일에 작성해준다. 커밋에 이슈번호가 들어가기때문에 내용에 숫자가 있으면 에러가 나도록 했다. 어떤것이 이슈번호인지 구분하기 위해서이다.
#!/bin/bash
BRANCH_NAME=$(git symbolic-ref --short HEAD)
ISSUE_NUMBER_IN_BRANCH_NAME=$(echo $BRANCH_NAME | sed -n 's/^.*#\\([0-9]*\\)$/\\1/p')
COMMIT_MSG_FILE=$1
COMMIT_MSG_HEAD=$(head -n1 $COMMIT_MSG_FILE)
COMMIT_MSG_BODY=$(tail -n+2 $COMMIT_MSG_FILE)
# Error 1 : branch이름이 main 혹은 dev가 아닌데 github issue 번호가 없다면 에러 출력하고 종료
if [[ $BRANCH_NAME != "main" && $BRANCH_NAME != "dev" && $BRANCH_NAME != "deploy" && -z $ISSUE_NUMBER_IN_BRANCH_NAME ]]; then
echo "ERROR: Branch name must be 'main' or 'dev' or 'branch-name-#<issue number>'"
exit 1
fi
# Error 2 : 만약 Commit 전체 내용 중에 "#<number>"가 있다면 에러 출력하고 종료
if [[ $(grep -c '#[0-9]' $COMMIT_MSG_FILE) -gt 0 ]]; then
echo "ERROR: Commit message cannot contain '#<number>'"
exit 1
fi
# Success : commit message에 github issue 번호를 추가
echo "$COMMIT_MSG_HEAD #${ISSUE_NUMBER_IN_BRANCH_NAME}" > $COMMIT_MSG_FILE
if [[ -n $COMMIT_MSG_BODY ]]; then
echo "$COMMIT_MSG_BODY" >> $COMMIT_MSG_FILE
fi
내용을 저장하고 커밋을 하면 이렇게 브랜치 맨 뒤에 적어준 이슈 번호가 커밋 내용 뒤에 붙는걸 볼 수 있다!
최신 내용이 있는 글들을 찾지 못해서 엄청 헤메고 프로젝트 재설치하고..... 아무튼 길고 많이 괴로운 시간이었지만... 해두고 나니 굉장히 유용하다. 한번 익혀두었으니 다음 프로젝트때는 더 수월하게 할거같다.
참고한 글들
https://myeongjae.kim/blog/2019/02/02/prepare-commit-msg-hook-issue-number
https://velog.io/@xmun74/Next.js-TS에서-ESLint-Prettier-설정하기
https://velog.io/@rmaomina/prettier-eslint-settings
https://velog.io/@bohongu/husky-lint-staged-commitlint
https://velog.io/@jhsung23/git-husky로-git-hook-설정하기
https://library.gabia.com/contents/8492/
https://naamukim.tistory.com/18
'study > git' 카테고리의 다른 글
Git 명령어 정리 (0) | 2023.04.12 |
---|---|
[git] git commit message 작성법 (0) | 2023.03.01 |
[git] 원격 저장소의 파일 삭제하기 (0) | 2023.01.15 |