← 블로그로 돌아가기

두려움 없는 인터랙티브 rebase: 실전 가이드

tutorial git

Git에서 가장 기피되는 도구

개발자들에게 어떤 Git 명령어가 가장 불안하냐고 물으면, git rebase -i는 거의 항상 상위권에 오릅니다. 인터랙티브 rebase에는 이런 평판이 따라다닙니다. 히스토리를 다시 쓰고, 에디터에 낯선 todo list가 열리며, 무언가를 망가뜨리기 너무 쉬워 보인다는 것이죠. 그래서 대부분의 사람들은 그냥 피해버리고, 그 결과 pull request에는 "wip", "fix typo", "actually fix it" 같은 commit이 그대로 실려 도착합니다.

이는 정말 아쉬운 일입니다. 인터랙티브 rebase는 지저분한 branch를 깨끗하고 리뷰하기 좋은 히스토리로 바꾸는 데 단연 최고의 도구이기 때문입니다. 이 가이드에서는 하나의 구체적인 예제를 끝까지 따라가며 그 실체를 파헤칩니다. 9개의 지저분한 commit이 있는 기능 branch를 pull request를 열기 전에 3개의 깔끔한 commit으로 재구성할 것입니다. rebase가 과연 자신의 워크플로우에 맞는 선택인지 아직 고민 중이라면, 먼저 rebase와 merge 비교부터 읽어보세요. 이 글은 rebase하기로 결정했고 그것을 잘 해내고 싶은 사람을 위한 것입니다.

깨끗한 히스토리가 중요한 이유

pull request 전에 commit을 정리하는 것은 미적인 문제가 아닙니다. 잘 정돈된 히스토리는 매우 실용적인 세 가지 방식으로 보답합니다:

  • 리뷰 용이성. "Add login form with validation", "Add remember me option", "Add error messages"라는 세 개의 논리적인 commit을 읽는 리뷰어는 각 변경사항을 독립적으로 리뷰할 수 있습니다. 같은 코드가 9개의 반쯤 완성된 commit에 흩어져 있으면 전체 diff를 한 번에 리뷰해야 하고, 리뷰 품질은 떨어집니다.
  • git bisect. 몇 주 후에 버그가 나타나면 git bisect가 히스토리를 탐색하여 버그를 유발한 commit을 찾아냅니다. 이는 모든 commit이 단독으로 빌드되고 그 자체로 의미가 있을 때만 작동합니다. 컴파일조차 되지 않는 "wip" commit으로 가득한 히스토리에서는 bisect가 거의 무용지물입니다.
  • 되돌리기. 기능을 철회해야 한다면, 자기 완결적인 commit 하나를 revert하는 것은 간단합니다. 9개의 뒤엉킨 commit에 흩뿌려진 기능을 revert하는 것은 오후 내내 걸리는 고고학 발굴 작업이 됩니다.

예제: 9개의 지저분한 commit

우리가 정리할 branch는 다음과 같습니다. 지극히 평범한 작업 히스토리이고, 실제로 모두가 이렇게 코딩합니다:

$ git log --oneline main..feature/login
c9d0e1f oops forgot the css file
b8c9d0e add error messages
a7b8c9d wip 2
f6a7b8c add remember me checkbox
e5f6a7b actually fix it
d4e5f6a fix typo
c3d4e5f add form validation
b2c3d4e wip
a1b2c3d add login form skeleton

작업하는 동안 이렇게 commit하는 것은 전혀 잘못이 아닙니다. 문제는 이 상태 그대로 배포되는 경우뿐입니다. 우리의 목표는 다음 세 개의 commit으로 끝내는 것입니다:

  1. Add login form with validation
  2. Add remember me option
  3. Add error messages with styling

rebase 시작하기: git rebase -i HEAD~9

최근 9개의 commit을 다시 쓰려면 다음을 실행합니다:

git rebase -i HEAD~9

HEAD~9는 "현재 commit에서 9단계 이전의 commit"을 의미하며, 그 지점 이후의 모든 것이 편집 대상이 됩니다. Git은 에디터를 열고 todo list를 보여줍니다:

pick a1b2c3d add login form skeleton
pick b2c3d4e wip
pick c3d4e5f add form validation
pick d4e5f6a fix typo
pick e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
pick a7b8c9d wip 2
pick b8c9d0e add error messages
pick c9d0e1f oops forgot the css file

