리베이스란 무엇인가
리베이스(rebase)의 이름은 "base를 다시(re) 설정한다"는 뜻이다. 브랜치가 어디서 갈라졌는지—그 분기점(base)—를 변경하는 작업이다.
merge가 두 브랜치의 변경 사항을 "합류점"으로 통합한다면, rebase는 한쪽 브랜치의 커밋들을 다른 브랜치의 끝으로 "이식"한다. 결과적으로 히스토리가 선형처럼 보인다.
중요한 점은 리베이스가 기존 커밋 오브젝트를 물리적으로 이동시키지 않는다는 것이다. 1장에서 배웠듯 커밋은 불변이다. 리베이스는 같은 변경 내역(diff)을 새로운 위치에 재적용해 SHA가 완전히 다른 새 커밋 오브젝트를 생성한다. 원래 커밋들은 저장소에 남아 있지만 아무 브랜치도 가리키지 않으므로 결국 가비지 컬렉션으로 수거된다.
리베이스의 단계별 동작
git rebase main을 feature 브랜치에서 실행할 때 Git 내부에서 일어나는 일을 단계별로 추적해 보자.
시작 상황: main이 커밋 M2를 가리키고, feature가 M1에서 갈라져 F1→F2 두 커밋을 가지고 있다. M1이 공통 조상이다.
1. 공통 조상(M1) 찾기 2. feature 브랜치에서 공통 조상 이후의 커밋 목록 수집 → [F1, F2] 3. HEAD를 목표 브랜치(main) 끝인 M2로 이동 (Detached HEAD) 4. 수집된 각 커밋을 순서대로 cherry-pick a. F1의 변경 내역을 M2 위에 재적용 → 새 커밋 F1' 생성 (새 SHA) b. F2의 변경 내역을 F1' 위에 재적용 → 새 커밋 F2' 생성 (새 SHA) 5. feature 브랜치 포인터를 F2'으로 갱신 6. HEAD를 feature로 다시 붙임 (reattach)
각 단계에서 충돌이 발생할 수 있다. 충돌이 나면 리베이스가 해당 커밋에서 멈추고 사용자에게 해결을 요청한다. 충돌 해소 후 git rebase --continue로 진행하거나, git rebase --abort로 전체 작업을 취소할 수 있다.
리베이스 시각화
황금 규칙 — 공유된 히스토리는 절대 리베이스하지 않는다
리베이스에는 강력한 금기가 있다. 이미 원격에 push되어 다른 사람이 기반으로 작업하고 있는 커밋을 리베이스하면 팀 전체의 히스토리가 망가진다.
원인은 간단하다. 리베이스는 새 SHA의 커밋을 만든다. 동료 A가 로컬에 보유한 F1, F2(구 SHA)와 당신이 원격에 push한 F1', F2'(새 SHA)는 Git 입장에서 완전히 다른 커밋이다. 동료 A가 git pull을 하면 Git은 두 히스토리를 병합하려 하고, 결과적으로 F1과 F1'이 모두 히스토리에 나타나는 중복 커밋 지옥이 펼쳐진다.
안전한 리베이스의 범위: 아직 push하지 않은 로컬 전용 커밋, 또는 혼자 사용하는 개인 브랜치(다른 사람이 기반으로 작업하지 않음이 확실한 경우)에만 리베이스를 적용해야 한다.
팀이 공유하는 main, develop 같은 브랜치, 또는 PR을 열어 리뷰어가 이미 checkout한 브랜치는 리베이스 금지 구역으로 간주하는 것이 안전하다.
# 안전: 로컬 전용 브랜치를 main 위로 리베이스 git switch feature-local git rebase main # 위험: 이미 push된 브랜치를 리베이스 후 force push git rebase main # SHA가 바뀜 git push --force origin feature # ← 협업 중이라면 하지 마라 # 개인 PR 브랜치를 최신 main으로 업데이트 (혼자 쓴다면 허용) git fetch origin git rebase origin/main
대화형 리베이스 — 히스토리 조각하기
git rebase -i(interactive rebase)는 커밋을 단순히 이식하는 것을 넘어, 커밋 순서 변경, 합치기, 분리, 메시지 수정, 삭제까지 가능한 히스토리 편집 도구다.
git rebase -i HEAD~3을 실행하면 최근 3개 커밋을 편집할 수 있는 텍스트 파일이 열린다. 각 커밋 앞의 명령어를 바꾸어 동작을 지정한다.
pick a1b2c3 feat: add login form # 그대로 사용 reword a1b2c3 feat: add login form # 메시지만 수정 edit a1b2c3 feat: add login form # 멈추고 커밋 수정 squash a1b2c3 feat: add login form # 이전 커밋에 합치기 (메시지 합침) fixup a1b2c3 feat: add login form # 이전 커밋에 합치기 (메시지 버림) drop a1b2c3 feat: add login form # 이 커밋 삭제 # 순서를 바꾸면 커밋 순서가 변경됨
실무에서 가장 흔하게 쓰이는 시나리오는 squash다. 기능 브랜치에서 "wip", "typo 수정", "리뷰 반영" 같은 작은 커밋들을 쌓다가, PR 병합 전에 논리적으로 하나인 작업을 하나의 커밋으로 정리하는 것이다. 메인 브랜치의 히스토리가 한결 읽기 좋아진다.
edit을 사용하면 하나의 커밋을 두 개로 분리할 수도 있다. 리베이스가 해당 커밋에서 멈추면 git reset HEAD~로 커밋을 스테이징으로 되돌리고, 원하는 단위로 나누어 새로 커밋한 뒤 git rebase --continue한다.
리베이스 vs 머지 — 언제 무엇을 쓸까
두 방식은 서로 다른 철학을 반영한다. "히스토리는 실제 작업 기록이어야 한다"는 관점과 "히스토리는 독자에게 이해하기 쉬운 이야기여야 한다"는 관점의 차이다.
merge 선호 상황: 팀 단위 협업 브랜치, PR 병합, 브랜치의 존재와 병합 시점을 기록으로 남겨야 하는 경우, 병합 커밋을 통해 전체 기능을 하나의 단위로 revert할 수 있어야 하는 경우.
rebase 선호 상황: 로컬 작업 중 메인 브랜치의 최신 변경을 내 브랜치에 반영하고 싶을 때(merge 대신 rebase를 쓰면 불필요한 merge commit 생성을 방지), PR 제출 전 커밋 정리, 개인 브랜치의 히스토리를 읽기 좋게 선형화할 때.
많은 팀이 "기능 개발 중에는 rebase로 최신 main을 따라가고, PR 병합 시에는 --no-ff merge로 병합 커밋을 남긴다"는 혼합 전략을 채택한다. 어떤 전략이든 팀 내에서 일관되게 적용하는 것이 핵심이다.
--rebase 플래그를 쓰면 fetch + rebase가 된다. 로컬 커밋이 원격 최신 커밋 위에 재적용되므로 "원격과 동기화" 시 불필요한 merge commit을 만들지 않는다. git config --global pull.rebase true로 기본값으로 설정할 수 있다.리베이스와 가비지 컬렉션
리베이스 이후 원래 커밋들(F1, F2)은 저장소에서 즉시 사라지지 않는다. 이 동작을 이해하면 실수로 리베이스한 경우에도 복구할 수 있다.
Git은 reflog라는 안전망을 제공한다. git reflog는 HEAD가 최근에 가리켰던 모든 커밋의 SHA 목록을 보여준다. 리베이스로 "사라진" 커밋의 SHA를 reflog에서 찾아 git reset --hard <sha>로 브랜치를 되돌릴 수 있다.
reflog 항목은 기본적으로 90일간 보존된다. 그 이후 git gc(가비지 컬렉션)가 실행될 때 참조되지 않는 커밋들이 실제로 삭제된다. 즉, 리베이스 직후에는 여전히 복구가 가능하다.
# 리베이스 전 상태 확인
git reflog
# 출력 예:
# f2e3d1c (HEAD -> feature) HEAD@{0}: rebase finished: returning to refs/heads/feature
# a4b5c6d HEAD@{1}: rebase: F2
# 7e8f9a0 HEAD@{2}: rebase: F1
# 1b2c3d4 HEAD@{3}: checkout: moving from feature to main ← 리베이스 시작 직전
# d4e5f6a HEAD@{4}: commit: F2 ← 원래 F2의 SHA!
# c3d4e5f HEAD@{5}: commit: F1 ← 원래 F1의 SHA!
# 리베이스 이전으로 되돌리기
git reset --hard HEAD@{4} # 원래 F2 커밋으로
# 또는 리베이스 중 언제든 취소
git rebase --abort