junyeokk
Blog
DevOps·2026. 02. 06

Git Squash

여러 커밋을 하나로 합치는 것을 squash(스쿼시)라고 한다. 기능 하나를 구현하면서 남긴 커밋이 6개라면, squash로 이 6개를 의미 있는 1개 커밋으로 압축할 수 있다. git reset --soft, git rebase -i, GitHub의 Squash and merge 등 방법이 여러 가지인데, 각각 동작 방식과 사용 시점이 다르다.

squash가 어떻게 가능한지 이해하려면 먼저 Git이 커밋을 어떻게 관리하는지 알아야 한다.

커밋의 구조

Git의 커밋은 "변경 사항"이 아니라 프로젝트 전체의 스냅샷이다. 각 커밋은 그 시점의 모든 파일 상태를 기록하고, 이전 커밋을 가리키는 parent 포인터를 가진다.

HEAD는 "지금 내가 어디에 있는가"를 가리키는 포인터다. 브랜치는 특정 커밋을 가리키는 포인터이고, HEAD는 현재 브랜치를 가리킨다. HEAD → feature → commit F라는 체인이 만들어진다.

squash의 핵심은 이 포인터들을 조작하는 것이다. 커밋 B부터 F까지의 변경 사항을 하나의 새로운 커밋으로 만들면 된다.

git reset --soft

가장 단순한 squash 방법이다. HEAD를 과거 커밋으로 되돌리되, 변경 사항은 staging area에 남겨둔다.

bash
# 최근 5개 커밋을 하나로 합치기
git reset --soft HEAD~5
git commit -m "feat: 새로운 기능 추가"

git reset에는 세 가지 모드가 있다.

모드HEADStaging AreaWorking Directory
--soft이동유지유지
--mixed (기본)이동초기화유지
--hard이동초기화초기화

--soft는 HEAD 포인터만 옮기고 나머지는 건드리지 않는다. B~F 커밋에서 변경한 모든 파일이 staging area에 올라간 상태로 남아있으므로, 바로 새 커밋을 만들면 된다.

reset 전후의 히스토리를 비교하면 이렇다.

text
# Before
A ← B ← C ← D ← E ← F  (HEAD)

# After reset --soft HEAD~5
A  (HEAD)
  ↳ staging area에 B~F의 변경사항 보관

# After commit
A ← G  (HEAD)
  G = B+C+D+E+F를 하나로 합친 커밋

reset --soft의 장점은 단순함이다. 명령어 두 줄이면 끝난다. 단점은 세밀한 제어가 안 된다는 것이다. 커밋 일부만 합치고 나머지는 유지하고 싶다면 이 방법으로는 불가능하다.

git rebase -i (Interactive Rebase)

커밋별로 "이건 합치고, 이건 유지하고"를 선택할 수 있는 방법이다.

bash
git rebase -i HEAD~5

이 명령을 실행하면 에디터가 열리면서 커밋 목록이 나타난다.

text
pick abc1234 feat: 사용자 모델 추가
pick def5678 feat: 사용자 API 엔드포인트
pick 789abcd fix: 타입 오류 수정
pick bcd2345 fix: 누락된 import 추가
pick efg6789 style: 코드 포맷팅

각 커밋 앞의 pick을 다른 명령으로 바꿔서 동작을 지정한다.

명령동작
pick커밋을 그대로 유지
squash (또는 s)이전 커밋과 합치고, 커밋 메시지 편집
fixup (또는 f)이전 커밋과 합치고, 이 커밋의 메시지는 버림
reword (또는 r)커밋 메시지만 변경
drop (또는 d)커밋 삭제

전부 하나로 합치려면 첫 번째만 pick으로 두고 나머지를 squashfixup으로 바꾼다.

text
pick abc1234 feat: 사용자 모델 추가
fixup def5678 feat: 사용자 API 엔드포인트
fixup 789abcd fix: 타입 오류 수정
fixup bcd2345 fix: 누락된 import 추가
fixup efg6789 style: 코드 포맷팅

squashfixup의 차이는 커밋 메시지 처리 방식이다. squash는 합치면서 모든 커밋 메시지를 모아서 편집할 수 있게 해준다. fixup은 해당 커밋의 메시지를 버리고 pick된 커밋의 메시지만 남긴다. 최종 메시지를 새로 쓸 거라면 fixup이 깔끔하다.