무언가를 건드리기 전에 이해해야 할 것이 두 가지 있습니다. 첫째, 이것은 그냥 텍스트 파일입니다. 저장하고 닫기 전까지는 아무 일도 일어나지 않으며, 모든 줄을 삭제하거나 의미 있는 변경 없이 닫으면 rebase는 아무 피해 없이 취소됩니다. 둘째, 순서가 가장 오래된 것이 맨 위입니다. git log와는 반대죠. Git은 commit을 위에서 아래로 재생하면서 각 줄 맨 앞의 동사를 적용합니다.

여섯 가지 동사

pick

commit을 그대로 유지합니다. 모든 줄의 기본값입니다.

reword

commit의 변경사항은 유지하되, commit 메시지를 편집할 수 있도록 멈춥니다. 코드를 건드리지 않고 "wip 2"를 고치기에 안성맞춤입니다.

edit

이 commit에서 rebase를 일시 중지합니다. amend하거나, 여러 commit으로 분할하거나, 테스트한 다음 git rebase --continue로 계속할 수 있습니다. 가장 고급 동사이자 가장 드물게 사용하게 될 동사입니다.

squash

이 commit을 바로 위의 commit에 합치고, 두 commit 메시지를 하나로 합칠 수 있도록 에디터를 엽니다.

fixup

squash와 같지만, 이 commit의 메시지를 완전히 버립니다. 위의 commit은 메시지를 그대로 유지합니다. "fix typo" 같은 commit에 쓰고 싶은 것이 바로 이것입니다. 그런 메시지에는 보존할 가치가 있는 정보가 없으니까요.

drop

commit과 그 변경사항을 완전히 삭제합니다. todo list에서 줄을 지워도 같은 효과가 있지만, drop이라고 쓰면 의도가 명확해집니다.

단계별로 branch 정리하기

이제 우리의 9개 commit에 적용해 봅시다. 두 가지 작업이 필요합니다: 순서 바꾸기동사 변경하기입니다.

먼저 순서 바꾸기입니다. diff를 살펴보면 "oops forgot the css file"은 사실 에러 메시지가 아니라 remember me 체크박스를 위한 스타일시트입니다. todo list에서 순서를 바꾸는 것은 그냥 줄을 옮기는 일입니다. c9d0e1f 줄을 잘라내서 remember me 그룹 바로 뒤에 붙여넣으세요. Git은 새 순서대로 commit을 재생합니다.

다음은 동사입니다. "actually fix it"까지의 모든 것은 로그인 폼에 속하므로 첫 번째 commit으로 합칩니다. "add form validation"에는 squash를 사용합니다. 그 메시지를 최종 메시지에 합치고 싶기 때문입니다. 메시지가 사라져 마땅한 잡음 commit에는 fixup을 사용합니다. 편집된 todo list는 다음과 같습니다:

pick a1b2c3d add login form skeleton
fixup b2c3d4e wip
squash c3d4e5f add form validation
fixup d4e5f6a fix typo
fixup e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
fixup a7b8c9d wip 2
fixup c9d0e1f oops forgot the css file
reword b8c9d0e add error messages

저장하고 닫습니다. Git은 commit을 재생하며 두 번 멈춥니다. 한 번은 첫 번째 그룹의 합쳐진 메시지를 작성하도록("Add login form with validation"이라고 입력), 또 한 번은 reword를 위해("Add error messages with styling"이라고 입력) 멈춥니다. remember me commit의 경우 간단히 reword해도 됐겠지만, 여기서는 pick 그대로 두었습니다. 원래 메시지가 이미 충분히 괜찮았기 때문입니다. 결과는 다음과 같습니다:

$ git log --oneline main..feature/login
3f4e5d6 Add error messages with styling
2e3d4c5 Add remember me option
1d2c3b4 Add login form with validation

9개의 commit이 3개가 되었고, 각각이 자기 완결적이며 리뷰 가능합니다. 모든 해시가 바뀌었다는 점에 주목하세요. 이들은 완전히 새로운 commit입니다. 이 사실을 기억해 두세요. 아래에서 설명할 황금률의 핵심입니다.

프로의 기술: --autosquash

익숙해지면 마지막에 한꺼번에 정리하는 대신 작업하면서 정리를 미리 준비할 수 있습니다. 이전 commit의 내용을 수정할 때 이렇게 기록하세요:

git commit --fixup a1b2c3d

Git은 fixup! add login form skeleton이라는 메시지를 가진 commit을 만듭니다. 나중에 다음을 실행하세요:

git rebase -i --autosquash main

Git은 모든 fixup! commit을 이미 대상 아래로 옮기고 fixup으로 표시한 상태로 todo list를 미리 채워줍니다. 저장하고 닫기만 하면 됩니다. 이를 기본 동작으로 만들려면 한 번만 설정하세요:

git config --global rebase.autosquash true

