sy/dev
Study
13 min read

브랜치, 제대로 — 만들고 옮기고 합치고 지우기

브랜치가 왜 41바이트짜리 포인터인지부터 — switch로 만들고 옮겨다니기, fast-forward와 3-way merge의 차이, 머지된 브랜치 정리, topic 브랜치 워크플로, 그리고 실무 팁까지. Git 시리즈 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 ← HEAD

main은 그대로 C에 있고, feat/xD로 갔다. 이게 전부다. "브랜치를 만든다 = 포인터를 하나 더 둔다", "브랜치를 옮겨다닌다 = 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/x

mainfeat/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이 양쪽(FE)과 공통 조상(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 -로 복귀.
  • aliasgit 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 mergedgit 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까지 다룬다.

참고 자료

Comments