
개인 앱 프로젝트를 마일스톤 단위로 쪼개서 만들었다. 기능 묶음마다 브랜치를 따고, 끝나면 main 으로 합치는 방식이었다. 세 번째 묶음을 합치고 나서 git log --graph 를 열었는데, 선이 갈라졌다 붙었다를 반복해서 어느 작업이 언제 들어갔는지 읽히지 않았다.
찾아보면 "rebase 를 쓰면 히스토리가 깔끔해진다" 는 말이 흔하다. 그런데 이상했다. 두 방식 모두 결국 같은 파일 상태를 만든다. 최종 코드가 같은데 왜 둘 다 존재하고, 왜 한쪽이 더 깔끔하다는 걸까.
merge와 rebase는 최종 파일 상태가 같은데 왜 둘 다 있고, 언제 뭘 골라야 하나?
환경
| 항목 | 버전 |
|---|---|
| Git | 2.50.1 (Apple Git-155) |
| 확인한 문서 | git-scm.com 공식 레퍼런스 (2026-08-07 기준) |
두 명령이 실제로 하는 일
이름부터 다르다. merge 는 합치는 것이고, rebase 는 base 를 다시(re) 잡는 것이다.
main 에서 갈라져 나온 feature 브랜치가 있다고 하자. 갈라진 뒤 양쪽에 각각 커밋이 쌓였다.
merge 는 두 갈래를 하나로 묶는 커밋을 새로 만든다. 이 커밋은 부모가 두 개다. 공식 문서는 이렇게 적는다.
Except in a fast-forward merge (see above), the branches to be merged must be tied together by a merge commit that has both of them as its parents.
fast-forward 머지를 제외하면, 머지되는 브랜치들은 양쪽 모두를 부모로 갖는 merge commit 으로 묶여야 한다.
rebase 는 커밋을 옮겨 심는다. 문서의 첫 줄이 "Transplant a series of commits onto a different starting point" 다. 이식한다는 표현을 쓴다. 그런데 실제 동작은 옮기는 게 아니라 다시 만드는 것에 가깝다.
Replay the commits, one by one, in order. This is similar to running
git cherry-pick <commit>for each commit.커밋을 하나씩 순서대로 다시 재생한다. 각 커밋마다
git cherry-pick <commit>을 실행하는 것과 비슷하다.
여기가 핵심이다. cherry-pick 은 커밋을 복사해서 새 커밋을 만드는 명령이다. 그러니 rebase 를 거친 커밋은 내용이 같아도 해시가 다른 별개의 객체다. 공식 문서 예시도 원래 커밋을 A, 재생된 커밋을 A' 로 구분해서 표기한다.
안에서 무슨 일이 벌어지나

