1. 아직도 git checkout 쓰시나요?
git checkout은 Git의 만능 도구였습니다. 브랜치를 전환하고, 파일을 복구하고, 새 브랜치를 만드는 것까지 — 하나의 명령어가 너무 많은 일을 했죠.
# checkout이 했던 일들
git checkout develop # 브랜치 이동
git checkout -b feature/login # 브랜치 생성 + 이동
git checkout -- src/app.ts # 파일 변경 취소
git checkout HEAD -- . # 워킹 트리 복구
문제는 이 명령어들의 의도가 완전히 다르다는 것입니다. "브랜치를 바꾸겠다"와 "파일을 되돌리겠다"는 전혀 다른 작업인데, 같은 checkout으로 처리하니 실수가 잦았습니다. 특히 git checkout -- .은 되돌릴 수 없는 파괴적 명령어인데, 브랜치 전환과 비슷한 문법이라 혼동하기 쉬웠습니다.
Git 2.23(2019년)부터 이 문제를 해결하기 위해 두 가지 명령어가 도입됐습니다.
git switch — 브랜치 전환 전용
# Before: checkout
git checkout develop
git checkout -b feature/login
# After: switch
git switch develop # 브랜치 이동
git switch -c feature/login # 브랜치 생성 + 이동 (-c = create)
git switch - # 이전 브랜치로 복귀
switch는 브랜치 전환만 담당합니다. 실수로 파일을 날릴 위험이 없어요.
git restore — 파일 복구 전용
# Before: checkout
git checkout -- src/app.ts
git checkout HEAD -- .
git reset HEAD src/app.ts
# After: restore
git restore src/app.ts # 워킹 트리 변경 취소 (unstaged)
git restore --staged src/app.ts # 스테이징 취소 (unstage)
git restore --staged --worktree . # 둘 다 취소
git restore --source=HEAD~3 app.ts # 3커밋 전 상태로 복구
restore는 파일 복구만 담당합니다. --staged로 unstage, --source로 특정 커밋 버전 복구까지 직관적으로 할 수 있습니다.
핵심 비교
| 용도 | Before (checkout) |
After |
|---|---|---|
| 브랜치 이동 | git checkout develop |
git switch develop |
| 브랜치 생성+이동 | git checkout -b feat/x |
git switch -c feat/x |
| 파일 변경 취소 | git checkout -- file |
git restore file |
| 스테이징 취소 | git reset HEAD file |
git restore --staged file |
| 이전 브랜치 | git checkout - |
git switch - |
checkout이 사라지는 건 아닙니다. 하위 호환성을 위해 계속 동작하지만, 새 코드에서는switch/restore를 쓰는 것이 권장됩니다.
2. 브랜치 전략: 무겁지 않게, 그러나 체계적으로
고전적인 Git Flow(main, develop, feature, release, hotfix)는 체계적이지만 무겁습니다. 모든 프로젝트에 5단계 브랜치가 필요한 건 아닙니다.
명명 규칙 — 일관성이 핵심
# 좋은 예
feature/user-auth # 또는 feat/user-auth
hotfix/login-error
release/v1.2.0
chore/update-deps
# 나쁜 예
my-branch # 무슨 브랜치인지 모름
fix # 너무 모호
john/stuff # 의미 없음
타입 접두어(feature/, hotfix/, chore/)를 쓰면 브랜치 목적이 한눈에 보입니다. 이슈 번호를 포함하면 더 좋습니다.
git switch -c feat/issue-123-oauth-integration
상황별 전략 선택
| 전략 | 적합한 상황 | 핵심 |
|---|---|---|
| GitHub Flow | 빠른 배포, 소규모 팀 | main + feature 브랜치만 |
| Git Flow | 릴리스 주기가 있는 제품 | main + develop + feature/release/hotfix |
| Trunk-based | CI/CD 성숙, 대규모 팀 | main에 직접 커밋 + feature flag |
사이드 프로젝트에서는 GitHub Flow가 가장 현실적입니다. main에서 feature 브랜치를 따고, PR로 머지하면 끝입니다. 다만 한 가지 규칙은 지키세요 — main에 직접 코드 커밋하지 않기:
# main에서 실수로 코드를 수정했을 때
git switch -c feature/accidental-changes # ← 브랜치를 만들어서 이동
git push -u origin feature/accidental-changes
# 그다음 PR을 만들어 머지
3. PR Merge 전략: 히스토리를 어떻게 관리할 것인가
PR을 머지하는 방법은 세 가지입니다. "히스토리를 예쁘게 만드는 것"이 목적이 아니라, 코드 리뷰와 추적을 용이하게 하는 것이 진짜 목적입니다.
Merge Commit (--no-ff)
gh pr merge --merge # Merge commit 생성
* Merge PR #40 (main)
|\
| * fix: address review feedback
| * feat: add tool target display
|/
* Merge PR #39 (main)
- 각 PR의 커밋 히스토리가 그대로 보존됩니다
- "이 PR에서 무슨 변경이 있었나"를 추적하기 좋습니다
- Git Flow에서 feature → develop 머지 시 권장합니다
Squash and Merge
gh pr merge --squash # 모든 커밋을 1개로 합쳐서 머지
* feat: add tool target display (#40) (main)
* feat: add lines changed widget (#39)
typo fix,WIP,debug같은 자잘한 커밋이 사라집니다- main 브랜치가 깔끔해집니다
- 다만 세부 히스토리를 잃게 됩니다
Rebase and Merge
gh pr merge --rebase # 커밋을 main 위에 재배치
* fix: address review feedback (main)
* feat: add tool target display
* feat: add lines changed widget
- 일직선 히스토리를 만듭니다
- 각 커밋이 보존되지만, 머지 포인트가 없어 PR 경계가 불분명합니다
어떤 전략을 선택할까?
개인적으로 Merge Commit을 사용합니다. 이유는 단순합니다:
- PR 단위로 변경사항을 추적할 수 있습니다
git log --merges로 PR 목록을 볼 수 있습니다- 문제가 생겼을 때 PR 단위로
revert할 수 있습니다
# Merge commit이면 PR 단위 revert가 간단
git revert -m 1 <merge-commit-hash>
4. Git Worktree: stash는 이제 그만
긴급 hotfix가 필요할 때, 보통 이렇게 하시죠?
# 구식 방법: stash → 브랜치 이동 → 작업 → 돌아오기
git stash
git switch main
git switch -c hotfix/urgent-bug
# ... 수정 작업 ...
git switch feature/my-work
git stash pop # ← stash 충돌 가능성!
stash는 임시 저장소지 작업 공간 관리 도구가 아닙니다. 충돌이 나면 골치 아프고, 여러 stash가 쌓이면 어느 게 어느 건지 헷갈립니다.
Worktree로 병렬 작업
git worktree는 하나의 리포지토리에서 여러 브랜치를 동시에 다른 폴더로 체크아웃합니다.
# 현재 작업 중인 feature 브랜치는 그대로 두고
# 옆에 hotfix 작업 공간을 새로 만듦
git worktree add ../my-project-hotfix hotfix/urgent-bug
# 별도 폴더에서 hotfix 작업
cd ../my-project-hotfix
# ... 수정 → 커밋 → 푸시 ...
# 완료 후 정리
cd ../my-project
git worktree remove ../my-project-hotfix
핵심 장점:
- 컨텍스트 전환 비용 제로: 파일 시스템 수준에서 분리되므로, IDE를 두 개 열어놓고 동시 작업이 가능합니다
- 빌드 캐시 보존: 기존 브랜치의
node_modules나 빌드 결과물이 그대로 유지됩니다 - stash 충돌 없음: 각 worktree가 독립된 워킹 트리를 가집니다
Worktree 관리 명령어
git worktree list # 현재 worktree 목록
git worktree add <path> <branch> # 새 worktree 생성
git worktree remove <path> # worktree 제거
git worktree prune # 삭제된 worktree 참조 정리
Claude Code도
--worktree옵션으로 에이전트 세션을 격리된 worktree에서 실행할 수 있습니다. 이러면 에이전트가 현재 작업 중인 파일을 건드리지 않습니다.
5. 좀비 브랜치 청소: git gone 만들기
여기서부터가 실전입니다. PR을 머지하면 GitHub에서 remote 브랜치를 삭제하죠. 하지만 로컬 브랜치는 자동으로 삭제되지 않습니다. 프로젝트를 몇 달 하다 보면 이런 상태가 됩니다:
$ git branch
feature/p1-benchmark-improvements # ← remote에서 삭제됨
feature/p2-batch-a-widgets # ← remote에서 삭제됨
feat/session-id-and-sessions-list # ← 작업 중
* main
이 "좀비 브랜치"들이 쌓이면 어느 게 살아있고 어느 게 죽었는지 구분이 안 됩니다.
Step 1: Remote 동기화
$ git fetch --prune
From github.com:user/my-project
- [deleted] (none) -> origin/feature/p1-benchmark-improvements
- [deleted] (none) -> origin/feature/p2-batch-a-widgets
git fetch --prune(또는 -p)은 remote에서 삭제된 브랜치의 tracking 참조를 정리합니다. 하지만 로컬 브랜치 자체는 아직 남아있습니다.
Step 2: [gone] 브랜치 확인
$ git branch -v
feature/p1-benchmark-improvements cbac771 [gone] feat: implement P1
feature/p2-batch-a-widgets d37d1ce [gone] fix: PR review feedback
feat/session-id-and-sessions-list 434d736 feat(command): add sessions
* main 56f23fb Merge pull request #41
[gone]이 핵심입니다. remote tracking branch가 삭제된 로컬 브랜치에 이 마커가 붙습니다.
Step 3: 한 번에 청소
$ git branch -v | grep '\[gone\]' | sed 's/^[+* ]//' | awk '{print $1}' | while read b; do
git branch -D "$b"
done
Deleted branch feature/p1-benchmark-improvements (was cbac771).
Deleted branch feature/p2-batch-a-widgets (was d37d1ce).
이 과정을 매번 수동으로 하기는 번거로우니, alias로 만들겠습니다.
git gone Alias 등록
.gitconfig에 직접 작성합니다:
# ~/.gitconfig
[alias]
gone = "!f() { git fetch -p; for b in $(git branch -v | grep '\\[gone\\]' | sed 's/^[+* ]//' | awk '{print $1}'); do git branch -D $b; done; }; f"
이제 터미널에서 git gone 한 번이면 끝입니다:
$ git gone
Deleted branch feature/p1-benchmark-improvements (was cbac771).
Deleted branch feature/p2-batch-a-widgets (was d37d1ce).
좀비 브랜치가 없으면 아무 출력 없이 조용히 완료됩니다.
자동 prune 설정
매번 --prune을 붙이기 귀찮다면, 글로벌 설정으로 자동화할 수 있습니다:
git config --global fetch.prune true
이 설정을 켜두면 git fetch나 git pull 할 때마다 자동으로 prune이 실행됩니다.
6. .gitconfig Alias Escaping — 실제로 삽질한 이야기
git gone alias를 만드는 과정에서 .gitconfig의 escaping 지옥을 경험했습니다. 이 부분은 문서에도 잘 나와있지 않아서 공유합니다.
시도 1: git config 명령어로 등록
git config --global alias.gone \
'!git fetch -p && git branch -v | grep "\[gone\]" | ...'
결과: expansion of alias 'gone' failed; '!git' is not a git command
git config가 ! 앞에 \를 자동으로 붙여서 \!git이 저장됐습니다.
시도 2: .gitconfig에 직접 작성 (과도한 escaping)
# 잘못된 예 — backslash가 너무 많음
gone = "!f() { ... grep '\\\\[gone\\\\]' ... }; f"
결과: grep에 \\[gone\\]이 전달되어, 리터럴 백슬래시 + [gone]을 찾게 됩니다. 당연히 매칭되지 않습니다.
시도 3: 정확한 escaping
.gitconfig의 double-quoted 값에서 escaping 규칙을 이해해야 합니다:
.gitconfig 파일: \\[gone\\]
↓ (git config이 \\ → \ 로 해석)
shell에 전달: \[gone\]
↓ (single quote 안이므로 그대로 grep에 전달)
grep이 받는 값: \[gone\] → [를 리터럴로 매칭 ✓
# 정답 — backslash 정확히 2개씩
gone = "!f() { git fetch -p; for b in $(git branch -v | grep '\\[gone\\]' | sed 's/^[+* ]//' | awk '{print $1}'); do git branch -D $b; done; }; f"
Escaping 규칙 정리
.gitconfig 파일 |
git이 해석한 결과 | 비고 |
|---|---|---|
\\ |
\ |
백슬래시 1개 |
\" |
" |
큰따옴표 |
\n |
줄바꿈 | newline |
\t |
탭 | tab |
팁: 복잡한 alias는 git config 명령어 대신 .gitconfig 파일을 직접 편집하는 것이 훨씬 안전합니다. 명령어로 설정하면 shell escaping + git escaping이 이중으로 적용되어 디버깅이 매우 어렵습니다.
macOS 주의사항: xargs -r
Linux에서 흔히 쓰는 패턴이 macOS에서 안 되는 경우가 있습니다:
# Linux (GNU coreutils) — 동작함
... | xargs -r git branch -D # -r: 입력 없으면 실행하지 않음
# macOS (BSD) — -r 미지원
... | xargs -r git branch -D # ← 무시되거나 에러
macOS의 BSD xargs는 -r (--no-run-if-empty) 플래그를 지원하지 않습니다. 대신 for ... in $(...) 루프나 while read 패턴을 사용하세요:
# macOS 호환 패턴
for b in $(git branch -v | grep '\\[gone\\]' | awk '{print $1}'); do
git branch -D $b
done
7. 협업 워크플로우: Issue → Branch → PR → Cleanup
지금까지 배운 것을 조합하면, 하나의 완성된 워크플로우가 됩니다:
전체 흐름
# 1. 이슈 확인 후 브랜치 생성
git switch -c feat/issue-123-oauth
# 2. 작업 → 커밋
git add src/auth.ts
git commit -m "feat: add OAuth integration"
# 3. 푸시 + PR 생성
git push -u origin feat/issue-123-oauth
gh pr create --title "feat: add OAuth" --body "Closes #123"
# 4. PR 머지 (GitHub 웹 또는 CLI)
gh pr merge --merge
# 5. 로컬 정리
git switch main
git gone # ← fetch + prune + 좀비 브랜치 삭제
긴급 hotfix가 끼어들 때
# 현재 feature 작업 중인데 hotfix 요청이 옴
# 1. worktree로 별도 작업 공간 생성
git worktree add ../my-project-hotfix -b hotfix/login-error main
# 2. hotfix 작업
cd ../my-project-hotfix
# ... 수정 ...
git commit -m "fix: resolve login error"
git push -u origin hotfix/login-error
gh pr create --title "fix: login error" --body "Closes #456"
# 3. 머지 완료 후 정리
cd ../my-project
git worktree remove ../my-project-hotfix
git gone # hotfix 브랜치도 같이 정리됨
# 4. 원래 feature 작업 계속 (중단 없었음!)
PR 본문에 이슈 연결
PR 본문에 키워드를 넣으면 머지 시 이슈가 자동으로 닫힙니다:
## Summary
- OAuth 로그인 통합
## Test plan
- [ ] Google OAuth 테스트
- [ ] 토큰 갱신 확인
Closes #123
지원되는 키워드: Closes, Fixes, Resolves (대소문자 무관)
8. 추천 .gitconfig 설정
이 글에서 다룬 내용을 바탕으로, 추천하는 .gitconfig 설정입니다:
[alias]
# 좀비 브랜치 일괄 삭제
gone = "!f() { git fetch -p; for b in $(git branch -v | grep '\\[gone\\]' | sed 's/^[+* ]//' | awk '{print $1}'); do git branch -D $b; done; }; f"
# 깔끔한 로그
lg = log --oneline --graph --decorate --all -20
# 마지막 커밋 수정 (주의: push 전에만!)
amend = commit --amend --no-edit
# 현재 브랜치명
current = rev-parse --abbrev-ref HEAD
[fetch]
# fetch 시 자동 prune
prune = true
[push]
# 현재 브랜치만 push
default = current
# 태그 자동 push
followTags = true
[pull]
# pull 시 rebase (merge commit 방지)
rebase = true
핵심 개념 정리
| 개념 | 명령어 | 용도 |
|---|---|---|
| 브랜치 전환 | git switch |
checkout 대체 |
| 파일 복구 | git restore |
checkout -- 대체 |
| 병렬 작업 | git worktree add |
stash 대체 |
| Remote 동기화 | git fetch -p |
삭제된 remote 정리 |
| 좀비 정리 | git gone (alias) |
[gone] 로컬 브랜치 삭제 |
| 이슈 자동 닫기 | PR에 Closes #N |
머지 시 이슈 연동 |
FAQ
Q: git switch와 git checkout을 섞어 써도 되나요?
A: 네, 둘 다 동작합니다. 하지만 새로운 습관을 들이려면 하나로 통일하는 것이 좋습니다. switch/restore가 의도가 더 명확하므로 권장합니다.
Q: git gone이 작업 중인 브랜치를 삭제할 수도 있나요?
A: [gone] 마커는 remote tracking branch가 삭제된 경우에만 붙습니다. remote에 push한 적 없는 순수 로컬 브랜치는 대상이 아닙니다. 다만, 현재 체크아웃된 브랜치는 git branch -D로 삭제할 수 없으므로 안전합니다.
Q: git worktree와 git clone의 차이는 뭔가요?
A: clone은 완전히 독립된 리포지토리 복사본을 만들지만, worktree는 같은 .git 디렉토리를 공유합니다. 그래서 worktree는 디스크 공간을 거의 차지하지 않고, 브랜치/커밋/stash가 모든 worktree에서 공유됩니다.
Q: .gitconfig alias에서 !는 왜 필요한가요?
A: !는 alias를 shell 명령어로 실행하라는 뜻입니다. 없으면 git 서브커맨드로 해석되어, 파이프(|)나 && 같은 shell 문법을 사용할 수 없습니다.
Q: fetch.prune = true를 설정하면 git gone에서 fetch -p가 중복 아닌가요?
A: 맞습니다. 하지만 git gone은 독립적으로 동작해야 하므로 fetch -p를 포함시키는 것이 안전합니다. 이미 prune된 상태면 추가 네트워크 요청 없이 빠르게 넘어갑니다.
참고 자료
- Git 공식 문서 - git switch
- Git 공식 문서 - git restore
- Git 공식 문서 - git worktree
- Git 공식 문서 - git config
- Git Aliases: Advanced and Beyond — Eficode
- Git Prune — Atlassian Tutorial