← Git 강의 목록으로
🔀
Git·GitHub 심화
reset · revert · restore · checkout · reflog · stash

6단원 — 취소와 되돌리기

Git의 가장 큰 장점 중 하나는 실수를 되돌릴 수 있다는 것입니다. `reset`, `revert`, `restore`, `reflog`, `stash` — 다섯 가지 도구를 상황에 맞게 쓰면 잘못된 커밋, 잘못 스테이징된 파일, 임시 저장이 필요한 작업을 안전하게 처리할 수 있습니다.

resetrevertstashreflog
소요 시간
1~2시간
난이도
📊 중급
선수 조건
🎯 5단원
결과물
잘못된 변경을 안전하게 되돌리기

이 강의에서 배우는 것

  • 1`git reset`의 soft·mixed·hard 세 모드 차이를 설명하고 올바른 상황에 적용할 수 있다.
  • 2`git revert`로 공유 브랜치에 안전하게 커밋을 되돌릴 수 있다.
  • 3`git restore`로 스테이징·워킹 디렉토리의 변경을 취소할 수 있다.
  • 4`git reflog`로 reset/checkout 이후 잃어버린 커밋을 복구할 수 있다.
  • 5`git stash`로 작업 중인 변경을 임시 저장하고 복원할 수 있다.

핵심 개념

1) git reset

text
HEAD  →  커밋 이력을 되돌린다.

--soft  : HEAD 이동 + 스테이징 유지 + 워킹 유지
--mixed : HEAD 이동 + 스테이징 취소 + 워킹 유지  (기본값)
--hard  : HEAD 이동 + 스테이징 취소 + 워킹 취소  ⚠️
bash
git reset --soft  HEAD~1    # 커밋만 취소, 변경은 staged 상태
git reset --mixed HEAD~1    # 커밋 + 스테이징 취소
git reset --hard  HEAD~1    # 모두 취소 (워킹 디렉토리 변경 삭제)

2) git revert

bash
git revert HEAD         # 최신 커밋을 되돌리는 새 커밋 생성
git revert abc123       # 특정 커밋을 되돌리는 새 커밋
git revert HEAD~2..HEAD # 범위 revert

push된 브랜치에서 이력을 보존해야 할 때 사용한다.

3) git restore

bash
git restore 파일.txt             # 워킹 디렉토리 변경 취소 (스테이징 영향 없음)
git restore --staged 파일.txt   # 스테이징 취소 (워킹 유지)
git restore --staged --worktree 파일.txt  # 둘 다 취소

4) git reflog

bash
git reflog              # HEAD 이동 기록 전체
git reflog --date=iso   # 날짜 포함
git reset --hard HEAD@{3}   # 3번 전 HEAD 위치로 복구

5) git stash

bash
git stash               # 현재 변경 임시 저장
git stash push -m "메시지"
git stash list          # 목록
git stash pop           # 최근 stash 복원 + 삭제
git stash apply stash@{1}  # 특정 stash 적용 (삭제 안 함)
git stash drop stash@{0}   # 특정 stash 삭제
git stash clear         # 전체 삭제

예제로 보기

예제 1 — `ex01_reset.sh` : soft·mixed·hard reset 비교

bash
#!/usr/bin/env bash
REPO=$(mktemp -d); cd "$REPO"
git init -q && git config user.name "실습용" && git config user.email "demo@example.com"

for i in 1 2 3; do
    echo "v$i" > file.txt && git add . && git commit -q -m "commit $i"
done

echo "=== 초기 상태 ==="
git log --oneline

echo ""
echo "--- soft reset (HEAD~1) ---"
git reset --soft HEAD~1
git status --short
git log --oneline

echo ""
git add . && git commit -q -m "re-commit after soft"

echo "--- mixed reset (HEAD~1, 기본값) ---"
git reset HEAD~1
git status --short
git log --oneline

**실행 결과**

text
=== 초기 상태 ===
c3 commit 3 / b2 commit 2 / a1 commit 1

--- soft reset ---
M  file.txt    ← staged 상태 유지
b2 commit 2 / a1 commit 1

--- mixed reset ---
 M file.txt    ← unstaged
b2 commit 2 / a1 commit 1

핵심: `--soft`는 커밋만 취소, `--hard`는 모든 변경을 삭제한다.

예제 2 — `ex02_revert.sh` : push된 커밋을 revert로 안전하게 되돌리기

