rebase 처음 쓴 날, 커밋 3개를 날렸다.
정확히는 날린 줄 알았다. 금요일 오후 4시, 3일간 작업한 피처 브랜치에서 git rebase main을 쳤는데 conflict가 터졌다. 패닉 상태에서 이것저것 누르다가 터미널에 내 커밋이 사라진 걸 봤다. git log를 쳐봤다. 없다. 진짜 없다. 심장이 떨어지는 느낌이었다. 슬랙에 "rebase 하다가 커밋이 날아갔는데요..."라고 보낸 게 그날 가장 부끄러운 순간이었다.
팀 리드가 와서 git reflog를 치더니 30초 만에 복구해줬다. 그런데 그 뒤로 6개월 동안 나는 rebase를 안 썼다. merge만 썼다. 안전하니까. 익숙하니까.
이 글은 그런 나 같은 사람을 위한 것이다. rebase가 뭔지 설명하는 글은 차고 넘친다. 나는 rebase를 왜 두려워했고, 어떻게 두려움을 없앴는지 쓰려고 한다.
merge만 쓰던 시절의 git log
merge만 쓰면 히스토리가 이렇게 된다.
$ git log --graph --oneline --all
* e7a1b3c (HEAD -> main) Merge branch 'feature/payment'
|\
| * d4f2a1e 결제 로직 수정
| * b3c8e7f 결제 페이지 UI
|/
* a1d9f3b Merge branch 'feature/cart'
|\
| * 9e7c2d4 장바구니 수량 변경
| * 7f1a8b3 장바구니 페이지 추가
|/
* 5c3d6e9 Merge branch 'feature/login'
|\
| * 3b8f1c7 로그인 에러 핸들링
| * 1a4d7e2 로그인 폼 구현
|/
* f9e2c5a 초기 설정보면 안다. 모든 피처가 갈라졌다 합쳐지는 모양이 반복된다. 피처 브랜치가 3개면 괜찮다. 근데 팀원 5명이 각각 브랜치 2~3개씩 돌리면? git log --graph를 치면 지하철 노선도가 나온다. 어떤 커밋이 어떤 맥락인지 따라가기가 어렵다.
rebase를 쓰면 이렇게 바뀐다.
$ git log --graph --oneline --all
* e7a1b3c (HEAD -> main) 결제 로직 수정
* d4f2a1e 결제 페이지 UI
* a1d9f3b 장바구니 수량 변경
* 9e7c2d4 장바구니 페이지 추가
* 5c3d6e9 로그인 에러 핸들링
* 3b8f1c7 로그인 폼 구현
* f9e2c5a 초기 설정일직선이다. 한눈에 읽힌다.
rebase가 하는 일
merge는 두 브랜치를 합치면서 "merge commit"을 만든다. 두 줄기를 하나로 묶는 매듭 같은 것이다.
rebase는 다르다. 내 브랜치의 커밋들을 떼어내서, 대상 브랜치의 최신 커밋 위에 하나씩 다시 쌓는다. 마치 내가 처음부터 최신 main 위에서 작업한 것처럼 만들어준다.
# feature 브랜치에서 작업 중
git checkout feature/payment
# main의 최신 커밋 위로 내 커밋들을 옮기기
git rebase main이게 전부다. 개념 자체는 단순하다. 문제는 conflict가 날 때다.
conflict, 그리고 패닉
merge conflict는 한 번에 다 터진다. 전부 고치고 커밋 하나 만들면 끝이다.
rebase conflict는 커밋 하나하나 적용하면서 터진다. 커밋이 5개면 최악의 경우 5번 conflict를 해결해야 한다. 처음 겪으면 "이게 언제 끝나지?"라는 공포가 온다.
내가 처음 rebase를 망친 날도 이 상황이었다. 두 번째 커밋에서 conflict가 터졌고, 해결하는 법을 몰랐다. git rebase --abort를 치면 된다는 걸 그때는 몰랐다.
# conflict가 터졌을 때 선택지
# 1. 포기하고 원래 상태로 돌아가기
git rebase --abort
# 2. conflict 해결 후 계속 진행
# 파일 수정 → git add →
git rebase --continue
# 3. 현재 커밋을 건너뛰기 (거의 안 씀)
git rebase --skip--abort만 알아도 rebase의 공포 절반은 사라진다. 뭘 해도 원래대로 돌아갈 수 있으니까.
reflog: 진짜 안전망
--abort가 안 먹히는 상황이 올 수도 있다. rebase를 끝까지 해버렸는데 결과가 이상할 때. 그때 쓰는 게 git reflog다.
$ git reflog
e7a1b3c (HEAD -> feature/payment) HEAD@{0}: rebase (finish): returning to refs/heads/feature/payment
d4f2a1e HEAD@{1}: rebase (pick): 결제 로직 수정
b3c8e7f HEAD@{2}: rebase (pick): 결제 페이지 UI
a1d9f3b HEAD@{3}: rebase (start): checkout main
9c4f7d2 HEAD@{4}: commit: 결제 로직 수정
8b3e6a1 HEAD@{5}: commit: 결제 페이지 UIreflog는 HEAD가 움직인 모든 기록을 보여준다. rebase 이전 상태가 HEAD@{4}와 HEAD@{5}에 그대로 있다. 여기로 돌아가면 된다.
# rebase 이전 상태로 돌아가기
git reset --hard HEAD@{4}이걸 처음 알았을 때의 안도감을 아직도 기억한다. Git은 뭔가를 진짜로 삭제하는 경우가 거의 없다. reflog에 90일간 보관된다. 90일이면 충분하다.
Julia Evans가 자기 블로그에서 이런 말을 한 적 있다. "Git에서 작업을 잃어버리는 건 생각보다 훨씬 어렵다." 나도 이제 동의한다.
interactive rebase: 진짜 강력한 부분
git rebase main은 단순히 커밋을 옮기는 것이다. git rebase -i는 커밋 히스토리를 편집하는 것이다.
우리 팀에서 내가 실제로 작업한 피처 브랜치의 커밋 로그를 보자.
$ git log --oneline
a7b3c1d WIP: 결제 페이지
f2e8d4a 아 이거 왜 안 되지
c9d1f7b 결제 API 연동
b4a6e2c 타입 에러 수정
d8f3a5e 결제 페이지 UI 완성
e1c7b9f lint 수정이런 히스토리를 main에 merge하면? 리뷰어가 "아 이거 왜 안 되지"라는 커밋 메시지를 영원히 보게 된다.
git rebase -i HEAD~6이러면 에디터가 열린다.
pick a7b3c1d WIP: 결제 페이지
pick f2e8d4a 아 이거 왜 안 되지
pick c9d1f7b 결제 API 연동
pick b4a6e2c 타입 에러 수정
pick d8f3a5e 결제 페이지 UI 완성
pick e1c7b9f lint 수정
각 줄 앞의 pick을 바꿀 수 있다.
pick d8f3a5e 결제 페이지 UI 완성
squash a7b3c1d WIP: 결제 페이지
squash f2e8d4a 아 이거 왜 안 되지
squash e1c7b9f lint 수정
pick c9d1f7b 결제 API 연동
squash b4a6e2c 타입 에러 수정
- pick: 이 커밋 유지
- squash: 이전 커밋에 합치기 (메시지 수정 가능)
- fixup: squash와 같지만 커밋 메시지 버림
- reword: 커밋 메시지만 수정
- drop: 커밋 삭제
- edit: 커밋 내용 수정 후 계속
저장하고 닫으면 커밋 메시지를 정리할 수 있는 에디터가 다시 뜬다. 결과는 깔끔한 2개 커밋이 된다.
$ git log --oneline
f4d2a8c 결제 API 연동 및 에러 처리
e9b7c3a 결제 페이지 UI 구현PR 리뷰어 입장에서 생각해보면, 6개 커밋을 하나하나 보는 것보다 논리적으로 정리된 2개 커밋을 보는 게 훨씬 낫다. 실제로 우리 팀에서 이 습관을 들인 후 코드 리뷰 속도가 눈에 띄게 빨라졌다. 측정한 건 아니다. 체감이다. 하지만 체감이 확실했다.
rebase 하면 안 되는 때
이게 중요하다. rebase는 커밋의 해시를 바꾼다. 같은 내용이어도 새로운 커밋이 된다는 뜻이다.
혼자 쓰는 브랜치에서는 아무 문제 없다. 하지만 다른 사람과 같이 쓰는 브랜치에서 rebase를 하면?
내가 rebase로 커밋 해시를 바꿨다. 팀원은 원래 해시를 기준으로 작업 중이다. 둘이 push하면 난리가 난다.
# 절대 하면 안 되는 것
git checkout main
git rebase some-branch
git push --force # main에 force push = 팀 전체 혼란규칙은 간단하다.
이미 push한 커밋, 다른 사람이 보고 있는 브랜치에서는 rebase 하지 않는다.
내 로컬 브랜치, 아직 push 안 한 커밋, 혹은 나만 쓰는 피처 브랜치에서만 rebase를 쓴다. 이 규칙만 지키면 사고가 안 난다.
우리 팀이 정한 rebase 컨벤션
처음에는 "rebase 쓸 사람은 쓰고, merge 쓸 사람은 쓰고" 식이었다. 당연히 히스토리가 엉망이 됐다. 2개월쯤 지나서 팀 미팅에서 컨벤션을 정했다.
1. 피처 브랜치는 PR 올리기 전에 main 위로 rebase한다.
git checkout feature/my-work
git fetch origin
git rebase origin/main
# conflict 해결 후
git push --force-with-lease--force-with-lease는 일반 --force보다 안전하다. 내가 마지막으로 fetch한 이후에 누군가 같은 브랜치에 push했다면 거부된다. 실수로 남의 작업을 덮어쓰는 걸 막아준다.
2. PR의 커밋은 interactive rebase로 정리한다.
"WIP", "fix typo", "lint 수정" 같은 커밋은 squash한다. PR 하나에 커밋 1~3개가 이상적이다. 커밋 하나하나가 "이것만 revert해도 말이 되는" 단위여야 한다.
3. main 브랜치에는 squash merge를 쓴다.
# GitHub PR 설정에서 "Squash and merge"를 기본으로
# 또는 CLI에서
gh pr merge --squash이러면 피처 브랜치의 커밋이 아무리 지저분해도 main에는 깔끔한 커밋 하나만 들어간다. interactive rebase를 아직 안 익힌 팀원도 main 히스토리를 어지럽히지 않게 된다.
이 세 규칙을 정한 뒤로 git log --oneline main을 치면 피처 단위로 깔끔하게 읽히는 히스토리가 나온다. 새로 입사한 사람이 "이 프로젝트 히스토리 깔끔하네요"라고 했을 때 좀 뿌듯했다.
실전에서 자주 쓰는 패턴 몇 가지
커밋 메시지 하나만 고치고 싶을 때:
# 마지막 커밋 메시지 수정
git commit --amend -m "새 메시지"
# 3개 전 커밋 메시지 수정
git rebase -i HEAD~3
# 해당 커밋의 pick을 reword로 바꾸기작업 중간에 main이 많이 앞서갔을 때:
git fetch origin
git rebase origin/main
# conflict가 많으면 포기하고 merge로 전환하는 것도 방법이다
git rebase --abort
git merge origin/main이게 중요한 포인트다. rebase가 무조건 좋은 건 아니다. conflict가 너무 많으면 merge가 나을 수 있다. 오래된 브랜치를 main에 rebase하면 모든 커밋에서 conflict가 터질 수 있다. 그럴 때는 merge 한 번이 훨씬 효율적이다.
방금 커밋한 걸 이전 커밋에 합치고 싶을 때:
# "oops" 수정을 직전 커밋에 합치기
git add .
git commit --fixup HEAD~1
git rebase -i --autosquash HEAD~3--fixup과 --autosquash 조합은 한번 익히면 매일 쓰게 된다. fixup 커밋을 만들면 interactive rebase에서 자동으로 해당 커밋 아래에 fixup으로 배치해준다.
두려움이 사라진 시점
돌이켜보면 전환점은 reflog를 직접 써서 복구에 성공한 날이었다.
rebase 중에 뭔가 꼬여서 히스토리가 이상해졌다. 예전 같으면 패닉에 빠져서 팀 리드를 불렀을 것이다. 대신 git reflog를 쳤다. rebase 시작 직전의 HEAD를 찾았다. git reset --hard로 돌아갔다. 다시 rebase를 쳤다. 이번엔 conflict를 차분하게 하나씩 해결했다.
5분이면 끝날 일이었다.
두려움의 대부분은 "되돌릴 수 없다"는 착각에서 온다. Git에서는 거의 모든 것을 되돌릴 수 있다. reflog가 그걸 보장한다. 그걸 직접 경험하고 나면, rebase는 그냥 도구 중 하나가 된다. 무서운 명령어가 아니라 편한 명령어가 된다.
지금 팀에서 rebase를 매일 쓴다. 특별한 일이 아니다. git fetch 하고 git rebase origin/main 치는 게 아침에 커피 내리는 것만큼 자연스럽다. 거기까지 오는 데 6개월 정도 걸렸다. 그리고 그 6개월 동안 실제로 작업을 잃어버린 적은 한 번도 없었다.
