브랜치, 제대로 — 만들고 옮기고 합치고 지우기
브랜치가 왜 41바이트짜리 포인터인지부터 — switch로 만들고 옮겨다니기, fast-forward와 3-way merge의 차이, 머지된 브랜치 정리, topic 브랜치 워크플로, 그리고 실무 팁까지. Git 시리즈 3편.
Series
Git 시리즈- 1Git 시작하기 — 가장 자주 쓰는 명령어 9개
- 2혼자 써도 따라야 할 Git Convention — 커밋 메시지부터 브랜치까지
- 3브랜치, 제대로 — 만들고 옮기고 합치고 지우기
1편에서 이어서 — 이제 브랜치
1편에서 첫 commit부터 push까지 한 줄로 걸어봤고, 2편에서 커밋 메시지·브랜치 네이밍 컨벤션을 잡았다. 이번 편은 브랜치 그 자체 — Git이 다른 버전 관리 도구와 갈라지는 지점이다.
다른 도구에서 "브랜치"는 보통 무겁다(복사·체크아웃에 시간이 걸린다). Git에서 브랜치는 거의 공짜라서, "일단 브랜치 따고 시작한다"가 기본 습관이 된다. 왜 가벼운지부터 보자 — 그걸 알면 merge, rebase, "되돌리기" 같은 다음 편 주제들이 전부 이 그림 위에서 돈다는 게 보인다.
브랜치는 왜 가벼운가
커밋 하나는 "그 시점의 스냅샷 + 부모 커밋(들)에 대한 포인터"다. 커밋들이 부모를 따라 줄줄이 엮여 그래프를 이룬다.
A ─── B ─── C여기서 브랜치는 그 커밋 중 하나를 가리키는 41바이트짜리 파일일 뿐이다. .git/refs/heads/main 안에는 커밋 SHA 한 줄이 들어있다. 그래서 git branch feat/x는 그 파일 하나를 새로 쓰는 일이고, 그래서 빠르다.
그럼 "지금 내가 어느 브랜치에 있나"는 누가 아는가? **HEAD**다. HEAD는 보통 어떤 브랜치를 가리키고, 그 브랜치가 어떤 커밋을 가리킨다.
A ─── B ─── C ← main ← HEAD커밋을 하면 현재 브랜치(=HEAD가 가리키는 브랜치)가 한 칸 앞으로 옮겨간다. 브랜치를 만들고 거기서 커밋하면:
A ─── B ─── C ← main
\
D ← feat/x ← HEADmain은 그대로 C에 있고, feat/x만 D로 갔다. 이게 전부다. "브랜치를 만든다 = 포인터를 하나 더 둔다", "브랜치를 옮겨다닌다 = HEAD가 가리키는 포인터를 바꾼다".
1) 브랜치 만들고 옮겨다니기
git branch feat/login # feat/login 브랜치 생성 (옮겨가진 않음)
git switch feat/login # feat/login 으로 이동
git switch -c feat/login # 위 둘을 한 번에: 생성 + 이동
git switch - # 직전 브랜치로 토글 (cd - 처럼)
git branch # 로컬 브랜치 목록 (* 가 현재 브랜치)
switch는 비교적 새 명령(Git 2.23+)이다. 예전엔git checkout -b feat/login을 썼는데,checkout이 "브랜치 이동"과 "파일 되돌리기"를 둘 다 하느라 헷갈려서switch(브랜치)와restore(파일)로 쪼개졌다. 새 글이면switch/restore를 쓰는 걸 권한다. 이 시리즈도 그렇게 쓴다.
작업 중인(아직 커밋 안 한) 변경이 있는 상태에서 git switch -c new를 하면, 그 변경이 새 브랜치로 따라온다. "어, 이거 main에서 작업하고 있었네" 싶을 때 git switch -c feat/x 한 줄로 옮겨 담을 수 있다.
2) 합치기 — fast-forward vs 3-way merge
브랜치에서 작업을 끝냈으면 다시 main으로 합쳐야 한다. git merge는 상황에 따라 두 가지로 다르게 동작한다.
fast-forward — 그냥 포인터를 앞으로
feat/x로 분기한 뒤 main에는 새 커밋이 안 생겼다면:
before: A ─── B ─── C ← main ← HEAD
\
D ─── E ← feat/x
git switch main
git merge feat/x →
after: A ─── B ─── C ─── D ─── E ← main ← HEAD, feat/xmain이 feat/x보다 뒤처져 있을 뿐, 갈라지진 않았다. 그래서 Git은 그냥 main 포인터를 E로 앞으로 당긴다(fast-forward). 새 커밋은 안 생긴다.
3-way merge — 갈라졌으면 머지 커밋
feat/x에서 작업하는 동안 main에도 누가 커밋을 했다면(=갈라졌다면):
before: A ─── B ─── C ─── F ← main ← HEAD
\
D ─── E ← feat/x
git switch main
git merge feat/x →
after: A ─── B ─── C ─── F ─── M ← main ← HEAD
\ /
D ─── E ┘ ← feat/x이때는 포인터만 옮길 수 없으니, Git이 양쪽(F와 E)과 공통 조상(C)을 보고 **새 머지 커밋 M**을 만든다. M은 부모가 둘(F, E)인 특별한 커밋이다. 로그에 "여기서 합쳐졌다"는 흔적이 남는다.
이 머지 커밋이 히스토리를 지저분하게 만든다고 느끼는 사람도 있다 — 그래서 rebase가 있다(다음 편). 반대로 "언제 합쳐졌는지 기록이 남아 좋다"는 쪽은 git merge --no-ff로 fast-forward 가능한 상황에서도 일부러 머지 커밋을 만든다. 정답은 없고, 팀이 정하면 된다.
3) 브랜치 정리
브랜치는 가벼우니까 자주 만들고, 끝나면 자주 버려라. 안 그러면 git branch가 죽은 브랜치로 가득 찬다.
git branch -d feat/login # 안전 삭제 — 다른 브랜치에 머지된 경우에만
git branch -D feat/login # 강제 삭제 — 머지 안 됐어도 지움 (주의)
git branch --merged main # main에 이미 머지된 브랜치들 (= 지워도 되는 것들)
git branch --no-merged main # 아직 main에 안 들어간 브랜치들 (= 작업 남은 것들)-d(소문자)는 "이 브랜치 내용이 어딘가 머지돼 있으니 지워도 안 잃는다"가 확인될 때만 삭제한다. 머지 안 된 브랜치를 -d로 지우려 하면 Git이 거부한다 — 안전장치다. 정말 버릴 작업이면 -D(대문자)로 강제.
4) 워크플로 — topic 브랜치와 long-running 브랜치
브랜치를 어떻게 굴리느냐는 결국 두 종류로 정리된다.
- long-running 브랜치 — 오래 살아있는 줄기. 보통
main하나면 충분하다.main은 항상 배포 가능한 상태로 유지한다(깨진 코드를 main에 직접 커밋하지 않는다). - topic 브랜치 — 기능 하나, 버그 하나처럼 수명이 짧은 브랜치.
feat/login,fix/typo처럼. 작업 끝나면main에 머지하고 삭제.
규칙은 단순하다: 새 작업은 무조건 topic 브랜치에서 시작 → 끝나면 main에 합치고 브랜치 삭제. 혼자 하는 프로젝트라도 이 습관을 들이면, 작업 중간에 "급한 버그부터 고쳐야 해"가 와도 깔끔하게 갈아탈 수 있다.
한 사이클 — feature 브랜치로 작업 후 머지
# 1. main에서 새 작업 시작
git switch main
git switch -c feat/login
# 2. 작업하고 커밋
# ...코드 수정...
git add .
git commit -m "feat(auth): add login form"
# 3. (선택) 그 사이 main이 갱신됐으면 가져와서 따라잡기
git fetch
git merge origin/main # 또는 rebase — 다음 편 주제
# 4. main으로 돌아가 머지
git switch main
git merge feat/login
# 5. 다 끝났으니 브랜치 정리
git branch -d feat/login원격까지 올리는 흐름(push, PR)은 5편(리모트와 협업)에서 이어서 다룬다.
실무에서 — 알아두면 좋은 것들
git switch -— 직전 브랜치로 토글.cd -처럼. 브랜치 두 개를 오가며 작업할 때 손에 붙여두면 빠르다.- 브랜치 이름에 슬래시(
feat/login,fix/header) — GitHub UI나 일부 Git GUI가feat/아래로 폴더처럼 묶어준다. 브랜치가 많아져도 한눈에 들어온다. - 커밋 안 한 변경도 따라온다 —
git switch -c new하면 작업 중인 변경이 새 브랜치로 옮겨진다. "main에서 작업하고 있었네"의 구제책. - 머지된 브랜치 한 번에 청소:
git branch --merged main | grep -v -e '^\*' -e 'main' | xargs -r git branch -d git branch -vv— 각 로컬 브랜치의 upstream + 앞/뒤 커밋 수까지 한 줄로.git branch --sort=-committerdate는 최근 작업한 순서로 정렬 → "그 브랜치 이름이 뭐였더라" 할 때.- detached HEAD에서 빠져나오기 — 태그나 특정 커밋(
git switch --detach <sha>)을 체크아웃하면HEAD가 브랜치가 아닌 커밋을 직접 가리키는 상태(detached)가 된다. 거기서 커밋했다면 잃기 전에git switch -c rescue로 브랜치를 붙여라. 그냥 둘러보기만 했으면git switch -로 복귀. - alias —
git config --global alias.sw switch,alias.co checkout,alias.br branch. 매일 치는 명령은 두 글자로 줄여두면 누적 절약이 크다.
자주 막히는 지점
error: Your local changes ... would be overwritten by checkout— 작업 중인 변경 때문에 브랜치 이동이 막힘. 셋 중 하나: 커밋하거나,git stash로 잠깐 치워두거나(6편),git switch -c new로 변경을 새 브랜치에 담아 가거나.error: The branch 'feat/x' is not fully merged—git branch -d가 거부함.git branch --no-merged main으로 정말 머지 안 된 게 맞는지 확인. 버려도 되는 작업이면-D로 강제 삭제.- detached HEAD에서 한 커밋이 사라진 것 같다 — 안 사라진다.
git reflog에 다 남아있다(6편).git switch -c rescue <reflog의 SHA>로 되살린다. - 머지하다 충돌(
CONFLICT) —<<<<<<<마커가 박힌다. 이건 다음 편(merge vs rebase)에서 충돌 마커 읽는 법·해결 흐름·--abort까지 제대로 다룬다. 일단 지금은git merge --abort로 머지 전으로 되돌릴 수 있다는 것만 기억.
정리 및 다음 편
- 브랜치 = 커밋을 가리키는 41바이트 포인터,
HEAD= "지금 어느 브랜치". 그래서 가볍다 → 자주 만들고 자주 버려라. - 머지에는 두 종류: 갈라지지 않았으면 fast-forward(포인터만 당김), 갈라졌으면 3-way merge(머지 커밋 생성).
main은 항상 배포 가능하게, 작업은 topic 브랜치에서.
다음 편(4편)은 그 머지 커밋이 거슬리는 사람을 위한 rebase — 그리고 "merge냐 rebase냐"를 무엇으로 정할지, 충돌이 났을 때 어떻게 빠져나오는지(--continue/--abort), --force-with-lease까지 다룬다.