bash
#!/usr/bin/env bash
REPO=$(mktemp -d); cd "$REPO"
git init -q && git config user.name "실습용" && git config user.email "demo@example.com"

echo "정상 코드" > app.py && git add . && git commit -q -m "feat: good code"
echo "버그 코드" > app.py && git add . && git commit -q -m "feat: buggy code"
echo "다른 기능" > other.py && git add . && git commit -q -m "feat: other feature"

echo "=== revert 전 로그 ==="
git log --oneline

BUGGY_HASH=$(git log --oneline | grep "buggy" | awk '{print $1}')
git revert "$BUGGY_HASH" --no-edit

echo ""
echo "=== revert 후 로그 (새 revert 커밋 추가됨) ==="
git log --oneline

rm -rf "$REPO"

**실행 결과**

text
=== revert 전 로그 ===
c3 feat: other feature
b2 feat: buggy code
a1 feat: good code

=== revert 후 로그 ===
d4 Revert "feat: buggy code"
c3 feat: other feature
b2 feat: buggy code
a1 feat: good code

핵심: `revert`는 이력을 삭제하지 않고 반대 커밋을 추가한다.

예제 3 — `ex03_restore.sh` : restore 로 변경 취소

bash
#!/usr/bin/env bash
REPO=$(mktemp -d); cd "$REPO"
git init -q && git config user.name "실습용" && git config user.email "demo@example.com"

echo "원본" > file.txt && git add . && git commit -q -m "init"

echo "수정됨" > file.txt
git add file.txt

echo "=== 수정 + 스테이징 후 status ==="
git status --short

git restore --staged file.txt
echo ""
echo "=== --staged 취소 후 (unstaged) ==="
git status --short

git restore file.txt
echo ""
echo "=== 워킹 디렉토리 취소 후 ==="
git status --short
echo "파일 내용: $(cat file.txt)"

rm -rf "$REPO"

**실행 결과**

text
=== 수정 + 스테이징 후 ===
M  file.txt

=== --staged 취소 후 ===
 M file.txt

=== 워킹 디렉토리 취소 후 ===
(nothing)
파일 내용: 원본

핵심: `restore --staged`는 스테이징만, `restore`는 워킹 디렉토리만 취소한다.

예제 4 — `ex04_stash.sh` : stash 로 작업 임시 저장

bash
#!/usr/bin/env bash
REPO=$(mktemp -d); cd "$REPO"
git init -q && git config user.name "실습용" && git config user.email "demo@example.com"

echo "초기 코드" > app.py && git add . && git commit -q -m "init"

# 작업 중 긴급 버그 수정 요청
echo "WIP: 새 기능 개발 중" >> app.py
git stash push -m "WIP: 새 기능 개발"

echo "=== stash 후 status (clean) ==="
git status --short

echo ""
echo "=== stash list ==="
git stash list

# 긴급 수정
git switch -c hotfix/urgent
echo "긴급 수정" > hotfix.py && git add . && git commit -q -m "fix: urgent hotfix"
git switch main
git merge hotfix/urgent -q

# 원래 작업 복원
git stash pop

echo ""
echo "=== stash pop 후 status ==="
git status --short
echo "app.py 내용: $(cat app.py)"

rm -rf "$REPO"

**실행 결과**

text
=== stash 후 status ===
(clean)

=== stash list ===
stash@{0}: WIP: 새 기능 개발

=== stash pop 후 status ===
 M app.py
app.py 내용: 초기 코드
WIP: 새 기능 개발 중

핵심: `stash`로 임시 저장 후 다른 브랜치에서 작업하고 돌아와 복원할 수 있다.

다른 시각으로 보기

명령일상 비유push 후 사용
`reset --hard`파일 영구 삭제❌ 절대 안 됨
`reset --soft`종이에 쓴 내용 지우개로 지우기❌ 공유 전에만
`revert`수정 사항을 반대로 덮어쓰기✅ 안전
`restore`마지막 저장 불러오기
`stash`서랍에 잠깐 넣기

자주 하는 실수

  1. **push된 브랜치에 `reset --hard`** — 팀원의 이력이 꼬이고 강제 push가 필요해진다.
  2. **`reset`과 `revert` 혼동** — 공유 브랜치에서는 항상 `revert`를 사용한다.
  3. **stash 후 잊어버리기** — `git stash list`로 주기적으로 확인하고 불필요한 것은 삭제한다.
  4. **`restore` 후 복구 불가** — 스테이징되지 않은 변경을 `restore`하면 reflog로도 복구 불가능하다.
  5. **reflog 기간 만료** — 기본 90일 후 reflog 항목이 삭제되므로 빠르게 복구해야 한다.

