3.1 브랜치의 실체
브랜치는 커밋 하나를 가리키는 41바이트 텍스트 파일입니다. .git/refs/heads/main을 열어보면 커밋 해시 한 줄이 전부입니다.
그래서 브랜치 생성은 O(1)이고, 100개를 만들어도 용량이 안 늘고, 삭제해도 커밋 자체는 사라지지 않습니다(도달 경로만 사라짐).
┌── feature (포인터)
▼
A ─ B ─ C
▲
└── main (포인터)
HEAD → 현재 체크아웃된 브랜치를 가리키는 포인터3.2 브랜치 기본 명령
git branch # 로컬 목록 (안전)
git branch -a # 원격 포함 전체
git branch -v # 마지막 커밋까지
git branch --merged # 현재 브랜치에 병합 완료된 것 ★
git branch --no-merged # 아직 병합 안 된 것 ★
git switch -c feature/login # 생성 + 이동 (Git 2.23+, 권장)
git switch main # 이동
git switch - # 직전 브랜치로 토글
git checkout -b feature/login # 구버전 방식 (동일 동작)
git branch -d feature/login # 삭제 — 병합 안 됐으면 거부함 ★ 안전
git branch -D feature/login # 강제 삭제 ⚠ 병합 여부 무시
git push origin --delete feature/login # 원격 브랜치 삭제 ⚠-d와 -D의 차이가 곧 안전 설계입니다. -d는 "이 브랜치의 작업이 어딘가에 병합되었는가?"를 검사하고, 아니면 거부합니다. 거부당했다는 것은 "아직 살릴 작업이 남아있다"는 Git의 경고입니다. 습관적으로 -D를 치면 이 안전장치가 통째로 무력화됩니다. -d가 거부하면 --no-merged로 확인부터 하세요.
git branch --no-merged main # main에 안 들어간 브랜치들
git log main..feature/login # 이 브랜치에만 있는 커밋 확인switch/restore가 checkout을 대체한 이유도 같은 맥락입니다. checkout은 "브랜치 이동", "파일 복원", "임의 커밋으로 이동" 세 가지 전혀 다른 일을 하나의 명령에 몰아넣어서, 오타 하나로 의도치 않은 파일 덮어쓰기가 일어났습니다. Git 2.23부터 switch(브랜치)와 restore(파일)로 책임을 분리했습니다.
3.3 브랜치 네이밍 컨벤션
main # 프로덕션
develop # 개발 통합
feature/login-oauth # 기능
fix/header-overflow # 버그
hotfix/payment-500 # 긴급 수정
release/1.4.0 # 릴리스 준비
chore/upgrade-deps # 잡무
docs/api-guide # 문서이슈 번호를 붙이면 추적이 쉬워집니다: feature/42-login-oauth
3.4 merge — 세 가지 방식
(1) Fast-forward — 갈라진 게 없을 때
병합 전 병합 후
main ──▶ A ─ B main ──────────▶ A ─ B ─ C ─ D
└─ C ─ D ◀feature ▲featuremain이 그냥 앞으로 이동합니다. 새 커밋이 생기지 않고 히스토리가 일직선입니다.
(2) 3-way merge — 양쪽 다 진행됐을 때
A ─ B ─ E ─ F ──── M ◀main M = 병합 커밋 (부모가 2개)
└─ C ─ D ─────┘(3) Squash merge — 여러 커밋을 하나로
A ─ B ─ E ─ F ─ S ◀main S = C,D의 변경을 합친 새 커밋 1개
└─ C ─ D (연결되지 않음)git merge feature # 기본 (가능하면 FF)
git merge --no-ff feature # 항상 병합 커밋 생성 ★
git merge --squash feature # 변경만 가져와 스테이징 (커밋은 수동)
git merge --abort # 충돌 중 원상복구 ★ 안전--no-ff를 권장하는 이유: fast-forward는 "이 기능이 언제 어디서 시작해서 언제 합쳐졌는지"라는 정보를 지웁니다. --no-ff로 병합 커밋을 남기면 그래프에서 기능 단위가 시각적으로 보이고, 문제가 생겼을 때 병합 커밋 하나만 revert하면 기능 전체를 되돌릴 수 있습니다.
3.5 rebase — 히스토리를 다시 쓰기
rebase 전 git rebase main 후
A ─ B ─ E ─ F ◀main A ─ B ─ E ─ F ◀main
└─ C ─ D ◀feature └─ C' ─ D' ◀featurefeature의 커밋들을 떼어내서 main 끝에 새로 만들어 붙입니다. C와 C'는 내용은 같지만 해시가 다른 별개의 커밋입니다. 이것이 rebase가 "히스토리 변경"인 이유입니다.
git switch feature
git rebase main # feature를 main 위로 재배치
git rebase --continue # 충돌 해결 후 진행
git rebase --skip # 이 커밋 건너뛰기
git rebase --abort # 전체 취소 ★ 안전| merge | rebase | |
|---|---|---|
| 히스토리 | 실제 발생 순서 보존 | 일직선으로 정리 |
| 커밋 해시 | 유지 | 변경됨 |
| 병합 커밋 | 생김 | 안 생김 |
| 추적성 | 높음 | 낮음 (실제 시점 소실) |
| 안전성 | 높음 | 공유 브랜치에선 위험 |
rebase 황금률 — 원격에 push한 브랜치는 rebase 하지 않는다.
왜? 동료가 이미 내
feature브랜치를 받아갔다고 합시다. 내가 rebase하면 C는 C'가 되고, 둘은 Git 입장에서 완전히 다른 커밋입니다. 동료가 다음에 pull 하면 C와 C'가 둘 다 존재하는 엉망진창 히스토리가 되고, 심하면 동료의 후속 커밋이 고아가 됩니다.예외는 "나만 쓰는 브랜치" 입니다. 내 PR 브랜치를 정리하려고 rebase하는 건 일반적인 관행입니다. 이때 push는 반드시
--force-with-lease를 씁니다(5편의 force-with-lease 참고).
3.6 충돌 해결
git merge feature
# CONFLICT (content): Merge conflict in src/app.ts
git status # 어떤 파일이 충돌했는지 (Unmerged paths)<<<<<<< HEAD
const timeout = 3000; // 현재 브랜치(내 쪽)
=======
const timeout = 5000; // 들어오는 브랜치(상대 쪽)
>>>>>>> feature# 1. 파일을 열어 <<<<<<<, =======, >>>>>>> 마커를 지우고 최종 코드로 정리
# 2. 해결 표시
git add src/app.ts
# 3. 완료
git commit # merge인 경우
git rebase --continue # rebase인 경우유용한 도구들:
git diff --name-only --diff-filter=U # 충돌 파일 목록만
git restore --ours file.ts # 내 쪽 통째로 채택
git restore --theirs file.ts # 상대 쪽 통째로 채택
git mergetool # 시각적 병합 도구
git merge --abort # 포기하고 원래대로 ★주의: rebase 중에는 --ours와 --theirs의 의미가 뒤집힙니다. rebase는 "내 커밋을 상대 위에 다시 올리는" 작업이라 기준(ours)이 재배치 대상 브랜치가 됩니다. 헷갈리면 수동으로 편집하세요.
반복되는 충돌이 지겹다면:
git config --global rerere.enabled true
# rerere = REuse REcorded REsolution
# 같은 충돌을 다시 만나면 이전 해결 방식을 자동 적용