지난번에 작성한 Git 친해지기에 이어서, Git 관련 포스팅을 올리게 되었다.
그 이유는, 깃 특강이 이틀치가 아니라 총 사흘치였고,
이번에도 깃 특강 후기를 미션으로 받았기 때문이다.

물론 그렇다고 동일한 수업을 또 반복해서 하는 건 아니다.
지난 특강에서는 Git을 배우면서 사용했던 `git add`, `git commit`, `git push`, `git fetch`, `git merge`, `git pull`, `git rebase` 등등... 다양한 명령어들을 익히고 어떤 역할을 수행하는지 알아보는 시간과, 직접 깃 레포지토리를 만들어서 팀프로젝트 느낌을 주어서 깃의 워크플로우를 몸소 느끼는 과정이었다.
지난 시간에는 Git을 배워보고 조금 친숙해지는 시간이었다면, 이번 3일차 특강에서는 좀 더 협업에서 다루는 감각을 중심으로 배우게 된다.
깃을 쓴다는 것
이번 깃 특강을 듣는 때는, 이전에 들은 특강의 경험과 스켈레톤 프로젝트를 진행하면서 갖가지 경험치들이 쌓인 상태로 듣게 된다.
그래서 어느 정도 깃에 익숙해진 상태로 특강을 듣는 것이다.
하지만... 깃은 언제 어디서 사용하던 생각보다 어렵고 까다롭다.
팀 프로젝트를 할 때도 느꼈지만, 깃 관리가 생각보다 굉장히 까다로웠다.
분명히 내 로컬에서는 원격에서 `git pull` 한 후에 잘만 작동되는데, 다른 랩탑에서는 동작을 안한다던가.
아주 당연하게도 원격 레포지토리에서 가져올 때나 push 할 때 충돌이 발생한다던가.
커밋과 브랜치들이 서로 난장판으로 섞여버린다던가...

그렇다고 프로젝트를 할 때 깃을 안 쓸 수도 없다.
Git은 단순히 코드를 저장하는 도구가 아니라, 프로젝트의 시계열을 기록하고 관리하는 도구기 때문이다.
깃이 관리하는 것은 파일 그 자체라기보단, 어떤 시점에 어떤 상태였는가를 기록해주는 것이다.
커밋은 프로젝트의 특정 시점에서 찍힌 스냅샷이고, 브랜치는 그 커밋을 가리키는 포인터다.
그래서 깃을 이해할 때 중요한건 현재 HEAD가 어떤 커밋을 가리키고 있고, 어떤 브랜치에서 기록하고 있는지를 파악하는 눈이 필요하다.
이 감각이 없으면, 프로젝트 할 때 몇 시간을 깃과 씨름하면서 허비해야 한다.
내가 그랬다...

마찬가지로 깃이 없으면 누가 어제 뭘했고, 오늘 뭘 했는지 알 방법이 없다.
매일매일 스크럼을 진행한다고 하더라도, 실제 코드가 어떻게 반영되었고 어떤 것이 개선되었는지 알기 어렵다.
실제 코드가 바뀐 부분을 찾겠다고 내 코드와 상대가 보내준 코드를 대조하고 있는 것도 말이 안된다.
카톡으로 코드를 보내고 받고 하는 것도 이상하고 말이다.
그리고 깃이 없으면 누가 이상한 코드를 넣었는지 질타도 할 수 없다.

