🔀
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 # 범위 revertpush된 브랜치에서 이력을 보존해야 할 때 사용한다.
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` | 서랍에 잠깐 넣기 | — |
자주 하는 실수
- **push된 브랜치에 `reset --hard`** — 팀원의 이력이 꼬이고 강제 push가 필요해진다.
- **`reset`과 `revert` 혼동** — 공유 브랜치에서는 항상 `revert`를 사용한다.
- **stash 후 잊어버리기** — `git stash list`로 주기적으로 확인하고 불필요한 것은 삭제한다.
- **`restore` 후 복구 불가** — 스테이징되지 않은 변경을 `restore`하면 reflog로도 복구 불가능하다.
- **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응용:
- `git reset --hard HEAD~3` 로 3개 커밋을 날린 뒤 `git reflog`로 복구해 보세요.
- `git stash branch <브랜치명>`으로 stash를 새 브랜치로 바로 분리해 보세요.
💻 예제 (examples)
실제로 실행해 결과를 확인할 수 있는 예제입니다.
ex01_reset.sh— soft·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 1ex02_revert.sh— push된 커밋을 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 codeex03_restore.sh— restore 로 변경 취소
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.sh— stash 로 작업 임시 저장
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"