들어가며
Git은 개발자가 매일 사용하지만, 대부분은 add-commit-push 루프에서 벗어나지 못한다. merge 충돌이 발생하면 당황하고, rebase는 두렵고, bisect나 worktree의 존재조차 모르는 경우가 많다.
이 글에서는 Git의 내부 구조부터 시작해서 실무에서 반드시 알아야 할 고급 명령어, 브랜칭 전략, PR 리뷰 문화, 모노레포 관리, 위기 상황 대처법까지 **Git의 모든 것**을 350줄 이상에 걸쳐 총정리한다.
1. Git 내부 구조 이해하기
Git을 제대로 쓰려면 내부 구조를 이해해야 한다. Git은 본질적으로 **키-값 저장소(content-addressable filesystem)**이다.
1.1 네 가지 핵심 객체
Git의 모든 데이터는 네 가지 객체 타입으로 저장된다.
| 객체 | 역할 | 설명 |
|------|------|------|
| blob | 파일 내용 | 파일의 스냅샷 (이름 없이 내용만) |
| tree | 디렉토리 | blob과 다른 tree를 참조하는 목록 |
| commit | 스냅샷 | tree + 부모 커밋 + 메타데이터 |
| tag | 이름표 | 특정 커밋에 붙이는 annotated tag |
각 객체는 SHA-1 해시로 식별된다. 같은 내용이면 같은 해시가 나오므로, Git은 자연스럽게 중복을 제거한다.
객체 타입 확인
git cat-file -t HEAD
커밋 객체 내용 보기
git cat-file -p HEAD
tree 객체 탐색
git ls-tree HEAD
1.2 세 가지 영역
Git에는 세 가지 핵심 영역이 있다.
- **Working Directory** -- 실제 파일이 있는 디렉토리. 우리가 편집하는 공간이다.
- **Staging Area (Index)** -- 다음 커밋에 포함될 변경 사항을 준비하는 영역이다.
- **Repository (.git)** -- 커밋 히스토리와 모든 객체가 저장되는 공간이다.
세 영역 간 파일 이동 흐름
Working -> Staging
git add file.txt
Staging -> Repository
git commit -m "feat: add file"
Repository -> Working (특정 파일 되돌리기)
git checkout HEAD -- file.txt
1.3 HEAD와 ref
- **HEAD** -- 현재 체크아웃된 커밋을 가리키는 포인터이다. 보통 브랜치를 가리킨다.
- **branch** -- 특정 커밋을 가리키는 이동 가능한 포인터이다.
- **tag** -- 특정 커밋을 가리키는 고정 포인터이다.
HEAD가 가리키는 곳 확인
cat .git/HEAD
ref: refs/heads/main
브랜치가 가리키는 커밋 확인
cat .git/refs/heads/main
Detached HEAD 상태 만들기
git checkout HEAD~2
2. 필수 명령어 치트시트
매일 쓰는 명령어를 상황별로 정리했다.
2.1 기본 워크플로우
저장소 초기화 / 클론
git init
git clone https://github.com/user/repo.git
변경 사항 확인
git status
git diff # unstaged 변경
git diff --staged # staged 변경
git diff HEAD # 전체 변경
스테이징 & 커밋
git add -p # 대화형 부분 스테이징
git commit -m "feat: login" # 커밋
git commit --amend # 마지막 커밋 수정
원격 동기화
git fetch origin # 원격 변경만 가져오기
git pull --rebase origin main # fetch + rebase
git push origin feature/login # 푸시
2.2 브랜치 관리
브랜치 생성 & 이동
git branch feature/auth
git checkout -b feature/auth # 생성 + 이동
git switch -c feature/auth # Git 2.23+ 추천
브랜치 목록
git branch -a # 로컬 + 원격
git branch --merged # 병합 완료된 브랜치
git branch -d feature/old # 안전 삭제
git branch -D feature/old # 강제 삭제
원격 브랜치 정리
git remote prune origin
git fetch -p
2.3 stash -- 작업 임시 저장
작업 중 급하게 다른 브랜치로 이동해야 할 때 stash를 사용한다.
현재 변경 사항 저장
git stash
git stash push -m "WIP: login form"
stash 목록 확인
git stash list
복원
git stash pop # 꺼내면서 삭제
git stash apply stash@{0} # 꺼내되 유지
특정 파일만 stash
git stash push -m "partial" -- src/auth.ts
stash를 브랜치로 변환
git stash branch new-branch stash@{0}
3. 브랜칭 전략 비교
3.1 Git Flow
가장 전통적인 브랜칭 모델로, Vincent Driessen이 2010년에 제안했다.
**구조:**
- `main` -- 프로덕션 코드
- `develop` -- 다음 릴리스 개발
- `feature/*` -- 기능 개발
- `release/*` -- 릴리스 준비
- `hotfix/*` -- 긴급 버그 수정
**적합한 팀:** 릴리스 주기가 긴 프로젝트 (월 1-2회), 여러 버전을 동시에 유지해야 하는 경우.
**단점:** 브랜치가 많아서 복잡하고, CI/CD와 잘 맞지 않는다.
3.2 GitHub Flow
GitHub가 실제로 사용하는 단순한 모델이다.
**구조:**
- `main` -- 항상 배포 가능한 상태
- `feature/*` -- main에서 분기, PR로 병합
**적합한 팀:** 지속적 배포 환경, 소규모 팀, 웹 서비스.
**규칙:** main은 항상 배포 가능해야 하며, feature 브랜치에서 작업 후 PR을 통해 merge한다.
3.3 Trunk-Based Development
Google, Meta 등 대형 IT 기업이 사용하는 전략이다.
**구조:**
- `main` (trunk) -- 모든 개발자가 직접 커밋하거나 short-lived 브랜치로 작업
- 브랜치 수명: 최대 1-2일
**적합한 팀:** Feature Flag 인프라가 있는 팀, 대규모 조직, 빠른 피드백 루프.
3.4 비교 요약
| 항목 | Git Flow | GitHub Flow | Trunk-Based |
|------|----------|-------------|-------------|
| 복잡도 | 높음 | 낮음 | 중간 |
| 배포 빈도 | 월 1-2회 | 매일 | 하루 수회 |
| 팀 규모 | 대규모 | 소-중규모 | 모든 규모 |
| Feature Flag 필요 | 아니오 | 아니오 | 예 |
| CI/CD 궁합 | 보통 | 좋음 | 매우 좋음 |
4. Merge vs Rebase
Git에서 가장 논쟁적인 주제 중 하나이다.
4.1 Merge -- 히스토리 보존
feature 브랜치를 main에 merge
git checkout main
git merge feature/auth
merge는 두 브랜치의 공통 조상을 찾아 3-way merge를 수행하고, **merge commit**을 생성한다. 히스토리가 그대로 보존되므로 "언제 어떤 브랜치에서 작업했는지"를 추적할 수 있다.
4.2 Rebase -- 깔끔한 히스토리
feature 브랜치에서 main의 최신 변경을 가져오기
git checkout feature/auth
git rebase main
rebase는 feature 브랜치의 커밋들을 main의 끝에 하나씩 재적용한다. 결과적으로 **직선형 히스토리**가 만들어진다.
4.3 Interactive Rebase -- 커밋 정리의 핵심
최근 5개 커밋 정리
git rebase -i HEAD~5
인터랙티브 리베이스 편집기에서 사용할 수 있는 명령어는 다음과 같다.
- **pick** -- 커밋 유지
- **reword** -- 커밋 메시지 수정
- **edit** -- 커밋 내용 수정
- **squash** -- 이전 커밋과 합치기 (메시지 합침)
- **fixup** -- 이전 커밋과 합치기 (메시지 버림)
- **drop** -- 커밋 삭제
실제 사용 예시: PR 전에 커밋 정리
에디터에서 아래처럼 수정
pick abc1234 feat: add login page
squash def5678 fix: typo in login
squash ghi9012 style: adjust padding
-> 3개 커밋이 1개로 합쳐짐
4.4 언제 어떤 것을 쓸까
| 상황 | 추천 |
|------|------|
| PR 머지 | Squash Merge 또는 Rebase Merge |
| 로컬 커밋 정리 | Interactive Rebase |
| 공유 브랜치 | Merge (rebase 금지) |
| 개인 feature 브랜치 | Rebase로 main 동기화 |
**황금률:** 이미 push된 커밋은 rebase하지 마라. 다른 사람이 참조하고 있을 수 있다.
5. 고급 명령어 마스터
5.1 cherry-pick -- 특정 커밋만 가져오기
다른 브랜치의 특정 커밋 하나를 현재 브랜치로 복사한다.
특정 커밋 가져오기
git cherry-pick abc1234
여러 커밋 한번에
git cherry-pick abc1234 def5678
범위로 가져오기 (abc는 미포함, def는 포함)
git cherry-pick abc1234..def5678
커밋하지 않고 변경만 적용
git cherry-pick --no-commit abc1234
**사용 사례:**
- hotfix 브랜치의 버그 수정을 develop에도 적용
- 잘못된 브랜치에 커밋한 것을 올바른 브랜치로 이동
- 릴리스 브랜치에 특정 기능만 선별 포함
5.2 bisect -- 이진 검색으로 버그 찾기
수백 개의 커밋 중 어디서 버그가 생겼는지 이진 검색으로 찾는다.
bisect 시작
git bisect start
현재(버그 있음)를 bad로 표시
git bisect bad
정상이었던 커밋을 good으로 표시
git bisect good v2.0.0
Git이 중간 커밋을 체크아웃 -> 테스트 -> 판별
git bisect good # 또는 git bisect bad
원인 커밋을 찾으면 리셋
git bisect reset
자동화도 가능하다.
테스트 스크립트로 자동 bisect
git bisect start HEAD v2.0.0
git bisect run npm test
이렇게 하면 Git이 자동으로 각 커밋에서 테스트를 돌리고, 처음으로 실패하는 커밋을 찾아준다.
5.3 worktree -- 여러 브랜치 동시 작업
하나의 저장소에서 여러 작업 디렉토리를 만들어 동시에 다른 브랜치를 작업할 수 있다.
새 worktree 생성
git worktree add ../project-hotfix hotfix/critical-bug
worktree 목록 확인
git worktree list
worktree 제거
git worktree remove ../project-hotfix
새 브랜치를 만들면서 worktree 생성
git worktree add -b feature/new-ui ../project-ui
**사용 사례:**
- main에서 코드 리뷰하면서 feature에서 개발 계속하기
- 긴급 hotfix와 현재 작업을 동시에 진행
- CI 빌드를 기다리는 동안 다른 작업 시작
5.4 reflog -- 실수 복구의 최후 보루
reflog는 HEAD가 가리킨 모든 곳의 기록이다. 실수로 커밋을 잃어버려도 reflog로 복구할 수 있다.
reflog 확인
git reflog
실수로 reset --hard 했을 때 복구
git reflog
HEAD@{2}: commit: important feature 를 찾으면
git reset --hard HEAD@{2}
삭제된 브랜치 복구
git reflog
git checkout -b recovered-branch HEAD@{5}
특정 기간 내 기록
git reflog --since="2 days ago"
**주의:** reflog는 로컬에만 존재하고, 기본 90일 후 만료된다.
6. .gitconfig 최적화
생산성을 높이는 Git 설정을 소개한다.
6.1 유용한 alias
[alias]
상태 & 로그
st = status -sb
lg = log --oneline --graph --decorate --all
last = log -1 HEAD --stat
브랜치
co = checkout
sw = switch
br = branch -vv
brd = branch -d
커밋
cm = commit -m
ca = commit --amend --no-edit
undo = reset HEAD~1 --mixed
diff
df = diff --stat
dfc = diff --cached
stash
sl = stash list
sp = stash pop
ss = stash push -m
정리
cleanup = "!git branch --merged | grep -v '\\*\\|main\\|develop' | xargs -n 1 git branch -d"
6.2 core 설정
[core]
editor = code --wait
autocrlf = input # macOS/Linux
ignorecase = false
pager = delta # delta pager 사용
[pull]
rebase = true # pull 시 항상 rebase
[push]
default = current # 현재 브랜치만 push
autoSetupRemote = true # push 시 자동 upstream 설정
[init]
defaultBranch = main
[diff]
tool = vscode
colorMoved = default
[merge]
conflictstyle = diff3 # 3-way 충돌 표시
tool = vscode
[rerere]
enabled = true # 충돌 해결 기억하기
6.3 delta -- 더 나은 diff 도구
delta는 Git diff 출력을 보기 좋게 바꿔주는 도구이다.
설치
brew install git-delta
.gitconfig에 추가
[core]
pager = delta
[interactive]
diffFilter = delta --color-only
[delta]
navigate = true
line-numbers = true
side-by-side = true
7. GitHub PR 리뷰 베스트 프랙티스
7.1 좋은 PR의 조건
1. **크기:** 200-400줄 이하. 그 이상이면 쪼개라.
2. **단일 목적:** 하나의 PR은 하나의 변경 사항만.
3. **설명:** 왜 이 변경이 필요한지, 어떻게 테스트했는지 기술.
4. **Self-Review:** 올리기 전에 본인이 먼저 리뷰.
7.2 PR 템플릿
변경 사항
- 무엇을 왜 변경했는지
테스트
- [ ] 단위 테스트 추가/수정
- [ ] 로컬에서 동작 확인
- [ ] 엣지 케이스 검증
스크린샷 (UI 변경 시)
관련 이슈
- closes #123
7.3 리뷰 에티켓
**리뷰어:**
- 코드의 "의도"를 먼저 이해하라. 스타일보다 로직에 집중.
- 질문 형태로 피드백하라: "이 부분은 왜 이렇게 했나요?" 가 "이건 틀렸습니다"보다 낫다.
- nit, suggestion, question 등 코멘트 유형을 구분하라.
- Approve, Request Changes, Comment를 명확히 사용하라.
**작성자:**
- 리뷰어의 시간을 존중하라. PR을 작게 유지하라.
- 모든 코멘트에 응답하라 (최소한 리액션이라도).
- force push 후에는 리뷰어에게 알려라.
7.4 CODEOWNERS
.github/CODEOWNERS
전체 코드베이스
* @team-lead
프론트엔드
/src/components/ @frontend-team
/src/pages/ @frontend-team
백엔드 API
/src/api/ @backend-team
인프라
/terraform/ @devops-team
/k8s/ @devops-team
보안 민감 파일
/src/auth/ @security-team @team-lead
8. 커밋 메시지 컨벤션
8.1 Conventional Commits
type(scope): description
body (선택)
footer (선택)
**type 종류:**
| type | 설명 |
|------|------|
| feat | 새 기능 |
| fix | 버그 수정 |
| docs | 문서 변경 |
| style | 코드 포맷팅 (기능 변경 없음) |
| refactor | 리팩토링 |
| perf | 성능 개선 |
| test | 테스트 추가/수정 |
| chore | 빌드, 설정 변경 |
| ci | CI 설정 변경 |
예시
git commit -m "feat(auth): add Google OAuth login"
git commit -m "fix(api): handle null response from payment gateway"
git commit -m "docs(readme): update installation instructions"
Breaking Change
git commit -m "feat(api)!: change response format to JSON:API"
8.2 gitmoji
이모지로 커밋 유형을 시각적으로 구분한다.
git commit -m ":sparkles: add user profile page"
git commit -m ":bug: fix login redirect loop"
git commit -m ":recycle: refactor database connection pool"
git commit -m ":white_check_mark: add unit tests for auth module"
8.3 commitlint로 강제하기
설치
npm install -D @commitlint/cli @commitlint/config-conventional
commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style',
'refactor', 'perf', 'test', 'chore', 'ci'
]],
'subject-max-length': [2, 'always', 72],
},
};
9. 모노레포 관리
9.1 git sparse-checkout
대규모 모노레포에서 필요한 디렉토리만 체크아웃한다.
sparse-checkout 활성화
git sparse-checkout init --cone
필요한 디렉토리만 지정
git sparse-checkout set packages/frontend packages/shared
디렉토리 추가
git sparse-checkout add packages/backend
설정 확인
git sparse-checkout list
전체 복원
git sparse-checkout disable
9.2 git subtree
외부 저장소를 서브디렉토리로 통합 관리한다.
외부 저장소 추가
git subtree add --prefix=libs/shared-utils \
https://github.com/org/shared-utils.git main --squash
변경 사항 가져오기
git subtree pull --prefix=libs/shared-utils \
https://github.com/org/shared-utils.git main --squash
변경 사항 내보내기
git subtree push --prefix=libs/shared-utils \
https://github.com/org/shared-utils.git main
9.3 Nx / Turborepo와의 연동
모노레포 빌드 도구와 Git을 연동하면 변경된 패키지만 빌드/테스트할 수 있다.
Nx: 영향받는 프로젝트만 테스트
npx nx affected --target=test --base=main --head=HEAD
Turborepo: 변경된 패키지만 빌드
npx turbo run build --filter=...[HEAD~1]
GitHub Actions에서 변경 감지
CI에서 main과 비교하여 변경된 패키지만 처리
10. 위기 상황 대처법
10.1 force push 복구
누군가 실수로 force push를 했을 때 대처법이다.
1. reflog에서 원래 커밋 찾기
git reflog show origin/main
2. 원래 커밋으로 복구
git push origin HEAD@{1}:main --force
3. 다른 팀원에게 알리기
"main 브랜치가 복구되었습니다. git pull --rebase를 실행해주세요"
**예방책:**
main 브랜치 force push 차단 (GitHub Settings)
Settings -> Branches -> Branch protection rules
"Restrict force pushes" 활성화
10.2 민감 정보 제거 (BFG Repo-Cleaner)
실수로 비밀번호, API 키 등을 커밋했을 때이다.
BFG 설치
brew install bfg
특정 파일 히스토리에서 완전 제거
bfg --delete-files secrets.env
특정 텍스트 치환
bfg --replace-text passwords.txt
정리
git reflog expire --expire=now --all
git gc --prune=now --aggressive
강제 푸시
git push --force
**주의사항:**
- BFG 실행 전에 저장소를 반드시 백업하라.
- 이미 clone한 팀원들에게 fresh clone을 요청해야 한다.
- GitHub에서 캐시 삭제를 요청해야 할 수 있다.
10.3 대용량 파일 관리 (Git LFS)
바이너리 파일, 미디어 파일 등 대용량 파일은 Git LFS를 사용한다.
Git LFS 설치 & 초기화
brew install git-lfs
git lfs install
추적할 파일 패턴 지정
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "assets/videos/*"
.gitattributes가 자동 생성됨 - 커밋 필요
git add .gitattributes
git commit -m "chore: configure Git LFS tracking"
LFS 파일 목록 확인
git lfs ls-files
이미 커밋된 대용량 파일을 LFS로 마이그레이션
git lfs migrate import --include="*.psd" --everything
10.4 잘못된 merge 되돌리기
merge 커밋 자체를 revert
git revert -m 1 MERGE_COMMIT_SHA
-m 1: 첫 번째 부모(main)를 기준으로 되돌림
-m 2: 두 번째 부모(feature)를 기준으로 되돌림
10.5 모든 것을 잃었을 때의 최후 수단
1. reflog 확인 (90일 이내)
git reflog
2. dangling 객체에서 복구
git fsck --lost-found
3. 최악의 경우: 팀원의 로컬 저장소에서 복구
팀원에게 push를 요청하거나, bundle로 받기
git bundle create backup.bundle --all
마무리
Git은 단순한 버전 관리 도구가 아니다. 팀의 협업 방식을 정의하고, 코드 품질을 보증하며, 배포 파이프라인의 근간이 되는 **핵심 인프라**이다.
이 글에서 다룬 내용을 요약하면 다음과 같다.
1. **내부 구조**를 이해하면 명령어가 직관적으로 이해된다.
2. **브랜칭 전략**은 팀 상황에 맞게 선택하라 -- 정답은 없다.
3. **rebase**는 로컬에서, **merge**는 공유 브랜치에서 사용하라.
4. **cherry-pick, bisect, worktree, reflog**는 실전에서 빛을 발한다.
5. **PR 리뷰 문화**가 코드 품질을 결정한다.
6. **Conventional Commits**로 커밋 히스토리를 깔끔하게 유지하라.
7. **위기 상황**에서 reflog와 BFG는 생명줄이다.
Git을 마스터하는 것은 하루아침에 되지 않는다. 하지만 매일 한 가지씩 새로운 명령어를 써보고, 내부 구조를 이해해 나간다면, 어느새 어떤 상황에서도 흔들리지 않는 Git 마스터가 되어 있을 것이다.
현재 단락 (1/305)
Git은 개발자가 매일 사용하지만, 대부분은 add-commit-push 루프에서 벗어나지 못한다. merge 충돌이 발생하면 당황하고, rebase는 두렵고, bisect나 wo...