그러니 헷갈리는 것이 있다면 하나하나 다시 배우고, 다시 익혀가는 수 밖에 없다.
하지만 스켈레톤 프로젝트가 끝난 지금, 깃을 팀원들과 함께 사용할 기회가 거의 없다.
깃이 중요한건 맞지만, 우리가 깃만 배우러 온 것도 아니고 말이다.
그래서 커리큘럼 상 세번째 깃 특강을 팀 프로젝트 뒤에 둔 것이 아닐까.
다시 한 번 배우고 익히는 시간을 갖게 해주겠다는 의지가 느껴진다.
Rebase와 Merge
오전 시간에는 지난 특강에서 질문이 많았던 부분부터 복습 차원의 강의를 진행하셨다.
나 또한 충돌 처리에서 `git rebase` 와의 질긴 악연이 있었다.
그래서 이참에 한 번 제대로 정리해보려고 한다.
우리가 팀 프로젝트에서 가장 자주 만나는 것이 `merge` 그리고 `rebase`다.
둘 다 브랜치의 변경사항을 합쳐주는데 사용하는 명령어다.
하지만 두 명령어는 결과로 남는 "히스토리" 가 좀 다르다.
예를 들어 main 브랜치가 이런 상태였다고 해보자.
A---B
여기서 내가 `feature` 브랜치를 만들고 작업을 진행했다고 치자.
main A---B
\
feature D---E
그런데 내가 작업하는 사이에 다른 팀원이 main에 새로운 커밋을 추가했다.
다들 프로젝트에 열심히 참여하는 모습이다.
main A---B---C---F
\
feature D---E
이제 내 feature 브랜치를 최신 main과 합쳐야 한다.
이때 첫 번째 방법은 `git merge`다.
git fetch origin
git merge origin/main
그러면 Git은 두 흐름을 합치기 위한 merge commit을 만든다.
main A---B---C---F
\ \
feature D---E--- M ( 새로운거 )
이 방식은 히스토리가 정직하게 나온다.
“여기서 브랜치가 갈라졌고, 여기서 다시 합쳐졌다” 라는 사실이 그대로 남는다는 것이다.
그래서 뭔가 많은게 보이고 합쳐지고 하는 엄청난 그래프들을 볼 수 있다.
하지만 언제나 그렇듯, 너무 많은 것들이 쌓이면 오히려 눈에 잘 안들어온다...

