merge와 rebase 중 무엇을 쓸지는 취향 논쟁처럼 다뤄지지만, 실제로는 기준이 꽤 명확하다. 그 커밋을 남이 이미 가지고 있는가. 이 질문 하나로 대부분의 상황이 갈린다.
이 글은 두 명령이 실제로 무엇을 하는지 그래프로 확인한 다음, 팀에서 그대로 적용할 수 있는 규칙과 인터랙티브 리베이스 실전 절차를 정리한다.
두 명령이 하는 일
feature가 main에서 갈라져 나온 뒤 양쪽에 커밋이 쌓인 상황이다. 여기서 main의 변경을 내 브랜치로 가져오는 방법이 두 가지다.

git switch feature
git merge main # main의 변경을 머지 커밋으로 합친다merge는 부모가 둘인 커밋을 새로 만든다. 원래 커밋들은 해시 그대로 남고, 갈라졌다 합쳐진 모양이 이력에 그대로 기록된다.
git switch feature
git rebase main # 내 커밋들을 main 끝에 다시 얹는다rebase는 내 커밋을 하나씩 떼어내 main 위에 다시 적용한다. 변경 내용은 같아도 부모가 달라지므로 해시가 전부 바뀐다. 그림의 C', D'가 그 뜻이다. 원래 커밋 C, D는 아무도 가리키지 않는 상태로 남았다가 결국 정리된다.
이 한 가지 사실에서 나머지가 전부 따라 나온다.
황금률: 남이 가진 커밋은 리베이스하지 않는다
동료가 feature를 받아서 그 위에 작업 중인데 내가 feature를 리베이스해 강제 push하면, 동료의 로컬에는 이제 존재하지 않는 커밋 위에 쌓인 작업이 남는다. 동료가 git pull을 하면 같은 변경이 옛 해시와 새 해시로 두 벌 들어와 엉킨다.
이것이 Git 공식 문서가 말하는 리베이스의 황금률이다. 실무 규칙으로 옮기면 이렇다.
| 대상 | 방식 | 이유 |
|---|---|---|
| 나만 쓰는 기능 브랜치 | rebase 자유롭게 | 깨질 사람이 없다 |
| 리뷰 중인 내 PR 브랜치 | rebase 가능. 강제 push는 --force-with-lease |
리뷰어가 이미 본 diff가 흔들리므로 미리 알린다 |
| 둘 이상이 같이 쓰는 브랜치 | merge | 리베이스하면 상대 로컬이 깨진다 |
main, release |
절대 rebase 금지 | 모두가 가지고 있다 |
최신화는 rebase, 통합은 팀 규칙
실무에서 두 명령을 쓰는 자리는 사실 다르다.
작업 중 최신화는 rebase가 낫다. main이 앞서 나갔을 때 내 브랜치를 따라잡는 상황인데, 여기서 merge를 쓰면 "main을 머지함" 같은 의미 없는 커밋이 브랜치에 쌓이고 PR diff가 지저분해진다.
git fetch origin
git rebase origin/main작업 트리가 지저분해도 멈추지 않게 하려면 autostash를 켜둔다.
git config --global rebase.autoStash true
git config --global pull.rebase truepull.rebase를 켜면 git pull이 fetch 뒤에 rebase를 돌린다. 원격에 이미 있는 남의 커밋은 건드리지 않고 내 로컬 커밋만 그 위로 옮기므로 황금률에 어긋나지 않는다.
PR 통합은 팀이 하나를 정해서 지킨다. GitHub 기준으로 셋 중 하나다.
| 방식 | 이력 모양 | 되돌리기 | bisect |
|---|---|---|---|
| Merge commit | 브랜치가 갈라진 모양 그대로 | revert -m 1로 한 번에 |
중간 커밋이 깨져 있을 수 있다 |
| Squash merge | main이 한 줄. PR 하나가 커밋 하나 | revert 한 번 |
커밋 단위가 커서 범위가 넓다 |
| Rebase merge | main이 한 줄. 커밋 개수 유지 | 커밋마다 따로 되돌려야 | 커밋 단위가 작아 가장 정확하다 |
Squash merge가 가장 무난하다. main의 커밋 하나가 PR 하나에 정확히 대응하므로 이력이 읽기 쉽고, 되돌릴 단위도 명확하다. 대신 작업 중에 남긴 중간 커밋이 전부 사라진다. 커밋을 의미 단위로 잘 나눠 쓰는 팀이라면 rebase merge가 bisect에 유리하다.
주의할 점 하나. Squash merge로 통합하면 PR 브랜치의 원래 커밋들과 main의 스쿼시 커밋은 서로 다른 해시다. 그래서 머지된 브랜치를 지우지 않고 계속 쓰면 다음 PR에서 같은 변경이 또 올라온다. 스쿼시 방식을 쓰는 팀은 머지 후 브랜치 삭제를 규칙으로 둬야 한다.
인터랙티브 리베이스: 커밋을 발표 가능한 상태로
작업 중 커밋은 지저분한 것이 정상이다. "일단 저장", "오타", "다시 되돌림" 같은 커밋을 그대로 올리면 리뷰어가 읽을 것이 두 배가 된다. 올리기 직전에 정리한다.
git rebase -i origin/main편집기에 커밋 목록(todo)이 열린다. 위가 오래된 커밋, 아래가 최신이다. log와 순서가 반대라 처음에는 헷갈린다.