정리

  • `reset --soft`: 커밋만 취소, `--mixed`: 스테이징도 취소, `--hard`: 모두 취소.
  • push된 커밋은 `revert`로 되돌린다 — 이력이 보존된다.
  • `restore --staged`로 스테이징 취소, `restore`로 워킹 디렉토리 변경 취소.
  • `reflog`로 실수로 날린 커밋도 복구할 수 있다.
  • `stash`는 커밋 없이 변경을 임시 저장하고 나중에 복원한다.

직접 해 보기

bash
cd 06_취소와_되돌리기/src
chmod +x *.sh
./ex01_reset.sh
./ex02_revert.sh
./ex03_restore.sh
./ex04_stash.sh

응용:

  1. `git reset --hard HEAD~3` 로 3개 커밋을 날린 뒤 `git reflog`로 복구해 보세요.
  2. `git stash branch <브랜치명>`으로 stash를 새 브랜치로 바로 분리해 보세요.

💻 예제 (examples)

실제로 실행해 결과를 확인할 수 있는 예제입니다.

ex01_reset.shsoft·mixed·hard reset 비교
CODE
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "실습용" && git config user.email "demo@example.com"
    git config commit.gpgsign false

for i in 1 2 3; do
    echo "v$i" > file.txt
    git add .
    git commit -q -m "commit $i"
done

echo "=== 초기 로그 (3개 커밋) ==="
git log --oneline

echo ""
echo "--- soft reset HEAD~1 ---"
git reset --soft HEAD~1
echo "status (staged 상태):"
git status --short
echo "log:"
git log --oneline

echo ""
echo "(다시 커밋)"
git commit -q -m "commit 3 (re-committed)"

echo ""
echo "--- mixed reset HEAD~1 (기본값) ---"
git reset HEAD~1
echo "status (unstaged 상태):"
git status --short
echo "log:"
git log --oneline

echo ""
echo "(다시 add + 커밋)"
git add . && git commit -q -m "commit 3 (re-added)"

echo ""
echo "--- hard reset HEAD~1 ---"
git reset --hard HEAD~1
echo "status (clean - 변경 사라짐):"
git status --short
echo "log:"
git log --oneline

rm -rf "$REPO"
▶ 실행 결과
=== 초기 상태 ===
c3 commit 3 / b2 commit 2 / a1 commit 1

--- soft reset ---
M  file.txt    ← staged 상태 유지
b2 commit 2 / a1 commit 1

--- mixed reset ---
 M file.txt    ← unstaged
b2 commit 2 / a1 commit 1
ex02_revert.shpush된 커밋을 revert로 안전하게 되돌리기
CODE
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "실습용" && git config user.email "demo@example.com"
    git config commit.gpgsign false

echo "정상 기능" > app.py
git add . && git commit -q -m "feat: good feature"

echo "버그가 있는 코드" > app.py
git add . && git commit -q -m "feat: introduce bug"

echo "다른 정상 기능" > other.py
git add . && git commit -q -m "feat: another feature"

echo "=== revert 전 로그 ==="
git log --oneline

echo "=== revert 전 app.py ==="
cat app.py

# "feat: introduce bug" 커밋을 revert
BUGGY=$(git log --oneline | grep "introduce bug" | awk '{print $1}')
git revert "$BUGGY" --no-edit

echo ""
echo "=== revert 후 로그 (새 커밋 추가됨, 이력 보존) ==="
git log --oneline

echo ""
echo "=== revert 후 app.py (버그 이전으로 복원) ==="
cat app.py

rm -rf "$REPO"
▶ 실행 결과
=== revert 전 로그 ===
c3 feat: other feature
b2 feat: buggy code
a1 feat: good code

=== revert 후 로그 ===
d4 Revert "feat: buggy code"
c3 feat: other feature
b2 feat: buggy code
a1 feat: good code
ex03_restore.shrestore 로 변경 취소
CODE
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "실습용" && git config user.email "demo@example.com"
    git config commit.gpgsign false

echo "원본 내용" > file.txt
git add . && git commit -q -m "init"

# 수정 + 스테이징
echo "수정된 내용" > file.txt
git add file.txt

echo "[1] 수정 + 스테이징 후 status"
git status --short
echo "파일 내용: $(cat file.txt)"