작업 브랜치가 많고 merge commit이 계속 쌓이면, 나중에 히스토리를 읽을 때 흐름이 지저분하게 보일 수도 있다.
반대로 rebase는 접근 방식이 다르다.
git fetch origin
git rebase origin/main
rebase는 내 브랜치의 base를 최신 main으로 바꾼 뒤,
내 커밋을 그 위에 다시 쌓는다.
main A---B---C---F
\
feature D'---E'
여기서 중요한 점은 D와 E가 그대로 이동하는 것이 아니라는 점이다.
D', E' 처럼 새로운 커밋이 되어 쌓인다.
즉, rebase는 히스토리를 보기 좋게 바꿔버린다.
그러면서 커밋의 해시 값도 바꿔버린다.
커밋의 해시 값이 바뀌는 이유는, 커밋이 단순히 코드 변경 내용만 가지고 만들어지는게 아니라, 부모가 가진 커밋 정보들까지 함께 가져와서 해시를 결정하기 때문이라는데...
음. 어쨌든 rebase를 쓰면 커밋 히스토리를 깔끔하게 정리해줘서 Pull Request를 올릴 때 먼저 자잘한 커밋들을 정리하는데 사용할 수 있다. 너무 자잘한 커밋들을 그대로 PR에 올려버리면 리뷰하는 입장에서도 읽기 번거롭기도 하니 rebase를 사용해 커다란 feature 하나로 묶어서 보내주는 것이다.
대신 rebase는 커밋을 만들어서 교체해버리는 것이라서, 만약 다른 팀원이 내가 rebase한 브랜치를 사용하고 있었다면... 그때부턴 또 시간과 정신의 방에 들어가야한다.
그래서 `dev`나 `main`같은 공용 브랜치에서 rebase를 사용하는건 진짜로 조심하는게 좋다.
Rebase \ Merge 명령어 요약
| 상황 | 명령어 | 결과 | 주의점 |
| merge | git fetch origin git checkout feature/login git merge origin/main |
main 변경사항이 feature에 합쳐짐. merge commit이 생길 수 있음 | 히스토리가 분기/병합 형태로 남음 |
| rebase | git fetch origin git checkout feature/login git rebase origin/main |
feature의 base가 최신 main으로 바뀌고, 내 커밋이 그 위에 다시 쌓임 | 커밋 해시가 바뀜. 이후 push 시 force 계열 필요할 수 있음 |
| rebase충돌 해결 | git status# 파일 수정 git add <file> git rebase --continue |
충돌 해결 후 다음 커밋 재적용 진행 | 꼬였으면 git rebase --abort로 중단 가능 |
| 히스토리 정리 | git checkout feature/login git rebase -i HEAD~4 |
최근 4개 커밋의 순서 변경, squash, drop, reword, edit 가능 | 공유된 이력에 쓰면 협업자 히스토리가 꼬일 수 있음 |
| 커밋 합치기 | git rebase -i HEAD~4 pick ...squash ...squash ... |
여러 커밋을 하나의 의미 있는 커밋으로 정리 | squash 후 커밋 메시지 재정리 필요 |
| 커밋 제거 | git rebase -i HEAD~4 drop ... |
디버그 로그, 실수 커밋 등 제거 | 이미 공유된 커밋 삭제는 주의 |
| 커밋 메시지수정 | git rebase -i HEAD~3 reword ... |
커밋 내용은 그대로 두고 메시지만 수정 | 메시지 기준으로 히스토리 읽기 쉬워짐 |
| 커밋 손보기 | git rebase -i HEAD~3 edit ... git reset HEAD^ git add ... git commit -m "..." git rebase --continue |
하나의 큰 커밋을 쪼개거나 수정 가능 | 가장 강력하지만 초심자에겐 실수 포인트 많음 |
| 원격 브랜치 반영 | git push --force-with-lease origin feature/login | 원격 브랜치를 재작성된 내 이력으로 갱신 | --force보다 안전하지만, 공용 브랜치에서는 여전히 주의 |
| 강제 push | git push --force origin feature/login | 원격 이력을 무조건 덮어씀 | 다른 사람 커밋을 날릴 수 있어 위험 |
| safer force push | git push --force-with-lease origin feature/login | 원격이 내가 아는 상태일 때만 덮어씀 | 원격이 바뀌었으면 push 거부됨 |
Git reflog
이전까지 배웠던 것들을 외워서 쓴다고 하더라도 언젠가 실수를 하기 마련이다.
이유 없이 `git reset --hard` 을 해버린다거나...
`git rebase`를 하다가 중요한 커밋 내용을 잃어버릴수도 있다.
브랜치 가지치기를 하다가 중요한걸 끊어먹거나 할 수도 있다.
그러면 우리는 그 순간부터 눈물을 머금고 머릿속의 기억을 헤집어가면서 다시 코드를 작성해야만 한다.
하지만 마지막으로 하나의 기회가 남아있을 수 있다.
그것이 바로 `git reflog` 다.
`git reflog` 는 로컬 저장소에서 나의 HEAD가 이동했던 기록들을 보여준다.
abc1234 HEAD@{0}: commit: 로그인 기능 추가
def5678 HEAD@{1}: rebase finished: returning to main
ghi9101 HEAD@{2}: checkout: moving from feature/login to main
jkl1112 HEAD@{3}: commit: 회원가입 기능 추가
우리가 커밋을 잃어버린 것처럼 보여도, 만약 reflog에 기록이 남아있다면 복구할 가능성이 있다!
대신 reflog는 우리들의 로컬 컴퓨터에만 존재하기 때문에 원격 저장소에는 기록되지 않는다.
마찬가지로, 이것도 시간 제한이 있다...
일정 시간이 지나면 깃이 자동으로 잃어버린 커밋들의 흔적을 지워버린다.
그래도 rebase나 reset을 쓸 때 마지막 보험정도로 reflog를 사용할 수 있다는 걸 기억하자.