황금률: 공유된 commit은 절대 rebase하지 않기

rebase는 commit을 수정하는 것이 아니라 새 commit으로 교체합니다. 팀원들이 이미 이전 commit을 pull해서 그 위에 작업을 쌓았다면, 그 commit들을 다시 쓰는 순간 두 개의 갈라진 히스토리가 생기고, 다음 merge는 중복과 충돌의 아수라장이 됩니다.

규칙은 간단합니다: 아직 push하지 않은 commit이거나, 자신만 작업하는 branch에 있는 commit만 rebase하세요. pull request를 열기 전에 자신의 기능 branch를 정리하는 것은 교과서적인 안전 사례입니다. 이미 branch를 push했지만 여전히 자신이 유일한 작성자라면, git push --force-with-lease로 다시 쓴 히스토리를 push하세요. 이 명령은 그 사이에 다른 사람이 push한 작업을 덮어쓰는 것을 거부합니다. main이나 팀이 pull하는 branch는 절대 다시 쓰지 마세요.

문제가 생겼을 때

rebase 도중에는 두 가지 일이 일어날 수 있습니다. 재생되는 commit이 이전 변경사항과 충돌하면 Git은 멈추고 충돌 해결을 요청합니다. 해결한 후 파일을 git add하고 git rebase --continue를 실행하세요. 그리고 어느 시점에서든 길을 잃었다고 느낀다면, 완전한 탈출 버튼이 있습니다:

git rebase --abort

이 명령은 branch를 시작하기 전과 정확히 같은 상태로 되돌립니다. 아무것도 묻지 않습니다. --abort가 존재한다는 것만 기억하면, 진행 중인 rebase에 갇히는 일은 절대 없습니다.

rebase가 끝난 후에야 결과가 잘못되었다는 것을 깨달으면 어떻게 될까요? 그래도 잃은 것은 없습니다. 이전 commit들은 여전히 존재합니다. Git은 몇 주 동안 그것들을 보관하고, reflog는 branch가 가리켰던 모든 위치를 기록하므로, reset 한 번으로 rebase 이전 상태로 돌아갈 수 있습니다. 이 안전망에 대해서는 git reflog로 잃어버린 commit 복구하기에서 자세히 다룹니다. 실수가 더 작은 경우, 즉 마지막 commit만 바꾸면 되는 경우라면 rebase가 전혀 필요 없습니다. Git commit 되돌리는 방법을 참고하세요.

GitSquid의 인터랙티브 rebase

todo list는 강력하지만, 텍스트 파일에서 동사를 편집하는 부분이 바로 사람들을 불안하게 만드는 지점입니다. GitSquid는 이를 비주얼 인터랙티브 rebase로 대체합니다. drag & drop으로 commit 순서를 바꾸고, 각 commit의 동작(pick, squash, fixup, reword, drop)을 드롭다운 선택기에서 고릅니다. 실행 전에 전체 계획이 한눈에 보이기 때문에 두려움의 대부분이 사라집니다.

GitSquid는 시작하기 전부터 도움을 줍니다. 무료 티어에 포함된 Conflict Predictor는 rebase를 실행하기 전에 어떤 파일이 충돌할지 보여주므로, 중간에 충돌을 맞닥뜨리는 대신 무엇이 기다리고 있는지 미리 알 수 있습니다. rebase는 commit 그래프의 branch 및 commit 컨텍스트 메뉴에서 바로 사용할 수 있습니다.

빠른 참조

명령어 / 동사 동작
git rebase -i HEAD~n 최근 n개의 commit을 인터랙티브하게 다시 쓰기
pick commit을 그대로 유지
reword 변경사항은 유지, 메시지 편집
edit 이 commit에서 멈춰 amend하거나 분할
squash 이전 commit에 합치고 메시지를 결합
fixup 이전 commit에 합치고 이 메시지는 폐기
drop commit을 완전히 삭제
git commit --fixup <hash> 이전 commit을 향한 수정 기록
git rebase -i --autosquash fixup! commit을 todo list에 자동 배치
git rebase --continue 충돌 해결 후 재개
git rebase --abort 취소하고 rebase 이전 상태로 복원

인터랙티브 rebase는 약간의 연습에 커다란 힘으로 보답합니다. 버려도 되는 branch에서 시작하고, --abort를 기억하고, 황금률을 지키면 "wip, fix typo, actually fix it"을 깨끗한 히스토리로 바꾸는 일이 일상이 됩니다. 텍스트 파일을 편집하는 대신 계획을 눈으로 보고 싶다면, GitSquid를 무료로 다운로드하고 다음 기능 branch에서 비주얼 rebase를 시도해 보세요.