rebase는 내부적으로 커밋을 하나씩 다시 적용하면서 새로운 히스토리를 만든다. 기존 커밋 B~F는 사라지고, 그 변경 사항을 모두 담은 새로운 커밋이 생성된다. 새 커밋은 기존 커밋과 다른 해시를 가진다. 같은 내용이라도 해시가 달라지는 이유는 커밋 해시가 내용뿐 아니라 parent, timestamp 등을 포함해서 계산되기 때문이다.

reset --soft vs rebase -i

기준reset --softrebase -i
복잡도명령어 2줄에디터에서 편집 필요
세밀한 제어불가 (전부 합침)커밋별로 pick/squash/drop 선택 가능
커밋 메시지새로 작성기존 메시지 편집 또는 병합 가능
사용 시점전부 하나로 합칠 때일부만 합치거나 메시지를 조합할 때

전부 하나로 합치는 경우라면 reset --soft가 빠르고 간단하다. 일부만 합치거나 커밋 메시지를 세밀하게 다루고 싶으면 rebase -i를 사용한다.

git push --force

squash를 했으면 원격 저장소에 반영해야 한다. 그런데 일반 git push는 실패한다.

bash
git push origin feature
# ! [rejected] feature -> feature (non-fast-forward)
# error: failed to push some refs to 'origin'

이유는 로컬과 리모트의 히스토리가 달라졌기 때문이다.

text
# 리모트 (기존)
A ← B ← C ← D ← E ← F

# 로컬 (squash 후)
A ← G

Git의 일반 push는 리모트 히스토리에 새 커밋을 "추가"하는 동작이다. 로컬 브랜치가 리모트 브랜치의 히스토리를 포함하면서 그보다 앞서 있을 때, 리모트 포인터를 그냥 앞으로 옮기기만 하면 되는데, 이를 fast-forward라고 한다. squash 후에는 로컬 브랜치가 리모트와 완전히 다른 경로를 가지므로 fast-forward가 불가능하다.

--force 옵션은 리모트의 히스토리를 로컬의 히스토리로 덮어쓴다.

bash
git push --force origin feature

--force vs --force-with-lease

--force는 리모트의 상태와 무관하게 덮어쓴다. 다른 사람이 그 사이에 push한 커밋이 있어도 무시하고 날려버린다.

--force-with-lease는 한 단계 안전장치가 있다. "내가 마지막으로 fetch한 시점의 리모트 상태"와 현재 리모트 상태를 비교해서, 그 사이에 변경이 있으면 push를 거부한다.

bash
# 다른 사람의 push가 있으면 거부됨
git push --force-with-lease origin feature

혼자 작업하는 브랜치라면 --force--force-with-lease든 결과가 같다. 하지만 협업 브랜치에서는 --force-with-lease가 안전하다.

GitHub Squash and Merge

GitHub PR에서 "Squash and merge" 버튼을 누르면, 위에서 설명한 수동 과정을 GitHub 서버가 자동으로 수행한다.

수동 squash와의 핵심적인 차이가 있다.

기준수동 squash + push --forceGitHub Squash and merge
실행 위치로컬GitHub 서버
feature 브랜치 히스토리squash된 상태로 변경됨원본 그대로 유지 (머지 후 삭제)
main에 반영되는 방식squash 후 일반 merge 또는 rebasesquash된 커밋 1개가 main에 직접 추가
사용 시점리뷰 중간에 "커밋 정리해주세요" 요청PR 머지 시점
force push 필요 여부필요불필요

리뷰어가 "커밋을 합쳐주세요"라고 한 상황은 수동 squash가 필요한 경우다. PR을 머지하기 전 단계에서 feature 브랜치의 커밋을 정리해달라는 요청이기 때문이다. GitHub의 Squash and merge는 머지 시점에 자동으로 합쳐주는 것이므로, 리뷰 중간에 히스토리를 깔끔하게 만드는 것과는 목적이 다르다.

정리

커밋을 합치는 방법은 상황에 따라 다르다.

  • 전부 하나로 합칠 때: git reset --soft HEAD~Ngit commitgit push --force
  • 일부만 합치거나 세밀하게 제어할 때: git rebase -i HEAD~Ngit push --force
  • PR 머지 시 자동으로 합칠 때: GitHub Squash and merge 버튼

어떤 방법을 쓰든, squash의 본질은 같다. 여러 커밋의 변경 사항을 하나의 새로운 커밋으로 만드는 것이다. 방법이 다를 뿐, 결과는 동일하다.

관련 문서