이정도가 우리가 Git을 다룰 때 알아야 하는 기본적인 명령어들이었다.
사실 이정도만 알아도 프로젝트를 진행하는데 큰 무리가 없을 것이다.
그저 정해진 규칙대로 적절하게 수행해주고, 문제가 발생하면 해결하는 작업의 반복일 뿐이다.
커밋을 할 때 포맷을 맞춘다거나, 테스트를 돌려본다거나.
커밋 메시지를 특정한 탬플릿을 사용한다거나, push할 때 빌드가 깨지지 않는다거나 체크를 해주면 된다.
그런데... 사실 명령어를 외우는 것과 마찬가지로 이런 템플릿과 규칙들을 전부 외우는 것도 생각보다 까다롭다.
어차피 동일한 작업을 반복해서 처리해야하는거라면, 자동화 할 수 있지 않을까?
그래서 오후 시간에는 자동화 툴, Git Hooks에 대해서 배우게 되었다.
Git Hooks
Git Hooks는 특정 Git 이벤트가 발생했을 때 자동으로 실행되도록 하는 스크립트다.
우리가 커밋을 수행할 때, 커밋 전에 해야 할 일들을 수행하는 일을 해준다거나
커밋 메시지 작성 후, 푸시를 하기 직전, 머지를 한 이후에 실행해주는 일 등등을 자동으로 수행해준다.
이걸 사용하면 굳이 템플릿을 찾아보고 규칙을 찾아서 하는 일련의 과정을 기억하지 않아도 된다!
그렇게 좀 더 코드의 아키텍쳐적인 부분이나 비즈니스 로직에 집중할 수 있게 도와준다.
#!/bin/sh
echo "Running pre-commit checks..."
flake8 .
if [ $? -ne 0 ]; then
echo "Lint failed. Commit aborted."
exit 1
fi
예를 들어서 `pre-commit` hook에서 이런 lint 검사를 실행할 수 있다.
코드 리뷰어의 입장에서 들여쓰기나 따옴표같은 규칙을 지적하는건 진짜 피곤한 일이다.
그래서 lint와 formatter를 자동으로 실행시키고, 통과되는 것들만 commit으로 넘겨주는 것이다.

Lint는 문법 오류나 스타일 규칙을 검사해주는 일종의 코드 검사기다. 많은 사람들이 사용하는 VSCode의 Extension으로 Prettier를 생각하면 된다.
Lint를 사용하면 사전에 프로젝트 팀끼리 얼만큼의 들여쓰기를 사용할 것인지와 같은 일관된 코드 작성을 도와준다.
이를 통해서 팀 코드의 스타일을 통일시켜주고, 잠재적 버그까지 잡아줄 수 있다.
이후 리뷰어의 입장에서는 정돈된 코드만 읽게 되므로 코드 리뷰 시간도 단축할 수 있다.
하지만 이런 Git Hooks는 깃의 버전 관리 대상이 아니다.
무슨 소리냐면, 내 로컬에서 hook에 대한 여러 스크립트들을 설정하더라도 다른 팀원에게 공유되지 않는다는 것이다.
그렇다고 팀원 하나하나에게 똑같은 `.git/hooks` 안의 스크립트를 복붙하는 괴상한 짓은 하고싶지 않겠지.
그래서 팀 프로젝트에서는 Husky같은 도구를 사용한다.
husky, lint-staged, prettier, eslint

세상에는 여러가지 종류의 Lint와 Formmater들이 있다.
보통 JavaScript를 다루는 웹개발 환경에는 Husky를 사용하고, Python 기반인 경우에는 Pre-commit을 사용한다고 한다.
그래서 우리는 허스키를 중심으로 배우게 되었다.
Husky는 Git Hook 설정을 프로젝트 코드처럼 관리하게 도와주는 npm 패키지다.
그래서 평범한 npm 패키지를 설치하는 것처럼 설치하면 된다.
npm install husky --save-dev
npx husky init
허스키를 설치하고 초기화하면, 디렉토리에 `./husky` 가 생긴다.
그리고 패키지들을 확인할 수 있는 `package.json` 에는 `prepare` 스크립트가 추가된다.
{
"scripts": {
"prepare": "husky"
}
}
이 .json 파일에 허스키를 사용한다고 명시해주면서 다른 팀원들도 프로젝트를 클론한 후에 `npm install` 하나로 허스키 설정을 준비할 수 있다.
이제 `lint-staged` 를 함께 사용한다.
`lint-staged`는 말 그대로 깃 상태인 staged 상태의 파일에 lint와 format을 적용해준다.
코드 포맷팅을 하겠다고 전체 코드를 매번 읽고 검사하고 고치는 것보단, 당장 커밋하려 하는 파일만 검사하는 역할이라고 보면 된다.
`eslint`는 문법 오류를 검사하고 기본적인 포맷팅을 담당해준다.
그리고 `prettier`는 포맷팅의 역할을 해준다.
npm install --save-dev lint-staged eslint prettier eslint-config-prettier eslint-plugin-prettier
# eslint-config-prettier → ESLint 규칙 중 Prettier와 충돌하는 걸 끔
# eslint-plugin-prettier → ESLint 안에서 Prettier를 실행해서 오류로 보여줌
npx eslint --init
해당 명령어로 각각의 패키지들을 설치한 후에, `package.json` 을 확인해보면...
{
"name": "husky-lintstaged-eslint-prettier-example",
"version": "1.0.0",
"scripts": {
"lint": "eslint . --ext .js,.jsx", // 직접 실행 용
"format": "prettier --write .", // 직접 실행 용
"prepare": "husky install" // clone 후 .husky 자동 세팅
},
"devDependencies": {
"eslint": "^8.58.0",
"prettier": "^3.3.1",
"husky": "^9.1.7",
"lint-staged": "^15.2.2",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3"
},
"lint-staged": { // staged 상태의 파일들만 빠르게 검사하는 용 - pre-commit에서 실행
"**/*.{js,jsx}": [
"eslint --fix",
"prettier --write"
],
"**/*.{html,css,json,md}": [
"prettier --write"
]
}
}
이렇게 `lint-staged` 부분을 확인할 수 있다.
앞으로 `lint-staged` 스크립트를 사용하게 되는 경우에, staged 상태의 파일들에 자동으로 lint와 format을 적용해주게 된다.
이 스크립트를 husky에서 pre-commit hook에 적용시키면 완성이다.
echo "npx lint-staged" > .husky/pre-commit
이 명령어는 허스키의 훅 중에 pre-commit을 생성해주는 것이다.
앞으로 우리가 커밋을 할 때마다 `lint-staged`를 자동으로 실행해준다.
마침내 자동화 하나를 완성했다.

