프로젝트를 진행하다보면 고칠 수 있는 노란색 주의문구를 미쳐 발견못하고 커밋-푸시하거나 라인 정렬을 자주 까먹곤한다. 이러한 사소한 녀석들이 각 브랜치에 축척되고 브랜치를 변경해서 작업을 하게되다보면 종국엔 컨플릭트의 근원이 되기도 한다. 이러한 사소한 것들을 바로잡아줄 시스템을 고민하게 되었고 깃훅을 이용해 팀원 간 포맷팅이나 경고 에러잡기 같은 룰을 강제시킬 수 있는 방법을 알게되었다.
1. Githook 이란?
Git Hook은 Git의 특정 이벤트(예: commit, push, merge)가 발생할 때 자동으로 실행되는 스크립트이다. 이를 활용하면 CI/CD 없이도 개발자 로컬 환경에서 특정 룰을 강제하거나, 자동화된 워크플로우를 실행할 수 있다.
Hook 이름 | 실행시점 | 주요 목적 | 활용 예시 |
pre-commit | git commit 전에 실행 | 코드 품질 유지 | dart format . && dart analyze 실행하여 코드 스타일 및 버그 방지 |
prepare-commit-msg | 커밋 메시지 편집기 열리기 전에 실행 | 자동 메시지 생성 | JIRA 티켓 ID 자동 추가 ([JIRA-123] feat: add login UI) |
commit-msg | git commit 실행 후 메시지 검증 | 커밋 메시지 스타일 강제 | Conventional Commit (feat: fix: 등) 적용 검증 |
pre-rebase | git rebase 실행 전에 실행 | 리베이스 중 충돌 방지 | dart analyze 실행하여 문제 있는 커밋 방지 |
pre-merge-commit | git merge 실행 직전에 실행 | 머지 전 코드 검증 | Merge Conflict 자동 해결 또는 스타일 강제 |
pre-push | git push 실행 전에 실행 | CI/CD 사전 검증 | flutter test 실행 후 테스트 실패하면 push 차단 |
pre-receive | 원격 저장소에서 push 수신 시 실행 | 서버 사이드 검증 | main 브랜치에 force push 방지 |
update | 원격 저장소에서 브랜치 업데이트 전 실행 | 브랜치 정책 적용 | 특정 브랜치 보호 (develop 브랜치만 CI/CD 허용) |
post-receive | 원격 저장소에서 push 완료 후 실행 | CI/CD 트리거 | Jenkins/GitHub Actions 실행하여 배포 자동화 |
post-merge | git pull 후 실행 | 환경 설정 유지 | flutter pub get 자동 실행 |
이와 같이 생명주기 별로 별도의 명령어를 넣어줄 수가 있다. 구체적인 동작원리를 살펴보자면 먼저 git은 리눅스 기반으로 리눅스 파일시스템을 이용하고 있다. git의 실행파일은 로컬의 /usr/bin/git에 있고 우리가 명령어(커밋 푸시등)를 입력하면 해당 디렉토리 안의 실행파일이 실행된다.
이때 위와 같은 깃훅들은 로컬의 .git/hooks/ 디렉토리안의 실행파일로 저장하여 사용할 수 있다. 따라서 chomd 를 통해 해당 디렉토리 실행권한만 준다면 깃 명령 생명주기를 이용해서 내가 원하는 룰을 강제시킬 수 있다.
2. GitHook 프로젝트에 적용시켜보기
01. Git Hook 설정 스크립트
#!/bin/sh
echo "🔄 Setting up Git hooks for this project only..."
# 현재 프로젝트에만 Git Hooks 적용
cp -r .githooks/* .git/hooks/
chmod +x .git/hooks/*
echo "✅ Git hooks applied to this project only!"
위 스크립트는 해당 프로젝트 루트에 .githooks/ 폴더가 있고 안에 pre-commit 과 pre-push 실행파일을 저장해둔 상태에서 실행한다. 명령어를 찬찬히 살펴보면 echo로 명령어 시작되었음을 먼저 알려주고 프로젝트에 있는 실행파일을 내 로컬 깃 파일 시스템에 복사해준다. 그리고 chmod를 통해 실행권한을 부여해준다. 여기까지 하면 일단 셋업은 완료다! 마지막으로 echo를 통해 작업이 완료되었음을 알려준다.
이때 이런 궁금증이 들 수 있다.
💡 Githook 실행파일을 리눅스 파일시스템에 저장해두면 다른 프로젝트에서 커밋할 때도 명령어가 적용되어버리는거 아닐까?
나도 처음에 이런 생각이 들어서 망설였는데 파일 시스템 구조를 살펴보면
위와 같이 프로젝트별로 나뉘어서 실행파일이 저장된다. 따라서 명령어가 글로벌하게 적용되는 걱정은 따로 하지 않아도 괜찮다 :) (생각해보면 너무 당연하게 프로젝트 git init 하면 폴더생기는게 바로 우리가 사용하려는 폴더이다)
02. post-checkout 과 pre-commit 실행파일 작성하기
post-checkout 실행파일
#!/bin/sh
echo "🔄 Auto-applying Git hooks..."
sh setup-hooks.sh
pre-commit 실행파일
#!/bin/sh
echo "🔍 Running Dart format and analyze only on staged files..."
# 커밋하려는 파일 목록 가져오기
STAGED_FILES=$(git diff --cached --name-only --diff-filter=d | grep '\.dart$')
# 커밋할 Dart 파일이 없으면 종료
if [ -z "$STAGED_FILES" ]; then
echo "✅ No Dart files to check."
exit 0
fi
# 커밋할 파일에 대해 dart format 실행
echo "🔧 Formatting staged Dart files..."
echo "$STAGED_FILES" | xargs dart format -o write
# 커밋할 파일에 대해 dart analyze 실행
echo "🔍 Analyzing staged Dart files..."
echo "$STAGED_FILES" | xargs dart analyze
# 만약 `dart analyze`에서 오류가 발생하면 커밋 차단
if [ $? -ne 0 ]; then
echo "❌ Commit rejected: Fix issues before committing!"
exit 1
fi
echo "✅ All checks passed! Proceeding with commit."
먼저 post-checkout 실행파일부터 살펴보자면 단순히 이전에 살펴보았던 setup 명령어를 실행시켜줌을 알 수 있다. 사실 setup은 프로젝트 클론후 한 번 만 실행시켜주면 되는데 checkout할 때 한 번더 실행시켜주는 이유는 setup이 나중에 변경되었을 경우 따로 setup을 실행시켜주는걸 까먹으면 변경사항이 반영되지 않는 것을 방지하기위함이다. checkout 할 때 마다 다시 셋업 시켜줌으로써 최신 명령어를 반영해보았다.
다음으로 pre-commit 실행파일을 살펴보자. 사실상 이 파일이 핵심이라고 볼 수 있기 때문에 명령어 한 줄 한 줄을 자세히 보자면
git diff --cached --name-only --diff-filter=d
- git diff → 작업 디렉터리(Working Directory)와 Git 저장소의 차이(diff)를 보여주는 명령어이다.
- --cached → Staging Area(스테이징된 파일)에서 변경된 파일 목록만 가져온다.
- --name-only → 파일 경로만 출력.
- --diff-filter=d → 삭제된 파일(D)이 아닌 파일만 필터링.
(1) 파일을 수정
[Working Directory]
├── file.txt (수정됨)
(2) git add 실행 (Staging Area로 이동)
[Staging Area]
├── file.txt (추가됨)
(3) git commit 실행 (Repository에 저장됨)
[Repository]
├── file.txt (커밋됨)
(여기서 Staging이란 커밋하기 전에 변경된 파일을 임시로 저장하는 단계를 의미한다. 이곳에서 커밋할 파일을 미리 선택후 한꺼번에 커밋하게 된다.)
첫명령어를 통해 스테이징 된 파일(git add 된 파일)중 변경된 파일의 목록을 가져올 수 있게된다.
# 커밋할 Dart 파일이 없으면 종료
if [ -z "$STAGED_FILES" ]; then
echo "✅ No Dart files to check."
exit 0
fi
이전 명령어에서 STAGED_FILES 에 변경된 파일 목록을 넣었다면 두번째 명령어에서는 해당 변수가 비어있는 지 확인한다. -z는 변수가 비어있는지 확인하는 Bash 조건문으로 만약 비어있으면 엑싯으로 그냥 끝내고 없으면 fi를 통해 if문을 종료시키고 다음 명령어로 넘어간다.
# 커밋할 파일에 대해 dart format 실행
echo "🔧 Formatting staged Dart files..."
echo "$STAGED_FILES" | xargs dart format -o write
# 커밋할 파일에 대해 dart analyze 실행
echo "🔍 Analyzing staged Dart files..."
echo "$STAGED_FILES" | xargs dart analyze
# 만약 `dart analyze`에서 오류가 발생하면 커밋 차단
if [ $? -ne 0 ]; then
echo "❌ Commit rejected: Fix issues before committing!"
exit 1
fi
echo "✅ All checks passed! Proceeding with commit."
이제 스테이지파일 변수에 저장된 파일목록을 통해 dart format -o wirte 명령어(다트 코드스타일 맞게 파일 자동 정렬 명령어)를 실행시켜 커밋할 코드 정렬작업을 해준다. 이떄 명령어 전달은 xargs 를 활용한다.
다음으로 경고문구나 코드에러있는지 확인을 위해 dart analyze 를 실행시켜준다. 명령어 전달은 똑같이 xargs 를 활용한다. 다음 명령어에서 $? 의 의미는 이전 명령어의 종료 상태를 나타내는데 만약 이전명령어(dart analyze)에서 -ne 0 (실패) 한다면 exit 시키고 성공하면 fi를 통해 조건문을 종료시킨다.
03. restore-hooks 실행파일 작성하기
#!/bin/sh
echo "🛠 Restoring default Git hooks for this project..."
rm -rf .git/hooks
mkdir -p .git/hooks
echo "✅ Git hooks have been restored to default in this project."
다음으로 원상복귀 시키는 명령어이다. 프로젝트 파일을 지우면 그냥 바로 삭제되어서 딱히 필수적인 실행파일은 아니지만 혹시 만약의 상황을 대비해서 (진짜 너무 급해서 룰이고 나발이고 일단 커밋-푸시 해야할떄) 실행파일을 작성해준다. 명령어 자체는 간단한데 그냥 hooks 디렉토리를 지워주면 된다.
이렇게 명령어를 적용해주면 커밋하려는 파일에 무조건 dart format 과 dart analyze를 통과한 코드만 깃헙에 올라가게되고 그에따라 다트가 권장하는 사항을 강제적으로 따를 수 있게 된다.
개인적으로 이방법이 마음에 드는게 커밋 룰이 망가지는 걸 "원천적으로, 시스템적으로 막을 수 있다."는 것이다. 별도의 cicd 시스템을 구축할 필요도, 그에따른 비용지불 걱정할 필요도 없다는 것도 너무 좋은것 같다 :)
'Infra' 카테고리의 다른 글
EC2에 HTTPS 통신 붙이기 (0) | 2025.02.11 |
---|---|
Amazon Linux 에서 mysql 설치할 때 발생한 에러 트러블 슈팅 (GPG check FAILED) (0) | 2025.02.11 |
develop 브랜치와 feature 브랜치가 컨플릭트 났던 이슈 트러블 슈팅 (0) | 2025.02.05 |
서버를 옮겼더니 젠킨스 접속 속도가 느려진 이슈 트러블 슈팅 (0) | 2025.01.24 |
Jenkins가 Flutter 경로를 찾지 못해 발생한 이슈 트러블 슈팅 (0) | 2025.01.23 |