# 스테이징만 취소 (워킹 디렉토리는 유지)
git restore --staged file.txt
echo ""
echo "[2] restore --staged 후 (unstaged, 내용 유지)"
git status --short
echo "파일 내용: $(cat file.txt)"

# 워킹 디렉토리도 취소
git restore file.txt
echo ""
echo "[3] restore 후 (clean, 원본으로 복원)"
git status --short
echo "파일 내용: $(cat file.txt)"

echo ""
echo "=== 새 파일에 restore 적용 불가 예시 ==="
echo "신규 파일" > newfile.txt
git restore newfile.txt 2>&1 || echo "→ untracked 파일은 restore 불가 (rm 으로 삭제)"

rm -rf "$REPO"
▶ 실행 결과
=== 수정 + 스테이징 후 ===
M  file.txt

=== --staged 취소 후 ===
 M file.txt

=== 워킹 디렉토리 취소 후 ===
(nothing)
파일 내용: 원본
ex04_stash.shstash 로 작업 임시 저장
CODE
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "실습용" && git config user.email "demo@example.com"
    git config commit.gpgsign false

echo "초기 앱 코드" > app.py
git add . && git commit -q -m "init: app.py"

# 새 기능 개발 중 (커밋 안 된 상태)
echo "WIP: 새 기능 작업 중" >> app.py
echo ""
echo "[1] 작업 중 status"
git status --short

# 긴급 수정 요청 → stash
git stash push -m "WIP: 새 기능 개발"
echo ""
echo "[2] stash 후 status (clean)"
git status --short

echo ""
echo "[3] stash list"
git stash list

# 긴급 hotfix
git switch -c hotfix/critical -q
echo "긴급 수정 완료" > hotfix.py
git add . && git commit -q -m "fix: critical hotfix"
git switch main -q
git merge hotfix/critical -q

echo ""
echo "[4] hotfix 병합 후 로그"
git log --oneline

# 원래 작업 복원
git stash pop
echo ""
echo "[5] stash pop 후 status"
git status --short
echo "app.py 내용:"
cat app.py

rm -rf "$REPO"
▶ 실행 결과
=== stash 후 status ===
(clean)

=== stash list ===
stash@{0}: WIP: 새 기능 개발

=== stash pop 후 status ===
 M app.py
app.py 내용: 초기 코드
WIP: 새 기능 개발 중

📝 과제 (exercises)

직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.

과제 1

문제 1 (hw01.sh)

목표: `git reset` 세 가지 모드를 직접 비교하는 스크립트를 작성하세요.

요구사항
  • 파일명: hw01.sh
정답 코드 펼치기 / 접기
SOLUTION
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "실습용" && git config user.email "demo@example.com"
    git config commit.gpgsign false

echo "a" > a.txt && git add . && git commit -q -m "add a"
echo "b" > b.txt && git add . && git commit -q -m "add b"
echo "c" > c.txt && git add . && git commit -q -m "add c"

echo "=== 초기 로그 ==="
git log --oneline

echo ""
echo "--- soft reset ---"
git reset --soft HEAD~1
git status --short
git log --oneline

echo ""
echo "--- mixed reset ---"
git reset HEAD~1
git status --short
git log --oneline

echo ""
echo "(re-commit b and c)"
git add . && git commit -q -m "re-commit b and c"

echo ""
echo "--- hard reset ---"
git reset --hard HEAD~1
git status --short
echo "남은 파일:"
ls

rm -rf "$REPO"
과제 2

문제 2 (hw02.sh)

목표: `git stash`로 작업을 임시 저장하고 복원하는 시나리오를 구현하세요.

요구사항
  • 파일명: hw02.sh
정답 코드 펼치기 / 접기
SOLUTION
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "실습용" && git config user.email "demo@example.com"
    git config commit.gpgsign false

echo "def main(): pass" > main.py
git add . && git commit -q -m "init: main.py"

echo "# WIP 작업" >> main.py
git stash push -m "WIP"
echo "stash 후 status: $(git status --short | wc -l) 변경 (0이어야 함)"

git switch -c fix/patch -q
echo "def patch(): pass" > patch.py
git add . && git commit -q -m "fix: add patch"
git switch main -q
git merge fix/patch -q

echo ""
echo "병합 후 로그:"
git log --oneline

git stash pop

echo ""
echo "stash pop 후 main.py:"
cat main.py

echo ""
echo "stash list (비어 있어야 함):"
git stash list

rm -rf "$REPO"
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