쓰는 명령은 사실상 여섯 개다.
| 명령 | 하는 일 |
|---|---|
pick |
그대로 둔다 (기본값) |
reword |
내용은 두고 메시지만 고친다 |
squash |
위 커밋에 합치고, 메시지는 둘 다 편집한다 |
fixup |
위 커밋에 합치고, 이 커밋 메시지는 버린다 |
drop |
커밋을 통째로 뺀다 |
edit |
그 커밋에서 멈춘다. 내용을 고칠 수 있다 |
줄 순서를 바꾸면 커밋 순서가 바뀌고, 줄을 지우면 drop과 같다. 저장하고 닫으면 위에서부터 실행한다.
edit으로 멈춘 뒤에는 이렇게 작업한다.
# 파일을 고치고
git add lib/auth.ts
git commit --amend # 그 커밋에 합쳐 넣는다
git rebase --continue커밋 하나를 둘로 쪼갤 때도 edit을 쓴다.
git reset HEAD~1 # 커밋을 풀어 작업 트리로
git add -p # 절반만 담고
git commit -m "첫 번째 조각"
git add .
git commit -m "두 번째 조각"
git rebase --continue--autosquash: 정리를 미리 예약한다
리뷰 지적을 반영할 때 "리뷰 반영" 커밋을 새로 쌓으면 나중에 또 정리해야 한다. 처음부터 어느 커밋에 합칠지 표시해서 커밋한다.
git commit --fixup=9b7e104 # 9b7e104에 합칠 커밋이라고 표시
git commit --squash=9b7e104 # 합치되 메시지도 편집하겠다--fixup으로 만든 커밋은 메시지가 fixup! <원래 제목>이 된다. 정리할 때가 되면 이렇게 한다.
git rebase -i --autosquash origin/maintodo 목록이 이미 정렬되어 있고 명령어까지 fixup으로 채워져 있다. 확인하고 저장만 하면 끝난다. 이 옵션은 아예 켜두는 편이 낫다.
git config --global rebase.autoSquash true--fixup=amend:<해시>를 쓰면 내용과 메시지를 함께 고치는 커밋으로 표시된다. 어느 커밋에 붙일지 고르기 번거로우면 git log --oneline -10으로 해시를 보고 지정하거나, 파일 기준으로 자동 선택하는 --fixup을 쓰는 대신 마지막 커밋이면 그냥 --amend를 쓴다.
--update-refs: 쌓아 올린 브랜치를 한 번에
기능을 여러 PR로 쪼개 feature/a 위에 feature/b를 얹어 작업하는 경우, 아래쪽을 리베이스하면 위쪽 브랜치는 옛 커밋에 남는다. Git 2.38부터는 함께 옮겨준다.
git rebase -i --update-refs origin/maingit config --global rebase.updateRefs truetodo 목록에 update-ref refs/heads/feature/a 같은 줄이 끼어 있는 것이 보이면 제대로 동작하는 것이다.
충돌을 두 번 풀지 않는다
긴 브랜치를 여러 번 리베이스하면 같은 충돌을 반복해서 만난다. rerere를 켜면 지난번 해결을 기억했다가 자동으로 적용한다.
git config --global rerere.enabled true켜두면 충돌을 해결할 때마다 조용히 기록하고, 같은 충돌이 다시 나오면 이미 풀린 상태로 보여준다. 자동 적용된 결과는 반드시 눈으로 확인해야 하지만, 열 번 풀 것을 한 번으로 줄여준다.
강제 push를 안전하게
리베이스한 브랜치는 원격과 이력이 어긋나므로 강제 push가 필요하다. --force는 쓰지 않는다.
git push --force-with-lease원격이 내가 마지막으로 fetch한 그 상태일 때만 덮어쓴다. 그사이 동료가 push했다면 거부한다.
여기에 함정이 하나 있다. --force-with-lease는 내 로컬의 원격 추적 브랜치와 실제 원격을 비교한다. 그런데 IDE나 에디터가 백그라운드로 git fetch를 돌리면 추적 브랜치가 조용히 갱신되어, 동료의 새 커밋이 "내가 이미 본 상태"로 둔갑한다. 이 구멍을 막는 옵션이 Git 2.30에 들어왔다.
git push --force-with-lease --force-if-includes--force-if-includes는 내가 그 커밋 위에서 실제로 작업했는지까지 확인한다. 자동 fetch를 쓰는 환경이라면 둘을 함께 쓴다.
이럴 때는 rebase를 쓰지 않는다
- 이미 공유된 브랜치: 앞의 황금률이다.
- 머지 커밋이 의미를 담고 있을 때: 릴리스 브랜치를 main에 합친 기록처럼, 갈라졌다 합쳐진 사실 자체가 정보인 경우다.
- 충돌이 커밋마다 반복될 때: 리베이스는 커밋 하나하나에 대해 충돌을 풀게 한다. 커밋 스무 개를 옮기는데 매번 같은 파일이 충돌한다면, merge 한 번으로 끝내는 편이 정직하고 빠르다.
rerere로도 감당이 안 되는 상황이 여기다. - 이력을 감사 기록으로 쓰는 조직: 규제 산업에서는 이력 재작성 자체가 금지되기도 한다.
정리
# 작업 중 최신화
git fetch origin && git rebase origin/main
# 올리기 직전 정리
git rebase -i --autosquash origin/main
# 리뷰 반영은 fixup으로 예약
git commit --fixup=<해시>
# 강제 push는 항상 이 조합
git push --force-with-lease --force-if-includes기준은 하나다. 내 손 안에 있는 커밋은 마음껏 다듬고, 남에게 나간 커밋은 건드리지 않는다. 깨끗한 이력은 미학의 문제가 아니라, 여섯 달 뒤 git log와 git bisect로 원인을 찾을 때 걸리는 시간의 문제다.
다음 글에서는 여기까지의 개념을 눈으로 익히기 좋은 학습 사이트 하나를 처음부터 끝까지 풀어본다.
댓글 0
첫 댓글을 남겨보세요.