그렇게 세 번째 깃 특강을 마칠 때, 비로소 나는 깃이 만만치 않다고 느끼게 되었다.
여러가지 패키지들을 조합해 자동화하는 일에 혓바닥을 한 번 담궜을 뿐인데 말이다.
깃 명령어들을 외우고 동작하는 과정을 배우는 것도 생각보다 고달팠던 경험이었지만, 이번 자동화 과정은 더 쉽지 않았던 것 같다.
사실 자동화라는 영역이 해당 기술의 성숙기에 해당하는 단계가 아닌가.
언제나 처음에는 사람이 직접 한땀한땀 써가면서 사용하지만,
결국 사람이 많아지고 프로젝트가 커질수록 이런 자동화 기술이 필요해질 수 밖에 없다.
그렇기 때문에 이번에 자동화를 한 번 맛보는 일이, 나중에 분명히 도움이 되지 않을까.
그래서 이번 기회에 어느 정도 깃에 익숙해지고, 좀 더 깃을 잘 사용해보는 단계에 들어선 거라고 말할 수 있겠다.
이전 스켈레톤 프로젝트를 진행할 때, 이런 자동화 설정도 함께 공유하면 생산성을 꽤 높일 수 있지 않았을까 생각이 든다.
아무래도 매번 커밋할 때나, 브랜치를 올릴 때나, 컨벤션을 지키지는걸 깜빡하곤 했었다.
물론 소규모 프로젝트고, 바로 앞에 팀원들이 있어서 그렇게 소통하는데 문제는 없었지만,
규모가 커진 프로젝트에서는 이런 것 또한 생각보다 거슬리는 실수인 것도 맞다.
실수를 사전에 차단해준다는 철학은... 뭐랄까 Java 언어의 철학과도 조금 비슷하지 않나 생각이 든다.
실수 차단. 자동화. 프로그래밍 철학의 궁극적인 아젠다가 아닐까.
앞선 선배 개발자들이 고민해 만든 산물에 감사함을 느낄 뿐이다.

'개발 > KB IT's Your Life 7기' 카테고리의 다른 글
| KB IT's Your Life 7기 - 코딩테스트 특강 (0) | 2026.05.17 |
|---|---|
| KB IT's Your Life 7기 - Java 프로그래밍 2 (0) | 2026.05.10 |
| KB IT's Your Life 7기 - Java 프로그래밍 (0) | 2026.04.26 |
| KB IT's Your Life 7기 - 스켈레톤 프로젝트 후기 (1) | 2026.04.19 |
| KB IT's Your Life 7기 - 프론트엔드 교육을 들으며 (0) | 2026.04.11 |