Git— 스냅샷으로 이해하는 버전 관리
6장

되돌리기 — reset · revert · checkout

Git에서 "되돌리기"는 단일한 연산이 아니다 — reset은 브랜치 포인터를 움직이고, revert는 새 커밋으로 과거를 상쇄하며, restore는 파일 하나를 이전 상태로 돌려놓는다.

세 가지 "되돌리기"의 차이를 이해하기 전에

초보자가 Git에서 가장 많이 혼동하는 질문은 "어떻게 되돌리냐"이다. 그 혼란의 근원은 "되돌리기"라는 말이 실제로 세 가지 전혀 다른 연산을 가리키기 때문이다.

첫 번째: 브랜치 포인터 이동 — 내가 아직 공유하지 않은 커밋 히스토리를 다시 쓰고 싶다. 이때 쓰는 것이 reset이다. 두 번째: 새 커밋으로 과거 취소 — 이미 공유한 커밋의 변경 내용을 되돌리고 싶지만 히스토리는 지우고 싶지 않다. 이때 쓰는 것이 revert다. 세 번째: 파일 단위 복원 — 특정 파일을 커밋된 버전으로 되돌리고 싶다. 이때 쓰는 것이 restore(또는 구식 checkout -- <file>)다.

이 세 가지는 목적과 작동 범위가 다르므로, 상황에 맞는 도구를 고르는 것이 핵심이다.

reset vs revert의 결정적 차이 — reset은 기존 커밋을 없애거나 이동시키며(히스토리를 다시 씀), revert는 기존 커밋을 건드리지 않고 새 커밋을 추가한다(히스토리를 보존함).

git reset — 브랜치 포인터 이동

git reset은 현재 브랜치가 가리키는 커밋을 다른 커밋으로 옮긴다. "포인터 이동"이 본질이다.

reset에는 세 가지 모드가 있으며, 차이는 "스테이징 영역(index)과 작업 트리를 어떻게 처리하느냐"다.

--soft: 브랜치 포인터만 이동. 스테이징 영역과 작업 트리는 그대로 둔다. 커밋은 사라지지만 변경 내용은 스테이지된 상태로 남는다. 여러 커밋을 하나로 합치는 "squash" 작업에 유용하다.

--mixed(기본): 브랜치 포인터 이동 + 스테이징 영역 초기화. 작업 트리는 그대로. 변경 내용은 unstaged 파일로 남는다. git reset HEAD~1--mixed다.

--hard: 브랜치 포인터 이동 + 스테이징 영역 초기화 + 작업 트리 초기화. 변경 내용이 완전히 사라진다. --hard 이후에는 git reflog를 통해 원래 커밋 SHA를 찾아 복구하지 않는 한 되돌릴 방법이 없다.

reset 모드 비교
git reset --soft  HEAD~1   # 포인터만 이동, staged 유지
git reset --mixed HEAD~1   # 포인터 + index 초기화 (기본값)
git reset --hard  HEAD~1   # 포인터 + index + 작업트리 초기화
git reset --hard  origin/main  # 원격 기준으로 완전 초기화
함정: --hard 옵션은 저장되지 않은 모든 변경사항을 영구적으로 삭제한다. 공유 브랜치에서 reset 후 force push하면 팀원의 히스토리와 충돌한다. 공유된 커밋에는 절대 reset을 사용하지 말 것.
핵심: reset은 "브랜치 포인터를 과거로 이동시키는 것"이다. 커밋 객체 자체는 즉시 삭제되지 않는다 — git reflog에 최대 90일간 남아 복구 가능하다.

git revert — 새 커밋으로 과거를 안전하게 취소

git revert는 지정한 커밋의 변경 내용을 정반대로 적용하는 새 커밋을 만든다. 원본 커밋은 히스토리에 그대로 남는다.

revert의 결정적 장점은 히스토리를 다시 쓰지 않는다는 것이다. 이미 원격에 push했거나 팀원과 공유한 커밋을 취소해야 할 때 revert가 올바른 선택이다. 원본 커밋은 "이런 변경이 있었다"는 기록으로 남고, revert 커밋은 "이 변경을 취소했다"는 기록을 추가한다.

revert는 지정한 커밋 하나의 diff를 역전시킨다. 따라서 "A → B → C" 히스토리에서 B를 revert하면, B가 추가한 변경만 취소하는 커밋 D가 추가된다. A와 C의 변경 내용은 유지된다. 만약 B와 C가 같은 파일을 건드렸다면 충돌이 발생할 수 있다.

여러 커밋을 순서대로 revert할 때는 보통 최신 커밋부터 역순으로 진행해야 충돌을 최소화할 수 있다.

revert 명령
git revert HEAD              # 가장 최근 커밋 취소
git revert <sha>             # 특정 커밋 취소 (새 커밋 생성)
git revert HEAD~3..HEAD      # 최근 3개 커밋 각각 revert
git revert --no-commit HEAD  # 커밋 없이 index에만 적용 (묶기용)
직관: revert는 "잘못된 레고 블록을 제거하되, 그 블록이 거기 있었다는 기록은 남기는" 것이다. reset은 "레고 블록을 처음부터 없었던 것처럼 히스토리에서 지우는" 것이다.

git restore — 파일 단위 복원

git restore는 Git 2.23에서 도입된 명령으로, 작업 트리 또는 스테이징 영역의 파일을 지정한 버전으로 복원한다. 브랜치나 히스토리에는 영향을 주지 않는다.

이전에는 이 역할을 git checkout -- <파일>이 담당했다. checkout이 브랜치 전환과 파일 복원이라는 두 가지 기능을 함께 맡았던 것을 Git이 분리한 것이다. 현재도 두 명령 모두 동작하지만, 명시적 의도 전달을 위해 restore를 권장한다.

git restore <파일>은 작업 트리의 파일을 스테이징 영역(index) 기준으로 복원한다. git restore --staged <파일>은 스테이징 영역을 마지막 커밋 기준으로 되돌린다(unstage). git restore --source=HEAD~2 <파일>은 2커밋 전의 버전으로 파일을 복원한다.

restore는 커밋을 생성하지 않으며, 작업 중인 변경사항을 즉시 버릴 때 사용한다. restore로 작업 트리를 복원한 경우 해당 변경사항은 복구할 수 없다 — 스태시에 저장하지 않았다면.

restore 명령
git restore <파일>                    # 작업트리를 index 기준으로 복원
git restore --staged <파일>           # unstage (index → HEAD 기준)
git restore --staged --worktree <파일> # 둘 다 복원
git restore --source=HEAD~2 <파일>   # 2커밋 전 버전으로 복원
git checkout -- <파일>               # 구식 동등 명령

reset 시각화 — 포인터 이동과 고아 커밋

아래 애니메이션은 reset --hard HEAD~1의 동작과, 같은 상황에서 revert를 선택했을 때의 차이를 단계별로 보여준다.

reset vs revert — 브랜치 포인터와 히스토리 1 / 4
SPACE 재생 · ← → 이동 · R 리셋
세 가지 reset 모드 — soft · mixed · hard 1 / 3
SPACE 재생 · ← → 이동 · R 리셋
← 원격 저장소충돌 해결과 스태시 →