merge — 세 지점을 비교한다
merge 라고 하면 두 브랜치를 비교할 것 같지만 아니다. 실제로는 공통 조상까지 포함해 세 지점을 본다. 이걸 3-way merge 라고 한다.
기본 전략은 ort 다. 예전에 쓰던 recursive 는 v2.49.0 까지만 별개 구현이었고, v2.50.0 에서 ort 를 가리키도록 리다이렉트돼 지금은 이름만 남은 동의어다. 내 로컬이 2.50.1 이니 -s recursive 를 적어도 실제로 도는 건 ort 다.
세 지점을 보기 때문에 Git 은 "양쪽이 같은 곳을 고쳤는지" 를 판단할 수 있다. 한쪽만 고쳤으면 그쪽을 따라가고, 양쪽 다 고쳤으면 충돌을 낸다.
그리고 갈라진 적이 없으면 커밋을 만들 필요조차 없다.
In this case, a new commit is not needed to store the combined history; instead, the
HEAD(along with the index) is updated to point at the named commit, without creating an extra merge commit.이 경우 합쳐진 히스토리를 저장할 새 커밋이 필요 없다. 대신
HEAD가 (인덱스와 함께) 대상 커밋을 가리키도록 갱신되며, 별도의 merge commit 은 만들지 않는다.
이게 fast-forward 다. 현재 브랜치가 상대 커밋의 조상이기만 하면 일어난다. 즉 git merge 를 쳤다고 항상 merge commit 이 생기는 게 아니다. 이 사실을 모르면 "왜 어떤 머지는 커밋이 생기고 어떤 건 안 생기지" 에서 막힌다.
rebase — 커밋을 다시 만든다
rebase 는 갈라진 지점부터 내 커밋들을 떼어내, 상대 브랜치 끝에 하나씩 다시 붙인다. 하나씩이라는 게 중요하다. 충돌도 커밋 단위로 난다.
git switch feature
git rebase main
충돌이 나면 전체가 멈추는 게 아니라 문제가 된 커밋에서 멈춘다. 거기서 셋 중 하나를 고른다.
| 명령 | 하는 일 |
|---|---|
git rebase --continue |
충돌을 해결한 뒤 이어서 진행 |
git rebase --skip |
그 커밋을 버리고 다음으로 |
git rebase --abort |
전부 되돌리고 원래 브랜치로 |
커밋이 10개면 충돌을 10번 만날 수도 있다. merge 는 한 번에 몰아서 끝나는데 rebase 는 나눠서 겪는다. 총량이 줄어드는 게 아니라 분산된다.
나는 이렇게 나눠 쓴다
기준은 하나로 정리된다. 그 커밋을 나 말고 다른 사람이 가지고 있는가.
git switch feature
git rebase main
# 공유 브랜치로 합칠 때 → merge, 그리고 fast-forward 를 막는다
git switch main
git merge --no-ff feature
--no-ff 를 붙이는 이유는 문서 그대로다.
With
--no-ff, create a merge commit in all cases, even when the merge could instead be resolved as a fast-forward.
--no-ff를 쓰면 fast-forward 로 해결할 수 있는 경우에도 항상 merge commit 을 만든다.
fast-forward 로 붙어 버리면 커밋들이 main 히스토리에 그냥 섞인다. 어디부터 어디까지가 한 기능이었는지 경계가 사라진다. merge commit 하나를 남겨 두면 나중에 그 단위로 되돌릴 수 있다.
git pull 도 손봐 뒀다. 기본값이 merge 라 원격을 받을 때마다 의미 없는 merge commit 이 쌓인다.
git config --global pull.rebase true
정리하면 이렇게 나뉜다.
| 상황 | 고르는 것 | 이유 |
|---|---|---|
| 내 로컬 브랜치를 최신으로 | rebase |
아직 아무도 안 가진 커밋이라 다시 만들어도 안전 |
| 원격에서 받아오기 | pull --rebase |
의미 없는 merge commit 방지 |
| 기능 브랜치를 공유 브랜치로 | merge --no-ff |
작업 단위 경계를 남긴다 |
| 이미 올린 브랜치 | merge |
rebase 는 금지 (아래) |
대가
rebase 가 깔끔해 보이는 건 대가를 치르고 얻은 것이다.
첫째, 히스토리가 사실이 아니게 된다. rebase 를 거치면 내 커밋들은 마치 처음부터 최신 main 위에서 작업한 것처럼 보인다. 실제로는 그 시점에 존재하지도 않은 코드 위에서 작업한 게 된다. "언제 갈라져서 언제 합쳐졌나" 라는 정보는 사라진다. merge 히스토리가 지저분한 건 실제로 지저분하게 일어났기 때문이다.
둘째, 공유된 브랜치에서는 쓰면 안 된다. 문서의 경고가 분명하다.
Rebasing (or any other form of rewriting) a branch that others have based work on is a bad idea: anyone downstream of it is forced to manually fix their history.
다른 사람이 그 위에서 작업을 쌓아 둔 브랜치를 rebase 하는 것은(혹은 다른 어떤 형태로든 다시 쓰는 것은) 나쁜 생각이다. 그 아래에 있는 모든 사람이 자기 히스토리를 손으로 고쳐야만 한다.
해시가 바뀌기 때문이다. 원격에는 옛 해시로 된 커밋이 있는데 내 로컬에는 새 해시가 있으니, Git 은 이걸 서로 다른 커밋으로 본다. 이 상태에서 억지로 올리려면 --force 가 필요하고, 그 순간 남의 커밋을 지울 수 있다.
셋째, 충돌을 여러 번 겪는다. 앞에서 본 그대로다. 커밋이 많을수록 나눠서 만난다.
반대로 merge 의 대가는 히스토리 복잡도 하나다. 브랜치가 많은 팀에서 git log --graph 는 읽기 어려워진다. 다만 이건 정보가 많아서 어려운 것이지 틀린 게 아니다.
정리
질문으로 돌아가면, 두 명령은 결과가 같은 게 아니었다. 최종 파일 상태만 같고 히스토리는 다르다. merge 는 일어난 일을 그대로 기록하고, rebase 는 일어나지 않은 일을 기록한다.
그래서 기준도 하나로 좁혀진다. 그 커밋을 남이 가지고 있으면 merge, 나만 가지고 있으면 rebase. 깔끔함이 기준이 아니다. 히스토리를 다시 써도 아무도 안 다치는 구간인지가 기준이다.
처음의 그 갈라진 그래프는 사실 정직한 기록이었다. 다만 마일스톤 브랜치를 --no-ff 없이 합친 구간이 섞여 있어서, 어디가 한 묶음이었는지 경계가 지워져 있었다. 지금은 합칠 때 --no-ff 를 붙인다.
'Git' 카테고리의 다른 글
| [Git] 충돌은 왜 나고, 나는 뭘 고르는 건가 (0) | 2026.09.01 |
|---|---|
| [Git] 커밋을 날렸을 때 reflog 로 되살리기 (0) | 2026.09.01 |
| [Git] .gitignore 에 넣었는데 계속 올라가는 이유 (0) | 2026.09.01 |