Git rebase interactive 사용법 | pick·squash·fixup·충돌 해결·실수 복구
이 글의 핵심
git rebase -i로 커밋을 정리하는 법: pick, squash, fixup, reword, edit와 충돌 해결, reflog로 되돌리기까지 실무 순서로 정리합니다.
들어가며
Git rebase interactive(git rebase -i)는 로컬 브랜치에서 커밋 순서·메시지·단위를 다듬을 때 쓰는 핵심 도구입니다. PR을 보내기 전에 “의미 있는 단위”로 묶거나, 오타만 고친 커밋을 fixup으로 흡수하는 패턴이 실무에서 매우 흔합니다.
다만 이미 원격에 푸시된 커밋을 바꾸면 히스토리가 달라지므로, 팀 규칙과 force push 정책을 반드시 확인해야 합니다. 이 글은 Git rebase interactive 사용법 충돌 검색 의도에 맞춰 pick, squash, fixup, reword, edit와 충돌 해결·실수 복구까지 단계별로 정리합니다.
개념: 왜 interactive rebase인가
Interactive Rebase의 내부 동작 원리
Git rebase는 어떻게 작동하나요?
원래 상태:
main: A --- B --- C
\
feature: D --- E --- F
rebase 실행: git rebase main (feature 브랜치에서)
1단계: 공통 조상 찾기
공통 조상 = C
2단계: feature의 커밋들을 임시 저장
D, E, F를 .git/rebase-apply/ 에 패치로 저장
3단계: feature를 main으로 이동
feature: A --- B --- C
4단계: 저장된 커밋들을 순차 적용
feature: A --- B --- C --- D' --- E' --- F'
(새 커밋 해시)
결과:
main: A --- B --- C
\
feature: D' --- E' --- F'
왜 커밋 해시가 바뀌나요?
Git 커밋 객체 구조:
커밋 해시 = SHA-1(다음 내용들):
┌─────────────────────────────────────┐
│ tree <tree-sha1> │ ← 프로젝트 스냅샷
│ parent <parent-sha1> │ ← 부모 커밋 해시
│ author <name> <email> <timestamp> │ ← 작성자
│ committer <name> <email> <timestamp>│ ← 커밋터
│ │
│ 커밋 메시지 │
└─────────────────────────────────────┘
Rebase 시 무엇이 바뀌나?
원래 커밋 D:
parent: B
tree: <D의 파일 상태>
author: John Doe <[email protected]> 2026-04-01
committer: John Doe <[email protected]> 2026-04-01
Add login feature
Rebase 후 커밋 D':
parent: C ← 바뀜!
tree: <D와 동일한 파일 상태>
author: John Doe <[email protected]> 2026-04-01 ← 유지
committer: John Doe <[email protected]> 2026-04-17 ← 현재 시각으로 변경
Add login feature
parent가 B → C로 바뀌었으므로:
SHA-1(parent C + tree + author + ...) = 완전히 다른 해시
결과:
- 같은 코드 변경사항 (tree)
- 같은 작성자 (author)
- 같은 메시지
- 하지만 parent가 다르므로 → 완전히 다른 커밋 (D')
Git 객체 저장소의 내부 구조:
.git/objects/ 디렉토리:
├── ab/
│ └── c1234567... (commit 객체)
├── de/
│ └── f5678901... (tree 객체)
└── fe/
└── d9012345... (blob 객체)
Commit 객체:
- 프로젝트 특정 시점의 스냅샷
- parent 포인터 (부모 커밋)
- tree 포인터 (루트 디렉토리)
Tree 객체:
- 디렉토리 구조
- 파일명과 blob 포인터 목록
Blob 객체:
- 실제 파일 내용
- 파일명은 없음 (tree가 관리)
Rebase는 commit 객체만 새로 만들고:
- tree 객체는 재사용 가능 (내용 동일하면)
- blob 객체는 항상 재사용
Interactive rebase의 특별함:
git rebase -i HEAD~3
# 일반 rebase: 자동으로 커밋 적용
# interactive rebase: 각 커밋을 어떻게 처리할지 사용자가 결정
Interactive Rebase 명령어 완벽 가이드
pick (p) - 커밋 그대로 유지
pick abc1234 feat: 로그인 API
내부 동작:
1. abc1234의 패치를 가져옴
2. 현재 HEAD에 적용
3. 새 커밋 생성 (해시는 다름)
4. HEAD를 새 커밋으로 이동
reword (r) - 메시지만 변경
reword abc1234 feat: 로그인 API
내부 동작:
1. abc1234의 패치 적용
2. 커밋 생성 직전에 에디터 열기
3. 사용자가 메시지 수정
4. 새 메시지로 커밋 생성
주의사항:
- 코드 내용은 그대로
- 커밋 작성자(author)와 시간 유지
- 커밋 해시는 바뀜 (메시지가 해시에 포함됨)
edit (e) - 커밋 수정
edit abc1234 feat: 로그인 API
내부 동작:
1. abc1234의 패치 적용
2. 커밋 생성
3. rebase 중단 → 사용자에게 제어권 반환
4. git commit --amend 가능
5. git rebase --continue로 재개
활용 패턴:
- 파일 추가/수정: git add → git commit --amend
- 커밋 분할: git reset HEAD^ → 부분 커밋
- 메시지+내용 동시 수정
squash (s) - 이전 커밋과 합치기 (메시지 보존)
pick abc1234 feat: 로그인 API
squash def5678 feat: 검증 추가
내부 동작:
1. abc1234 적용
2. def5678의 변경사항을 추가로 적용
3. 두 커밋 메시지를 합쳐서 에디터 열기
4. 사용자가 최종 메시지 작성
5. 하나의 새 커밋 생성
결과 메시지 형식:
# This is a combination of 2 commits.
# This is the 1st commit message:
feat: 로그인 API
# This is the commit message #2:
feat: 검증 추가
# 사용자가 편집 → 최종:
feat: 로그인 API 구현
- JWT 토큰 발급
- 입력 검증 추가
fixup (f) - 이전 커밋과 합치기 (메시지 버림)
pick abc1234 feat: 로그인 API
fixup def5678 fix: typo
내부 동작:
1. abc1234 적용
2. def5678의 변경사항 추가로 적용
3. 에디터 열지 않음 (메시지는 abc1234 것만 사용)
4. 하나의 새 커밋 생성
언제 사용?
- "fix typo" 같은 잡음 커밋 제거
- "WIP" 임시 커밋 흡수
- 리뷰 반영 커밋 정리
drop (d) - 커밋 제거
pick abc1234 feat: 로그인 API
drop def5678 wip: 테스트
내부 동작:
1. abc1234 적용
2. def5678 건너뛰기
3. 다음 커밋으로 진행
주의:
- 커밋의 변경사항이 완전히 사라짐
- 다른 커밋이 해당 변경사항에 의존하면 충돌 가능
- 줄 자체를 삭제해도 동일한 효과
exec (x) - 명령어 실행 (고급)
pick abc1234 feat: 로그인 API
exec npm test
pick def5678 feat: 회원가입 API
exec npm test
내부 동작:
1. abc1234 적용 후 npm test 실행
2. 테스트 실패 시 rebase 중단
3. 성공 시 다음으로 진행
활용:
- 각 커밋마다 테스트 실행
- 빌드 검증
- 린터 실행
break (b) - 일시 중지 (디버깅용)
pick abc1234 feat: 로그인 API
break
pick def5678 feat: 회원가입 API
내부 동작:
1. abc1234 적용
2. 사용자에게 제어권 반환
3. git rebase --continue로 재개
언제 사용?
- 중간 상태 확인
- 디버깅
- 수동 작업 필요 시
에디터 읽는 법
pick abc1234 feat: 로그인 API
pick def5678 feat: 회원가입 API
pick fed9012 feat: 비밀번호 재설정 API
# Rebase 1234567..fed9012 onto 1234567 (3 commands)
# ↑ 오래된 것 ↑ 최신 ↑ 재배치할 커밋 범위
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
실행 순서:
- 위에서 아래 = 오래된 커밋 → 최신 커밋
- 순서를 바꾸면 적용 순서도 바뀜
- 줄을 삭제 = drop과 동일
언제 사용하나?
시나리오 1: PR 전 히스토리 정리
문제 상황:
* fed9012 fix lint
* def5678 fix typo
* abc1234 wip
* 1234567 feat: 로그인 API 구현
* 2345678 feat: JWT 토큰 발급
리뷰어 입장: "무엇을 했는지 파악하기 어려움"
해결:
pick 1234567 feat: 로그인 API 구현
fixup abc1234 wip
fixup def5678 fix typo
fixup fed9012 fix lint
pick 2345678 feat: JWT 토큰 발급
결과:
* 1234567 feat: 로그인 API 구현
* 2345678 feat: JWT 토큰 발급
리뷰어 입장: "명확한 2개의 기능 커밋"
시나리오 2: Conventional Commits 규칙 적용
팀 규칙: feat/fix/docs/refactor 접두사 필수
현재:
* add login
* bug fix
* update docs
해결:
reword abc1234 add login → feat: 로그인 API 추가
reword def5678 bug fix → fix: 토큰 검증 버그 수정
reword fed9012 update docs → docs: API 문서 업데이트
결과:
* feat: 로그인 API 추가
* fix: 토큰 검증 버그 수정
* docs: API 문서 업데이트
시나리오 3: 논리적 순서로 재배치
현재 (개발 순서):
* 토큰 검증 로직 추가
* 로그인 API 추가
* 회원가입 API 추가
* 로그인 테스트 추가
문제: 테스트가 멀리 떨어져 있음
해결:
1. 회원가입 API 추가
2. 로그인 API 추가
3. 로그인 테스트 추가
4. 토큰 검증 로직 추가
결과: 기능별로 그룹화됨
주의사항 및 함정
함정 1: 원격 푸시된 커밋 변경
# 로컬 히스토리:
main: A --- B --- C --- D --- E
↑ HEAD
# git push 후:
origin/main: A --- B --- C --- D --- E
# rebase로 D, E를 squash:
main: A --- B --- C --- F
↑ HEAD (D+E 합쳐짐)
# 이제 push 시도:
git push
# 에러: Updates were rejected
# 이유: 원격과 히스토리가 다름
# origin은 D, E 기대
# 로컬은 F만 있음
# 해결: force push (위험!)
git push --force-with-lease
# 결과: origin의 D, E가 사라지고 F만 남음
# 팀원이 D, E 기반으로 작업 중이면 문제 발생!
함정 2: 공유 브랜치에서 rebase
# 팀원 A:
git checkout develop
git pull # A --- B --- C --- D
# 팀원 B:
git checkout develop
git pull # A --- B --- C --- D
git rebase -i HEAD~2 # C, D를 squash → E
git push --force # develop = A --- B --- E
# 팀원 A (모르고):
git commit # F 생성
git push
# 에러: Updates were rejected
# 팀원 A가 pull하면:
# A --- B --- C --- D --- F (로컬)
# \--- E (원격)
# → 머지 커밋 생성 또는 충돌
# 결과: 히스토리가 엉망이 됨
안전한 사용 원칙:
- 개인 feature 브랜치에서만 rebase
- PR 머지 전에만 사용
- 팀원과 공유하는 브랜치는 merge 사용
- force push 전 —force-with-lease 사용
# 안전한 패턴:
git checkout -b feature/login # 개인 브랜치 생성
# ... 작업 ...
git rebase -i HEAD~5 # 로컬에서 정리
git push origin feature/login # 최초 push
# ... PR 생성, 리뷰 ...
# 리뷰 반영 후 다시 정리
git rebase -i HEAD~3
git push --force-with-lease origin feature/login # 안전한 force
# PR 머지 (develop에 합쳐짐)
# 이후 feature/login 브랜치 삭제
실전: 기본 명령과 에디터 토큰
최근 N개 커밋 정리
기본 사용법
# 최근 3개 커밋 정리
git rebase -i HEAD~3
# HEAD~3의 의미:
# HEAD: 현재 커밋
# HEAD~1 (또는 HEAD^): 1개 이전
# HEAD~3: 3개 이전
# 특정 커밋부터 정리
git rebase -i abc1234
# 공통 조상부터 정리
git rebase -i main
# 현재 브랜치의 모든 커밋 정리
git rebase -i $(git merge-base main HEAD)
HEAD~ 표기법 이해:
커밋 히스토리:
E (HEAD)
|
D (HEAD~1 또는 HEAD^)
|
C (HEAD~2 또는 HEAD^^)
|
B (HEAD~3)
|
A
git rebase -i HEAD~3
→ B, C, D, E를 재정리 (A는 기준점)
에디터 화면 상세 분석
pick abc1234 feat: 로그인 API
pick def5678 fix: typo
pick fed9012 wip
# Rebase 1234567..fed9012 onto 1234567 (3 commands)
# ↑ 기준점 ↑ 마지막 ↑ 처리할 커밋 개수
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# d, drop <commit> = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
# ↑ 중요: 위에서 아래로 실행
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# ↑ 줄 삭제 = drop과 동일
#
# However, if you remove everything, the rebase will be aborted.
# ↑ 모두 삭제하면 취소
에디터 조작법:
# Vim 사용자
i # 입력 모드 (pick → fixup 등 변경)
ESC # 명령 모드로 돌아가기
dd # 현재 줄 삭제 (커밋 drop)
yy, p # 줄 복사 및 붙여넣기 (순서 변경)
:wq # 저장 후 종료
:q! # 저장 없이 종료 (rebase 취소)
# VS Code 사용자
git config --global core.editor "code --wait"
# 저장 (Ctrl+S), 닫기 → rebase 실행
# 저장 안 하고 닫기 → rebase 취소
# Nano 사용자
Ctrl+O # 저장
Enter
Ctrl+X # 종료
에디터가 열리지 않을 때
# 문제: 에디터가 안 열리거나 자동 종료됨
git rebase -i HEAD~3
# fatal: could not read 'COMMIT_EDITMSG'
# 해결 1: 에디터 설정 확인
git config --global core.editor
# 출력이 없거나 이상하면 재설정
# Vim
git config --global core.editor "vim"
# VS Code
git config --global core.editor "code --wait"
# 중요: --wait 플래그 필수!
# Sublime Text
git config --global core.editor "subl -w"
# Notepad++ (Windows)
git config --global core.editor "'C:/Program Files/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin"
# 해결 2: 환경 변수 설정
export GIT_EDITOR=vim
export VISUAL=vim
export EDITOR=vim
# .bashrc나 .zshrc에 추가하여 영구 설정
echo 'export GIT_EDITOR=vim' >> ~/.bashrc
rebase 범위 지정하는 여러 방법
# 방법 1: HEAD 기준 (가장 일반적)
git rebase -i HEAD~5 # 최근 5개
# 방법 2: 특정 커밋 해시 (그 커밋은 포함 안 됨)
git rebase -i abc1234 # abc1234 이후의 모든 커밋
# 방법 3: 브랜치 분기점부터
git rebase -i main
# main에서 갈라진 이후의 모든 커밋
# 방법 4: 두 커밋 사이
git rebase -i abc1234 def5678
# abc1234와 def5678 사이의 커밋들
# 방법 5: 루트부터 (전체 히스토리)
git rebase -i --root
# 첫 커밋부터 모든 커밋 (매우 위험!)
# 방법 6: 특정 브랜치의 커밋들만
git rebase -i $(git merge-base main feature/login)
# main과 feature/login의 공통 조상 이후
예시 1: fixup으로 임시 커밋 제거 (단계별)
초기 상태:
git log --oneline
fed9012 wip
def5678 fix: typo
abc1234 feat: 로그인 API
1234567 feat: 회원가입 API
문제: wip와 fix: typo는 잡음 커밋
단계 1: rebase 시작
git rebase -i HEAD~3
단계 2: 에디터에서 fixup으로 변경
pick abc1234 feat: 로그인 API
pick def5678 fix: typo
fixup fed9012 wip
내부 동작:
- abc1234 적용 (feat: 로그인 API)
- def5678 적용 (fix: typo)
- fed9012의 변경사항만 def5678에 추가
- def5678의 메시지 유지, fed9012 메시지는 버림
단계 3: 결과 확인
git log --oneline
def5678 fix: typo # fed9012의 변경사항 포함
abc1234 feat: 로그인 API
1234567 feat: 회원가입 API
주의: fixup은 바로 위 커밋에 합쳐집니다. 순서가 중요합니다!
# ❌ 잘못됨: fixup이 첫 번째
fixup fed9012 wip
pick abc1234 feat: 로그인 API
# 에러: fixup할 대상이 없음
# ✅ 올바름
pick abc1234 feat: 로그인 API
fixup fed9012 wip
예시 2: squash로 커밋 합치고 메시지 작성
초기 상태:
git log --oneline -5
fed9012 wip
def5678 fix: typo
abc1234 feat: 로그인 API
1234567 feat: JWT 토큰 발급
2345678 feat: 리프레시 토큰
목표: 3개 커밋을 하나로 합치고 의미 있는 메시지 작성
단계 1: rebase 시작
git rebase -i HEAD~3
단계 2: squash로 변경
pick abc1234 feat: 로그인 API
squash def5678 fix: typo
squash fed9012 wip
단계 3: 메시지 편집 화면
에디터가 다시 열리며 다음 내용이 표시됩니다:
# This is a combination of 3 commits.
# This is the 1st commit message:
feat: 로그인 API
# This is the commit message #2:
fix: typo
# This is the commit message #3:
wip
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Wed Apr 17 10:30:00 2026 +0900
#
# interactive rebase in progress; onto 1234567
# Last commands done (3 commands done):
# squash def5678 fix: typo
# squash fed9012 wip
# No commands remaining.
# You are currently rebasing branch 'feature/login' on '1234567'.
#
# Changes to be committed:
# modified: src/auth.js
# modified: src/token.js
# new file: tests/auth.test.js
단계 4: 의미 있는 메시지 작성
불필요한 내용을 삭제하고 다음과 같이 작성:
feat: 로그인 API 구현
사용자 인증 시스템을 구현했습니다.
주요 변경사항:
- JWT 기반 로그인 엔드포인트 추가
- 이메일/비밀번호 검증 로직
- 리프레시 토큰 처리
- 에러 핸들링 (401, 403)
- 단위 테스트 추가
API 엔드포인트:
- POST /api/auth/login
- POST /api/auth/refresh
기술 스택:
- Express.js
- jsonwebtoken
- bcrypt
단계 5: 저장 및 결과 확인
# 저장 후 종료 (Vim: :wq)
# 결과
git log --oneline
abc1234 feat: 로그인 API 구현
1234567 feat: JWT 토큰 발급
2345678 feat: 리프레시 토큰
squash vs fixup 선택 가이드:
상황: 리뷰 반영 커밋을 원래 커밋에 합치기
커밋 히스토리:
* fed9012 리뷰 반영: 변수명 수정
* def5678 리뷰 반영: 에러 메시지 개선
* abc1234 feat: 로그인 API
패턴 1: fixup 사용 (메시지 버림)
pick abc1234 feat: 로그인 API
fixup def5678 리뷰 반영: 에러 메시지 개선
fixup fed9012 리뷰 반영: 변수명 수정
결과:
* abc1234 feat: 로그인 API
(원래 메시지 그대로 유지)
패턴 2: squash 사용 (메시지 통합)
pick abc1234 feat: 로그인 API
squash def5678 리뷰 반영: 에러 메시지 개선
squash fed9012 리뷰 반영: 변수명 수정
→ 에디터 열림 → 리뷰 내용을 메시지에 추가
결과:
* abc1234 feat: 로그인 API
구현 내용:
- 인증 로직
- JWT 토큰 발급
리뷰 반영 사항:
- 변수명 명확하게 수정
- 사용자 친화적 에러 메시지
실무 권장:
- fixup: “fix typo”, “lint”, “format” 같은 기계적 수정
- squash: 의미 있는 변경이지만 별도 커밋으로 남길 필요 없을 때
reword로 메시지만 변경
사용법
git rebase -i HEAD~2
에디터에서 변경
reword abc1234 feat: 로그인 API
pick def5678 fix: typo
저장 후 종료하면 메시지 편집 화면이 나타납니다:
feat: 로그인 API
# Please enter the commit message for your changes.
메시지를 수정하고 저장합니다:
feat: JWT 기반 로그인 API 구현
- 토큰 발급 로직 추가
- 리프레시 토큰 처리
edit로 커밋 내용 수정 (완벽 가이드)
사용 시나리오:
- 커밋에 파일 추가/삭제
- 코드 내용 수정
- 하나의 커밋을 여러 개로 분할
- 메시지와 내용 동시 수정
패턴 1: 기존 커밋에 파일 추가
상황:
git log --oneline
def5678 feat: 회원가입 API
abc1234 feat: 로그인 API # 테스트 파일을 깜빡함!
해결 단계:
# 1. rebase 시작
git rebase -i HEAD~2
# 2. 에디터에서 edit로 변경
edit abc1234 feat: 로그인 API
pick def5678 feat: 회원가입 API
# 3. 저장 후 종료 → abc1234에서 멈춤
출력:
Stopped at abc1234... feat: 로그인 API
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
4. 파일 추가 및 수정
# 깜빡한 테스트 파일 생성
vim tests/auth.test.js
# 코드도 수정 (필요시)
vim src/auth.js
# 스테이징
git add tests/auth.test.js
git add src/auth.js
# 커밋 수정 (메시지 유지)
git commit --amend --no-edit
# 메시지도 바꾸고 싶으면
git commit --amend
# 에디터 열림 → 메시지 수정 → 저장
# 5. rebase 계속
git rebase --continue
결과 확인:
git show abc1234
# 이제 tests/auth.test.js 포함됨
git log --stat abc1234
# 파일 목록에 테스트 파일 추가 확인
패턴 2: 커밋 분할 (가장 유용!)
상황:
git log --oneline
abc1234 feat: 로그인 및 회원가입 API # 너무 큰 커밋!
git show abc1234 --stat
src/auth/login.js | 50 ++++++++++++
src/auth/signup.js | 60 ++++++++++++
src/middleware/validate.js | 40 ++++++++
tests/auth.test.js | 100 +++++++++++++++++++
목표: 3개의 논리적 커밋으로 분리
해결 단계:
# 1. rebase 시작
git rebase -i HEAD~1
# 2. edit로 변경
edit abc1234 feat: 로그인 및 회원가입 API
# 3. 커밋 취소 (변경사항은 Working Directory에 남음)
git reset HEAD^
# 4. 상태 확인
git status
# Changes not staged for commit:
# modified: src/auth/login.js
# modified: src/auth/signup.js
# modified: src/middleware/validate.js
# modified: tests/auth.test.js
# 5. 첫 번째 커밋: 로그인 API만
git add src/auth/login.js
git add tests/auth.test.js
git commit -m "feat: 로그인 API 추가
- JWT 토큰 기반 인증
- 이메일/비밀번호 검증
- 테스트 커버리지 100%"
# 6. 두 번째 커밋: 회원가입 API
git add src/auth/signup.js
git commit -m "feat: 회원가입 API 추가
- 이메일 중복 체크
- 비밀번호 해싱 (bcrypt)
- 환영 이메일 발송"
# 7. 세 번째 커밋: 공통 미들웨어
git add src/middleware/validate.js
git commit -m "feat: 입력 검증 미들웨어 추가
- 이메일 형식 검증
- 비밀번호 강도 체크
- 재사용 가능한 밸리데이터"
# 8. rebase 계속
git rebase --continue
결과:
git log --oneline
1234567 feat: 입력 검증 미들웨어 추가
def5678 feat: 회원가입 API 추가
abc1234 feat: 로그인 API 추가
팁: git add -p로 더 세밀하게 분할
파일 내에서도 변경사항을 나눌 수 있습니다:
# 파일 일부만 스테이징
git add -p src/auth.js
# 출력:
diff --git a/src/auth.js b/src/auth.js
@@ -1,10 +1,20 @@
+// 로그인 함수
+function login(email, password) {
+ return authenticate(email, password);
+}
+
Stage this hunk [y,n,q,a,d,s,e,?]?
# y: 이 변경사항 스테이징
# n: 건너뛰기
# s: 더 작게 분할
# e: 수동 편집 (라인 단위 선택)
패턴 3: 커밋 내용 수정 (코드 개선)
상황: 리뷰 후 과거 커밋의 코드를 개선하고 싶음
git log --oneline
fed9012 feat: 회원가입 API
def5678 feat: 토큰 발급 # 이 커밋의 코드를 개선하고 싶음
abc1234 feat: 로그인 API
# 1. rebase 시작
git rebase -i HEAD~3
# 2. def5678을 edit로 변경
pick abc1234 feat: 로그인 API
edit def5678 feat: 토큰 발급
pick fed9012 feat: 회원가입 API
# 3. def5678에서 멈춤
# 코드 개선
vim src/token.js
# 변경: var → const/let
# 변경: 콜백 → async/await
# 4. 수정사항 반영
git add src/token.js
git commit --amend -m "feat: 토큰 발급 로직 구현
- JWT 생성 및 검증
- 리프레시 토큰 갱신
- 만료 시간 설정 가능
기술 개선:
- ES6+ 문법 적용
- async/await로 가독성 향상"
# 5. 계속 진행
git rebase --continue
주의: edit 이후의 커밋들이 수정된 코드에 의존하면 충돌 발생 가능!
패턴 4: 여러 커밋을 순차적으로 수정
# 여러 커밋을 edit로 표시
git rebase -i HEAD~5
edit abc1234 feat: 로그인 API
edit def5678 feat: 토큰 발급
pick fed9012 feat: 회원가입 API
edit 1234567 feat: 비밀번호 재설정
pick 2345678 docs: API 문서 추가
# 저장 → abc1234에서 멈춤
# ... 수정 ...
git commit --amend
git rebase --continue
# → def5678에서 멈춤
# ... 수정 ...
git commit --amend
git rebase --continue
# → 1234567에서 멈춤
# ... 수정 ...
git commit --amend
git rebase --continue
# → 완료
충돌 해결 완벽 가이드
충돌이 발생하는 이유 - 3-Way Merge 메커니즘
Git의 충돌 탐지 알고리즘 (3-Way Merge):
3-Way Merge란?
Base (공통 조상):
function login(user, pwd) {
return auth(user, pwd);
}
HEAD (rebase 대상 - main):
function login(username, password) {
// 파라미터명 변경: user → username
return authenticateUser(username, password);
}
Ours (적용하려는 커밋 - feature):
function login(user, pwd) {
// 내부 로직 변경
if (!user || !pwd) {
throw new Error('Required');
}
return auth(user, pwd);
}
Git의 판단 과정:
1. Base → HEAD 변경:
- 파라미터명: user → username
- 함수명: auth → authenticateUser
2. Base → Ours 변경:
- 검증 로직 추가
- 파라미터명 그대로 (user, pwd)
3. 충돌 판정:
- 같은 라인(파라미터)을 양쪽에서 다르게 수정
- user → username (HEAD)
- user → 그대로 + 검증 추가 (Ours)
→ 자동 병합 불가능!
결과: CONFLICT
Git이 자동 병합 가능한 경우:
Case 1: 다른 라인 수정
Base:
line 1: A
line 2: B
line 3: C
HEAD: Ours:
line 1: A line 1: A
line 2: X line 2: B
line 3: C line 3: Y
자동 병합:
line 1: A
line 2: X ← HEAD의 변경
line 3: Y ← Ours의 변경
Case 2: 한쪽만 수정
Base:
line 1: A
line 2: B
HEAD: Ours:
line 1: A line 1: A
line 2: X line 2: B (변경 없음)
자동 병합:
line 1: A
line 2: X ← HEAD의 변경 적용
Git이 충돌로 판정하는 경우:
Case 1: 같은 라인 양쪽 수정
Base:
line 1: A
HEAD: Ours:
line 1: X line 1: Y
→ CONFLICT! (어느 것을 선택?)
Case 2: 인접 라인 양쪽 수정 (맥락 부족)
Base:
line 1: A
line 2: B
line 3: C
HEAD: Ours:
line 1: X line 1: A
line 2: B line 2: Y
line 3: C line 3: Z
→ Git은 라인 2를 양쪽에서 수정했다고 판단 → CONFLICT
Case 3: 삭제 vs 수정
Base:
function login() {
return auth();
}
HEAD: Ours:
(함수 삭제) function login() {
// 로직 개선
return betterAuth();
}
→ CONFLICT! (삭제? 수정?)
3-Way Merge 알고리즘 내부 동작:
Git Merge 알고리즘 (Myers' diff algorithm 기반):
1. LCS (Longest Common Subsequence) 찾기:
Base, HEAD, Ours의 최장 공통 부분열 계산
Base: A B C D E
HEAD: A X C D E
Ours: A B C Y E
LCS: A _ C _ E (공통 부분)
2. 변경사항 추출:
Base → HEAD:
- B 삭제
- X 삽입 (위치 2)
Base → Ours:
- D 삭제
- Y 삽입 (위치 4)
3. 병합 가능 여부 판단:
두 변경사항이 겹치는가?
- B와 D는 다른 위치 → 병합 가능
결과:
A X C Y E
4. 충돌 조건:
- 같은 라인 수정
- 삭제 + 수정
- 문맥 라인 부족 (주변 3줄)
문맥 라인 (Context Lines):
Git은 변경 주변 3줄을 "문맥"으로 사용
예시:
@@ -10,7 +10,7 @@
line 8 ← 문맥
line 9 ← 문맥
line 10 ← 문맥
-line 11 ← 변경 (old)
+line 11 ← 변경 (new)
line 12 ← 문맥
line 13 ← 문맥
line 14 ← 문맥
양쪽 변경이 같은 문맥 범위에 있으면:
→ 충돌 가능성 증가
실제 Merge 엔진 동작 (Git 내부):
git merge 또는 git rebase 실행 시:
1. 3개 파일 준비:
.git/MERGE_BASE # 공통 조상 (Base)
.git/MERGE_HEAD # 병합 대상 (HEAD)
.git/MERGE_MSG # Ours (적용하려는 커밋)
2. xdiff 라이브러리 호출:
Git의 C 코드:
xpparam_t xpp;
xdemitconf_t xecfg;
mmfile_t mmfs[3]; // Base, Ours, Theirs
// LCS 계산
xdl_diff(mmfs[0], mmfs[1], &xpp, &xecfg, &ecb);
xdl_diff(mmfs[0], mmfs[2], &xpp, &xecfg, &ecb);
// 3-way merge 실행
xdl_merge(mmfs[0], mmfs[1], mmfs[2], &xpp,
XDL_MERGE_ZEALOUS, &result);
3. 결과 처리:
충돌 없음:
- 병합된 내용을 작업 디렉토리에 쓰기
- Index 업데이트
충돌 발생:
- 충돌 마커 삽입:
<<<<<<< HEAD
[HEAD 버전]
=======
[Ours 버전]
>>>>>>> commit-hash
- Index에 3개 스테이지 저장:
Stage 1: Base (공통 조상)
Stage 2: Ours (HEAD)
Stage 3: Theirs (적용하려는 커밋)
- 작업 디렉토리에 충돌 표시된 파일 쓰기
4. 충돌 해결 대기:
git add → Stage 0으로 이동 (해결됨)
git merge --continue / git rebase --continue
Index의 Stage 개념:
git ls-files --stage 명령으로 확인:
정상 파일:
100644 abc1234... 0 src/auth.js
↑ ↑
blob SHA Stage (0 = 충돌 없음)
충돌 발생 시:
100644 base123... 1 src/auth.js ← Base
100644 ours456... 2 src/auth.js ← Ours (HEAD)
100644 their789.. 3 src/auth.js ← Theirs
해결 후 git add:
100644 resolved1. 0 src/auth.js ← 해결된 버전
Stage 번호:
0: 정상 (충돌 없음)
1: Base (공통 조상)
2: Ours (현재 브랜치)
3: Theirs (병합하려는 브랜치)
Git이 자동 병합 실패하는 경우 (수동 개입 필요):
Case 1: Semantic Conflict (의미 충돌)
Base:
function calculate(a, b) {
return a + b;
}
result = calculate(10, 20);
HEAD:
function calculate(a, b, c) { // 파라미터 추가
return a + b + c;
}
result = calculate(10, 20, 30);
Ours:
function calculate(a, b) {
return a + b;
}
result = calculate(5, 15); // 값만 변경
Git 병합 결과:
function calculate(a, b, c) {
return a + b + c;
}
result = calculate(5, 15); // ❌ 파라미터 부족!
→ 문법적 충돌은 없지만 의미적으로 틀림
→ 컴파일/테스트에서 발견
Case 2: Rename Conflict
Base:
function login() { ... }
login();
HEAD:
function authenticate() { ... } // 이름 변경
authenticate();
Ours:
function login() {
// 로직 개선
...
}
login();
Git 병합 결과:
function authenticate() {
// 로직 개선
...
}
login(); // ❌ 함수명 불일치!
→ Git이 자동 병합하지만 호출 실패
line 3: C
HEAD: Ours:
line 1: X line 1: A
line 2: Y line 2: Y
line 3: C line 3: Z
→ CONFLICT! (line 2의 Y는 누가 작성?)
Case 3: 한쪽 삭제, 한쪽 수정
Base:
line 1: A
line 2: B
HEAD: Ours:
(line 2 삭제) line 2: X
→ CONFLICT! (삭제할까, 수정할까?)
Rebase 충돌의 특수성:
일반 merge:
HEAD = 내 브랜치 (현재 작업)
MERGE_HEAD = 가져오는 브랜치
rebase:
HEAD = 베이스 브랜치 (main)
적용 중인 커밋 = 내 변경사항
주의:
- rebase에서 "ours"는 베이스, "theirs"는 내 커밋!
- merge와 반대 의미
- 헷갈리면 --ours/--theirs 사용 주의
충돌 발생 단계별 해결
1단계: 충돌 발생 확인
git rebase -i HEAD~3
에디터 설정:
pick abc1234 feat: 로그인 API
pick def5678 feat: 회원가입 API
pick fed9012 feat: 비밀번호 재설정
저장 후 충돌 발생:
Auto-merging src/auth.js
CONFLICT (content): Merge conflict in src/auth.js
error: could not apply abc1234... feat: 로그인 API
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply abc1234... feat: 로그인 API
2단계: 상태 확인
git status
출력 분석:
interactive rebase in progress; onto 1234567
↑ rebase 진행 중 ↑ 기준 커밋
Last command done (1 command done):
pick abc1234 feat: 로그인 API
↑ 현재 처리 중인 커밋
Next commands to do (2 remaining commands):
pick def5678 feat: 회원가입 API
pick fed9012 feat: 비밀번호 재설정
↑ 앞으로 처리할 커밋들
You are currently rebasing branch 'feature/login' on '1234567'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/auth.js
↑ 양쪽에서 수정됨
Untracked files:
(use "git add <file>..." to include in what will be committed)
src/auth.js.orig # 원본 백업 (자동 생성)
3단계: 충돌 파일 확인
# 충돌 파일 열기
vim src/auth.js
# 또는
code src/auth.js
충돌 마커 이해:
<<<<<<< HEAD (현재 위치)
function login(username, password) {
// main 브랜치의 코드
// 또는 rebase 대상 브랜치의 이미 적용된 코드
return authenticateUser(username, password);
}
=======
function login(email, password) {
// 적용하려는 커밋 (abc1234)의 코드
return authenticateWithEmail(email, password);
}
>>>>>>> abc1234 (feat: 로그인 API)
마커 의미:
<<<<<<< HEAD: 현재 브랜치 (rebase 대상)의 코드=======: 구분선>>>>>>> abc1234: 적용하려는 커밋의 코드
주의: rebase에서 HEAD의 의미
일반 merge:
HEAD = 내 브랜치
OTHER = 가져오는 브랜치
rebase:
HEAD = rebase 대상 (main 등)
OTHER = 적용하는 커밋 (내 변경사항)
헷갈림 방지:
"어느 쪽이 최신인가"보다
"어느 코드가 맞는가"로 판단
4단계: 충돌 해결 방법들
방법 1: 수동 편집 (권장)
// 양쪽 코드를 보고 최선의 결과 작성
function login(email, password) {
// email 파라미터 사용 (더 명확)
// 하지만 함수명은 최신 버전 사용
if (!email || !password) {
throw new Error('Email and password required');
}
return authenticateWithEmail(email, password);
}
방법 2: 한쪽 전체 선택
# HEAD 쪽 선택 (rebase 대상)
git checkout --ours src/auth.js
git add src/auth.js
# OTHER 쪽 선택 (적용하는 커밋)
git checkout --theirs src/auth.js
git add src/auth.js
# ⚠️ rebase에서는 의미가 반대일 수 있음!
# 테스트 후 사용 권장
방법 3: 머지 도구 사용
# VS Code
code src/auth.js
# 화면에 "Accept Current Change" / "Accept Incoming Change" 버튼 표시
# 전용 머지 도구
git mergetool
# 설정 예시 (KDiff3)
git config --global merge.tool kdiff3
git config --global mergetool.kdiff3.path "/usr/bin/kdiff3"
# 설정 예시 (P4Merge)
git config --global merge.tool p4merge
5단계: 해결 표시 및 계속
# 충돌 해결 확인
cat src/auth.js
# 마커(<<<<, ====, >>>>)가 모두 제거되었는지 확인
# 테스트 실행 (권장)
npm test
# 또는
pytest
# 해결 표시
git add src/auth.js
# 상태 재확인
git status
# All conflicts fixed: run "git rebase --continue".
# rebase 계속
git rebase --continue
# 커밋 메시지 확인/수정 가능
# 저장 → 다음 커밋으로 진행
6단계: 추가 충돌 처리
# 다음 커밋(def5678)도 충돌 발생 가능
CONFLICT (content): Merge conflict in src/signup.js
# 동일한 과정 반복:
vim src/signup.js
git add src/signup.js
git rebase --continue
# 모든 커밋 처리 완료까지 반복
충돌 해결 고급 패턴
패턴 1: 충돌 파일이 여러 개
git status
# Unmerged paths:
# both modified: src/auth.js
# both modified: src/token.js
# both modified: src/utils.js
# 한 번에 하나씩 해결
vim src/auth.js
git add src/auth.js
vim src/token.js
git add src/token.js
vim src/utils.js
git add src/utils.js
# 모두 해결 후
git rebase --continue
패턴 2: 충돌이 복잡할 때 - 중간 저장
# 일부만 해결하고 임시 저장
git add src/auth.js # 이 파일만 해결
git stash push src/token.js # 나머지는 나중에
# 중단하고 나중에 재개
git rebase --continue # 에러 (아직 미해결 파일 있음)
# 올바른 방법: 모두 해결할 때까지 작업
# 급하면 abort하고 나중에 재시도
git rebase --abort
패턴 3: 같은 충돌 반복 - rerere 활용
# rerere (reuse recorded resolution) 활성화
git config --global rerere.enabled true
# 효과: 같은 충돌을 두 번 해결하면
# 이후 동일한 충돌은 자동 해결
# 예시:
git rebase -i HEAD~10
# src/auth.js 충돌 발생 → 수동 해결
# ... 3개 커밋 후 ...
# src/auth.js 동일 충돌 발생 → 자동 해결! ✨
충돌 디버깅 도구
diff3 형식 (3-way diff)
# 더 자세한 충돌 정보 표시
git config --global merge.conflictstyle diff3
# 이제 충돌 시:
<<<<<<< HEAD
function login(username, password) {
return authenticateUser(username, password);
}
||||||| merged common ancestors
function login(user, pwd) {
return auth(user, pwd);
}
=======
function login(email, password) {
return authenticateWithEmail(email, password);
}
>>>>>>> abc1234
# 중간에 "원본 공통 조상" 코드 표시
# 어떻게 변경되었는지 더 명확히 파악 가능
충돌 원인 추적
# 충돌이 언제 생겼는지 확인
git log --merge -p src/auth.js
# 양쪽 변경사항 비교
git diff HEAD...abc1234 src/auth.js
# 파일 변경 히스토리
git log --follow -p src/auth.js
포기하고 되돌리기
# rebase 완전히 취소
git rebase --abort
# 효과:
# - rebase 시작 전 상태로 완전히 복원
# - 모든 충돌 해결 작업 취소
# - HEAD는 원래 위치로
# 확인
git log --oneline
# 원래 히스토리 그대로 복원됨
실무 팁: 안전한 충돌 해결 절차
# 1. 백업 브랜치 생성 (안전장치)
git branch backup-before-rebase
# 2. rebase 시작
git rebase -i main
# 3. 충돌 발생 시
# - 천천히 하나씩 해결
# - 각 파일 저장 후 테스트
# - 확신 없으면 abort
# 4. 잘못되면 복구
git rebase --abort
git checkout backup-before-rebase
# 5. 성공하면 백업 삭제
git branch -d backup-before-rebase
고급: autosquash·부분 스테이징
fixup 커밋 자동 정렬 (Autosquash)
Autosquash는 리뷰 반영 작업을 훨씬 편하게 만들어주는 강력한 기능입니다.
문제 상황: 수동 정렬의 번거로움
# 개발 과정:
git commit -m "feat: 로그인 API" # abc1234
# ... 작업 ...
git commit -m "feat: 회원가입 API" # def5678
# ... 작업 ...
git commit -m "feat: 비밀번호 재설정" # fed9012
# 코드 리뷰 후:
git commit -m "fix: 로그인 typo 수정" # 111aaaa
git commit -m "fix: 회원가입 검증 개선" # 222bbbb
# rebase로 정리하려면:
git rebase -i HEAD~5
수동 방식 (번거로움):
pick abc1234 feat: 로그인 API
pick def5678 feat: 회원가입 API
pick fed9012 feat: 비밀번호 재설정
pick 111aaaa fix: 로그인 typo 수정
pick 222bbbb fix: 회원가입 검증 개선
# 수동으로 순서 변경 필요:
pick abc1234 feat: 로그인 API
fixup 111aaaa fix: 로그인 typo 수정 # ← 수동으로 올림
pick def5678 feat: 회원가입 API
fixup 222bbbb fix: 회원가입 검증 개선 # ← 수동으로 올림
pick fed9012 feat: 비밀번호 재설정
해결: Autosquash 워크플로
1단계: fixup 커밋 생성
# 기존 커밋의 해시를 찾기
git log --oneline
abc1234 feat: 로그인 API
def5678 feat: 회원가입 API
# 로그인 API 수정 후:
git add src/auth/login.js
git commit --fixup=abc1234
# 커밋 메시지 자동 생성: "fixup! feat: 로그인 API"
# 회원가입 API 수정 후:
git add src/auth/signup.js
git commit --fixup=def5678
# 커밋 메시지: "fixup! feat: 회원가입 API"
# 로그 확인
git log --oneline
222bbbb fixup! feat: 회원가입 API
111aaaa fixup! feat: 로그인 API
def5678 feat: 회원가입 API
abc1234 feat: 로그인 API
2단계: autosquash rebase
git rebase -i --autosquash HEAD~4
에디터 자동 정렬 (수동 편집 불필요!):
pick abc1234 feat: 로그인 API
fixup 111aaaa fixup! feat: 로그인 API # ← 자동 이동!
pick def5678 feat: 회원가입 API
fixup 222bbbb fixup! feat: 회원가입 API # ← 자동 이동!
# 그냥 저장하면 됨 (:wq)
3단계: 결과
git log --oneline
def5678 feat: 회원가입 API # 222bbbb 흡수됨
abc1234 feat: 로그인 API # 111aaaa 흡수됨
Autosquash 전역 설정 (필수!)
# 전역 설정: -i만 해도 자동 적용
git config --global rebase.autoSquash true
# 이제 --autosquash 플래그 불필요
git rebase -i HEAD~5 # 자동으로 autosquash 적용
squash 커밋 vs fixup 커밋
# fixup: 메시지 버림 (타이포, 린트 수정)
git commit --fixup=abc1234
# squash: 메시지 보존하고 합침
git commit --squash=abc1234
# 예시:
git log --oneline
abc1234 feat: 로그인 API
# fixup 사용
git commit --fixup=abc1234
git log --oneline
111aaaa fixup! feat: 로그인 API
# squash 사용
git commit --squash=abc1234
git log --oneline
222bbbb squash! feat: 로그인 API
# rebase 시:
# fixup → 메시지 자동 버림
# squash → 에디터 열림, 메시지 편집 기회
실전 워크플로: 리뷰 반영
# 1. 초기 커밋
git commit -m "feat: 사용자 인증 API" # abc1234
# 2. PR 생성 후 리뷰 받음
# 리뷰 코멘트: "변수명 개선 필요"
# 3. 수정 후 fixup 커밋
git add src/auth.js
git commit --fixup=abc1234
# 4. 또 다른 리뷰
# 리뷰 코멘트: "에러 메시지 개선"
# 5. 또 fixup
git add src/auth.js
git commit --fixup=abc1234
# 6. 여러 차례 반복...
git log --oneline
555eeee fixup! feat: 사용자 인증 API
444dddd fixup! feat: 사용자 인증 API
333cccc fixup! feat: 사용자 인증 API
222bbbb fixup! feat: 사용자 인증 API
111aaaa fixup! feat: 사용자 인증 API
abc1234 feat: 사용자 인증 API
# 7. 리뷰 완료 후 최종 정리
git rebase -i --autosquash HEAD~6
# 에디터 자동 정렬:
pick abc1234 feat: 사용자 인증 API
fixup 111aaaa fixup! feat: 사용자 인증 API
fixup 222bbbb fixup! feat: 사용자 인증 API
fixup 333cccc fixup! feat: 사용자 인증 API
fixup 444dddd fixup! feat: 사용자 인증 API
fixup 555eeee fixup! feat: 사용자 인증 API
# 8. 저장 → 모든 fixup이 흡수됨
git log --oneline
abc1234 feat: 사용자 인증 API # 깨끗한 한 개 커밋!
# 9. force push (PR 업데이트)
git push --force-with-lease origin feature/auth
Git Alias로 더 편하게
# ~/.gitconfig에 추가
[alias]
fixup = "!f() { git commit --fixup=$1; }; f"
squash-all = "!f() { git rebase -i --autosquash ${1:-HEAD~10}; }; f"
# 사용:
git fixup abc1234 # git commit --fixup=abc1234 단축
git squash-all HEAD~5 # autosquash rebase 단축
git squash-all # 기본값: HEAD~10
여러 커밋을 하나로 (squash)
방법 1: rebase -i
git rebase -i HEAD~4
pick abc1234 feat: 결제 API 추가
squash def5678 feat: 결제 검증 추가
squash fed9012 feat: 결제 이력 저장
squash 1234567 feat: 결제 알림 추가
방법 2: reset —soft
git reset --soft HEAD~4
git commit -m "feat: 결제 플로우 통합"
이 방식은 새 단일 커밋으로 덮어쓰므로, 이미 push한 브랜치면 같은 주의가 필요합니다.
부분 스테이징 (git add -p) - 마스터하기
부분 스테이징은 하나의 파일 내에서 일부 변경사항만 골라서 커밋하는 강력한 기능입니다.
기본 사용법
시나리오: 한 파일에 여러 변경사항
// src/auth.js (수정 전)
function login(email, password) {
return authenticateWithEmail(email, password);
}
function logout() {
clearSession();
}
변경사항:
// src/auth.js (수정 후)
function login(email, password) {
// 변경 1: 입력 검증 추가
if (!email || !password) {
throw new Error('Email and password required');
}
return authenticateWithEmail(email, password);
}
function logout() {
// 변경 2: 로깅 추가
console.log('User logged out');
clearSession();
}
목표: 두 변경사항을 별도 커밋으로 분리
1단계: 부분 스테이징 시작
git add -p src/auth.js
# 또는
git add --patch src/auth.js
2단계: 첫 번째 hunk 표시
diff --git a/src/auth.js b/src/auth.js
index 1234567..abcdefg 100644
--- a/src/auth.js
+++ b/src/auth.js
@@ -1,5 +1,10 @@
function login(email, password) {
+ // 변경 1: 입력 검증 추가
+ if (!email || !password) {
+ throw new Error('Email and password required');
+ }
return authenticateWithEmail(email, password);
}
(1/2) Stage this hunk [y,n,q,a,d,s,e,?]?
명령어 가이드:
y - yes, 이 hunk를 스테이징 (추가)
n - no, 이 hunk를 건너뛰기
q - quit, 종료 (지금까지 선택한 것만 스테이징)
a - all, 이 파일의 모든 hunk를 스테이징
d - don't stage, 이 파일의 나머지를 모두 건너뛰기
s - split, hunk를 더 작게 분할
e - edit, hunk를 수동으로 편집
? - help, 도움말 표시
3단계: 첫 번째 변경사항 선택
Stage this hunk [y,n,q,a,d,s,e,?]? y
4단계: 두 번째 hunk 표시
@@ -10,5 +15,6 @@
function logout() {
+ console.log('User logged out');
clearSession();
}
(2/2) Stage this hunk [y,n,q,a,d,s,e,?]?
5단계: 두 번째 변경사항 건너뛰기
Stage this hunk [y,n,q,a,d,s,e,?]? n
6단계: 첫 번째 커밋
git status
# Changes to be committed:
# modified: src/auth.js (검증 로직만)
# Changes not staged for commit:
# modified: src/auth.js (로깅 부분)
git commit -m "feat: 로그인 입력 검증 추가"
7단계: 두 번째 커밋
git add -p src/auth.js
# (2/2) Stage this hunk [y,n,q,a,d,s,e,?]? y
git commit -m "feat: 로그아웃 로깅 추가"
고급 기능: split (s)
상황: hunk가 너무 클 때
@@ -1,10 +1,15 @@
function login(email, password) {
+ if (!email || !password) {
+ throw new Error('Email and password required');
+ }
+ if (password.length < 8) {
+ throw new Error('Password too short');
+ }
return authenticateWithEmail(email, password);
}
Stage this hunk [y,n,q,a,d,s,e,?]?
문제: 검증 로직이 2개인데 하나만 커밋하고 싶음
해결: split 사용
Stage this hunk [y,n,q,a,d,s,e,?]? s
Split into 2 hunks.
@@ -1,5 +1,8 @@
function login(email, password) {
+ if (!email || !password) {
+ throw new Error('Email and password required');
+ }
return authenticateWithEmail(email, password);
}
(1/2) Stage this hunk [y,n,q,a,d,s,e,?]? y
@@ -3,6 +6,9 @@
+ if (password.length < 8) {
+ throw new Error('Password too short');
+ }
return authenticateWithEmail(email, password);
}
(2/2) Stage this hunk [y,n,q,a,d,s,e,?]? n
고급 기능: edit (e)
상황: 라인 단위로 선택하고 싶을 때
@@ -1,5 +1,10 @@
function login(email, password) {
+ console.log('Login attempt'); // 디버깅용 (커밋 안 함)
+ if (!email || !password) {
+ throw new Error('Email and password required');
+ }
+ console.log('Validation passed'); // 디버깅용 (커밋 안 함)
return authenticateWithEmail(email, password);
}
Stage this hunk [y,n,q,a,d,s,e,?]? e
에디터가 열리며:
# Manual hunk edit mode -- see bottom for a quick guide.
@@ -1,5 +1,10 @@
function login(email, password) {
+ console.log('Login attempt');
+ if (!email || !password) {
+ throw new Error('Email and password required');
+ }
+ console.log('Validation passed');
return authenticateWithEmail(email, password);
}
# ---
# To remove '+' lines, delete them.
# To remove '-' lines, change them to ' ' (space).
# Lines starting with # will be removed.
수동 편집: 디버깅 로그 제거
@@ -1,5 +1,10 @@
function login(email, password) {
+ if (!email || !password) {
+ throw new Error('Email and password required');
+ }
return authenticateWithEmail(email, password);
}
저장 후 종료 → 검증 로직만 스테이징됨
실전 예제: 리팩토링 중 커밋 분리
시나리오:
// src/api.js
// 한 번에 여러 개선을 했지만 논리적으로 분리하고 싶음
// Before
function fetchUser(id) {
return fetch('/api/users/' + id)
.then(res => res.json())
.catch(err => console.error(err));
}
// After
async function fetchUser(id) {
// 개선 1: async/await
// 개선 2: 에러 핸들링
// 개선 3: 타입 검증
if (!id || typeof id !== 'number') {
throw new TypeError('Invalid user ID');
}
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch user:', error);
throw error;
}
}
커밋 전략:
- async/await 변환
- 타입 검증 추가
- 에러 핸들링 개선
실행:
# 1차: async/await만
git add -p src/api.js
# → async/await 관련 줄만 선택 (y/n/e 활용)
git commit -m "refactor: async/await로 변환"
# 2차: 타입 검증
git add -p src/api.js
# → 타입 검증 부분만 선택
git commit -m "feat: 사용자 ID 타입 검증 추가"
# 3차: 에러 핸들링
git add src/api.js
git commit -m "feat: HTTP 에러 핸들링 개선"
유용한 패턴
패턴 1: 디버깅 코드 제외하고 커밋
# console.log는 커밋 안 함
git add -p src/app.js
# → console.log 줄만 n으로 건너뛰기
패턴 2: TODO 주석 남기기
# 일부 기능만 완성, 나머지는 TODO
git add -p src/feature.js
# → 완성된 부분만 y
# → TODO 있는 부분은 n
패턴 3: 포맷팅과 로직 분리
# 1차: 로직 변경만
git add -p src/module.js
# → 실제 로직 변경만 선택
git commit -m "feat: 새 기능 추가"
# 2차: 포맷팅
git add src/module.js
git commit -m "style: 코드 포맷팅"
부분 스테이징과 rebase 조합
# 1. 큰 변경사항을 여러 커밋으로 분할
git add -p src/auth.js
git commit -m "feat: 검증 로직"
git add -p src/auth.js
git commit -m "feat: 에러 핸들링"
git add src/auth.js
git commit -m "docs: 주석 추가"
# 2. 히스토리 확인
git log --oneline
333cccc docs: 주석 추가
222bbbb feat: 에러 핸들링
111aaaa feat: 검증 로직
# 3. rebase로 순서 조정
git rebase -i HEAD~3
# → 논리적 순서로 재배치
Alias로 더 편하게
# ~/.gitconfig
[alias]
ap = add -p
unstage = reset HEAD --
# 사용:
git ap src/auth.js # git add -p 단축
핵심 요약:
부분 스테이징(git add -p)은 논리적으로 의미 있는 커밋을 만드는 핵심 도구입니다:
y: 이 변경사항 포함n: 건너뛰기s: 더 작게 분할e: 수동 편집 (라인 단위 선택)
큰 변경사항을 한 번에 커밋하지 말고, 부분 스테이징으로 의미 있는 단위로 나누세요!
커밋 순서 변경
에디터에서 줄 순서 변경
git rebase -i HEAD~3
변경 전
pick abc1234 feat: 로그인 API
pick def5678 feat: 회원가입 API
pick fed9012 feat: 비밀번호 재설정 API
변경 후
pick def5678 feat: 회원가입 API
pick abc1234 feat: 로그인 API
pick fed9012 feat: 비밀번호 재설정 API
커밋 삭제 (drop)
에디터에서 drop
git rebase -i HEAD~3
pick abc1234 feat: 로그인 API
drop def5678 wip: 테스트 코드
pick fed9012 feat: 회원가입 API
또는 해당 줄을 삭제해도 동일한 효과입니다.
비교/주의: merge와의 차이
비교표
| 항목 | merge | rebase |
|---|---|---|
| 히스토리 | 병합 커밋이 남음 | 선형에 가깝게 정리 가능 |
| 공유 브랜치 | 안전 | 히스토리 변경 시 협업자와 충돌 |
| 충돌 | 한 번에 병합 | 커밋마다 순차 해결 가능 |
| 커밋 정리 | 불가능 | 가능 (squash, fixup) |
| 이력 추적 | 병합 시점 명확 | 선형이라 추적 어려움 |
merge 예시
git checkout main
git merge feature/login
* abc1234 (HEAD -> main) Merge branch 'feature/login'
|\
| * def5678 feat: 로그인 API
| * fed9012 feat: 토큰 발급
|/
* 1234567 feat: 회원가입 API
rebase 예시
git checkout feature/login
git rebase main
* abc1234 (HEAD -> feature/login) feat: 토큰 발급
* def5678 feat: 로그인 API
* 1234567 (main) feat: 회원가입 API
언제 merge를 사용하나?
- 공유 브랜치 (main, develop)
- 히스토리 보존이 중요한 경우
- 병합 시점 추적이 필요한 경우
언제 rebase를 사용하나?
- 개인 feature 브랜치
- PR 전 커밋 정리
- 선형 히스토리를 선호하는 팀 public branch에 rebase 후 push는 팀 규칙이 없으면 피하고, 개인 feature 브랜치에서 정리한 뒤 PR을 올리는 용도가 가장 무난합니다.
실무 사례
사례 1: PR 전 커밋 정리
문제: 개발 중 임시 커밋이 많음
* abc1234 feat: 로그인 API
* def5678 wip
* fed9012 fix lint
* 1234567 fix typo
* 2345678 feat: 토큰 발급
해결: fixup으로 정리
git rebase -i HEAD~5
pick abc1234 feat: 로그인 API
fixup def5678 wip
fixup fed9012 fix lint
fixup 1234567 fix typo
pick 2345678 feat: 토큰 발급
결과
* abc1234 feat: 로그인 API
* 2345678 feat: 토큰 발급
사례 2: 커밋 메시지 컨벤션 통일
문제: 메시지 형식이 일관되지 않음
* abc1234 add login api
* def5678 fix bug
* fed9012 update token logic
해결: reword로 통일
git rebase -i HEAD~3
reword abc1234 add login api
reword def5678 fix bug
reword fed9012 update token logic
각 커밋 메시지를 Conventional Commits 형식으로 변경:
feat: 로그인 API 추가
fix: 토큰 검증 버그 수정
refactor: 토큰 발급 로직 개선
사례 3: 큰 커밋 분할
문제: 하나의 커밋에 여러 기능이 섞임
* abc1234 feat: 로그인 및 회원가입 API
해결: edit로 분할
git rebase -i HEAD~1
edit abc1234 feat: 로그인 및 회원가입 API
git reset HEAD^
git add src/login.js
git commit -m "feat: 로그인 API 추가"
git add src/signup.js
git commit -m "feat: 회원가입 API 추가"
git rebase --continue
결과
* def5678 feat: 회원가입 API 추가
* abc1234 feat: 로그인 API 추가
사례 4: 리뷰 반영 커밋 정리
문제: 리뷰 반영 커밋이 많음
* abc1234 feat: 로그인 API
* def5678 리뷰 반영: 검증 추가
* fed9012 리뷰 반영: 에러 핸들링
* 1234567 리뷰 반영: 테스트 추가
해결: fixup 자동 정렬
git commit --fixup=abc1234
git commit --fixup=abc1234
git commit --fixup=abc1234
git rebase -i --autosquash HEAD~4
결과
* abc1234 feat: 로그인 API
트러블슈팅: 충돌·중단·복구
문제 1: rebase가 완전히 꼬였을 때
증상:
# 여러 충돌이 연속으로 발생
CONFLICT in src/auth.js
# 해결 → continue
CONFLICT in src/token.js
# 해결 → continue
CONFLICT in src/auth.js # 또 충돌!
# 해결 → continue
CONFLICT in src/utils.js
# "이거 뭔가 잘못됐는데..."
해결: reflog로 시간 여행
Git의 reflog는 모든 HEAD 이동을 기록합니다. rebase로 망가뜨려도 되돌릴 수 있습니다!
# 1. reflog 확인
git reflog
# 출력:
abc1234 (HEAD) HEAD@{0}: rebase -i (continue): feat: 로그인 API
def5678 HEAD@{1}: rebase -i (continue): feat: 회원가입 API
fed9012 HEAD@{2}: rebase -i (start): checkout HEAD~3
1234567 HEAD@{3}: commit: feat: 토큰 발급
2345678 HEAD@{4}: commit: feat: 로그인 API
3456789 HEAD@{5}: commit: feat: 회원가입 API
↑ rebase 시작 전 상태!
# 2. rebase 시작 전으로 되돌리기
git reset --hard HEAD@{5}
# 또는 (더 안전)
git reset --hard 3456789
# 3. 확인
git log --oneline
# 원래 히스토리로 완전히 복구됨!
reflog 이해하기:
HEAD@{0}: 현재 (0분 전)
HEAD@{1}: 5분 전
HEAD@{2}: 10분 전
HEAD@{3}: 1시간 전
...
# 시간 기반 조회
git reflog --date=relative
abc1234 HEAD@{5 minutes ago}: commit
def5678 HEAD@{1 hour ago}: commit
# 특정 시점으로 복구
git reset --hard HEAD@{1.hour.ago}
실무 팁: rebase 전 백업 태그
# rebase 전 안전장치
git tag backup-before-rebase
# rebase 진행
git rebase -i main
# 문제 발생 시 복구
git reset --hard backup-before-rebase
# 성공 시 태그 삭제
git tag -d backup-before-rebase
문제 2: 잘못 continue를 눌렀을 때
증상:
# 충돌을 대충 해결하고 continue
git rebase --continue
# 나중에 발견: "아, 이거 잘못 해결했네..."
git log
abc1234 (HEAD) feat: 로그인 API # 코드가 이상함
해결 1: 즉시 발견한 경우
# 아직 rebase 중이면
git rebase --abort
# 완전히 끝났으면 reflog
git reflog
git reset --hard HEAD@{3} # rebase 시작 전
해결 2: 이미 여러 커밋 진행한 경우
# 1. 현재 위치 저장
git branch temp-save
# 2. rebase 시작 전으로
git reset --hard HEAD@{10}
# 3. 다시 rebase
git rebase -i HEAD~5
# 4. temp-save와 비교하며 참고
git diff temp-save
# 5. 성공 시 temp 삭제
git branch -D temp-save
문제 3: 에디터가 안 열리거나 이상하게 동작
증상 1: 에디터가 즉시 닫힘
git rebase -i HEAD~3
# 에디터가 1초도 안 돼서 닫힘
# rebase 자동 완료 (변경 없이)
원인: VS Code 등 GUI 에디터에서 --wait 플래그 누락
해결:
# 현재 설정 확인
git config --global core.editor
# 출력: code
# 올바른 설정
git config --global core.editor "code --wait"
# ↑ 중요!
# 테스트
git rebase -i HEAD~1
# 에디터가 닫힐 때까지 기다림
증상 2: 에디터가 아예 안 열림
git rebase -i HEAD~3
# 아무 일도 안 일어남
해결:
# 환경 변수 확인
echo $GIT_EDITOR
echo $VISUAL
echo $EDITOR
# 모두 비어있으면 설정
export GIT_EDITOR=vim
export VISUAL=vim
export EDITOR=vim
# 영구 설정 (.bashrc / .zshrc)
echo 'export GIT_EDITOR=vim' >> ~/.bashrc
source ~/.bashrc
증상 3: Windows에서 에디터 경로 오류
# PowerShell/CMD에서
git config --global core.editor "notepad"
# 또는 Notepad++
git config --global core.editor "'C:/Program Files/Notepad++/notepad++.exe' -multiInst"
# VS Code (공백 있는 경로)
git config --global core.editor "'C:/Program Files/Microsoft VS Code/bin/code' --wait"
문제 4: 충돌이 반복될 때
증상:
# 같은 파일에서 계속 충돌
CONFLICT in src/auth.js # 1번째 커밋
# 해결 → continue
CONFLICT in src/auth.js # 2번째 커밋
# 해결 → continue
CONFLICT in src/auth.js # 3번째 커밋
# "이거 계속 반복되는데..."
원인 분석:
# 5개 커밋이 모두 같은 함수를 수정
* 5️⃣ 타임아웃 추가
* 4️⃣ 재시도 로직 추가
* 3️⃣ 에러 메시지 개선
* 2️⃣ 파라미터 변경
* 1️⃣ 로그인 함수 추가
# rebase 시 각 커밋마다 충돌 발생
해결 1: 먼저 squash로 합치기
# 1. 충돌 나는 커밋들을 먼저 합침
git rebase -i HEAD~5
pick 1️⃣ 로그인 함수 추가
squash 2️⃣ 파라미터 변경
squash 3️⃣ 에러 메시지 개선
squash 4️⃣ 재시도 로직 추가
squash 5️⃣ 타임아웃 추가
# 2. 하나의 커밋으로 합쳐짐 → 충돌 한 번만!
# 3. 이후 다른 rebase 작업 수행
해결 2: rerere 활성화
# rerere: Reuse Recorded Resolution
git config --global rerere.enabled true
# 동작 방식:
# 1. 첫 번째 충돌 해결
git add src/auth.js
git rebase --continue
# 2. Git이 해결 방법 기록 (.git/rr-cache/)
# 3. 동일한 충돌 발생 시 자동 적용
# 확인
ls .git/rr-cache/
# 디렉토리에 해결 방법 저장됨
rerere 실전 예제:
# 시나리오: 긴 rebase에서 같은 충돌 반복
git rebase -i HEAD~20
# 5번째 커밋에서 src/auth.js 충돌
# 수동 해결
vim src/auth.js
git add src/auth.js
git rebase --continue
# 12번째 커밋에서 동일한 충돌
# → rerere가 자동 해결! ✨
# Resolved 'src/auth.js' using previous resolution.
git rebase --continue
# 충돌 없이 바로 진행됨
문제 5: force push 후 팀원 문제
증상: 팀원의 에러 메시지
# 팀원 A가 작업 중:
git push
# To https://github.com/user/repo.git
# ! [rejected] feature/login -> feature/login (non-fast-forward)
# error: failed to push some refs
원인:
내가 force push:
원격: A --- B --- C --- D' --- E' (rebase 후)
팀원: A --- B --- C --- D --- E --- F (로컬 작업)
↑ 히스토리 불일치
팀원 해결 방법 (단계별):
# 1. 로컬 변경사항 확인
git status
# 수정 중인 파일 있으면 stash
git stash push -m "작업 중인 내용 임시 저장"
# 2. 원격 최신 상태 가져오기
git fetch origin
# 3. 현재 브랜치와 원격 비교
git log --oneline --graph HEAD origin/feature/login
# * abc1234 (HEAD) 내 로컬 커밋
# * def5678 (origin/feature/login) 원격 커밋
# → 완전히 다른 히스토리!
# 4. 옵션 A: 원격 기준으로 리셋 (로컬 작업 버림)
git reset --hard origin/feature/login
# ⚠️ 로컬 커밋 F가 사라짐!
# 5. 옵션 B: 로컬 작업 유지하고 rebase
git rebase origin/feature/login
# 로컬 커밋 F가 D', E' 위에 재적용됨
# 6. stash 복구
git stash pop
예방책: 팀 규칙 수립
# 규칙 1: force push 전 알림
# Slack/Discord: "@team feature/login rebase 예정, 커밋하고 푸시하세요"
# 규칙 2: 개인 브랜치만 rebase
# 공유 feature 브랜치: merge 사용
# 개인 feature 브랜치: rebase 허용
# 규칙 3: --force 대신 --force-with-lease
git push --force-with-lease origin feature/login
# 원격이 예상과 다르면 거부 (안전장치)
# 규칙 4: rebase 후 새 브랜치
git checkout -b feature/login-v2
git push origin feature/login-v2
# 팀원들이 새 브랜치로 이동
문제 6: 에디터에서 잘못 저장
증상: 의도치 않은 변경
# 실수로 모든 줄을 drop으로 변경
drop abc1234 feat: 로그인 API
drop def5678 feat: 회원가입 API
drop fed9012 feat: 토큰 발급
# 저장 (:wq) → 모든 커밋 삭제!
즉시 해결:
# rebase가 실행되면
Successfully rebased and updated refs/heads/feature/login.
# reflog로 복구
git reflog
git reset --hard HEAD@{1} # rebase 시작 전
예방: 빈 파일 저장 시 취소
# 모든 줄 삭제하고 저장하면
# Git이 rebase 자동 취소
# 에디터 비우기
(모든 줄 삭제)
:wq
# 출력:
Nothing to do
문제 7: 커밋 메시지 편집 중 취소하고 싶을 때
reword 중 취소:
# reword 실행 → 메시지 에디터 열림
# "아, 이거 바꾸면 안 되는데..."
# 방법 1: 빈 메시지 저장
(모든 내용 삭제)
:wq
# Aborting commit due to empty commit message.
git rebase --abort
# 방법 2: 저장 없이 종료
:q! # Vim
Ctrl+C # Nano
# 창 닫기 (저장 안 함) # VS Code
git rebase --abort
문제 8: rebase 중 커밋이 사라짐
증상:
# rebase 전
git log --oneline
fed9012 feat: 중요한 기능
def5678 feat: 로그인 API
abc1234 feat: 회원가입 API
# rebase 후
git log --oneline
def5678 feat: 로그인 API
abc1234 feat: 회원가입 API
# fed9012가 사라짐!
원인:
에디터에서:
pick abc1234 feat: 회원가입 API
pick def5678 feat: 로그인 API
# fed9012 줄을 실수로 삭제
복구:
# reflog에서 찾기
git reflog | grep "feat: 중요한 기능"
fed9012 HEAD@{5}: commit: feat: 중요한 기능
# cherry-pick으로 복구
git cherry-pick fed9012
# 또는 전체 되돌리기
git reset --hard HEAD@{5}
Reflog의 내부 동작 원리
reflog가 모든 것을 기억하는 방법:
.git/logs/ 디렉토리:
├── HEAD # HEAD의 이동 기록
├── refs/
│ ├── heads/
│ │ ├── main # main 브랜치 이동 기록
│ │ └── feature/login # feature/login 브랜치 이동 기록
│ └── remotes/
│ └── origin/
│ └── main # 원격 브랜치 fetch 기록
각 파일 내용:
old-sha1 new-sha1 작성자 <이메일> timestamp <tab> 작업 내용
예시 (.git/logs/HEAD):
0000000000 abc1234567 John <[email protected]> 1711234567 +0900 commit (initial): Initial commit
abc1234567 def5678901 John <[email protected]> 1711234670 +0900 commit: Add login feature
def5678901 fed9012345 John <[email protected]> 1711234800 +0900 rebase -i (start): onto abc1234567
fed9012345 a1b2c3d4e5 John <[email protected]> 1711234900 +0900 rebase -i (finish): returning to refs/heads/feature/login
a1b2c3d4e5 123456789a John <[email protected]> 1711235000 +0900 reset: moving to HEAD~1
reflog 조회 명령어:
# 1. 기본 조회
git reflog
# a1b2c3d (HEAD -> feature/login) HEAD@{0}: reset: moving to HEAD~1
# fed9012 HEAD@{1}: rebase -i (finish): returning to refs/heads/feature/login
# def5678 HEAD@{2}: rebase -i (start): onto abc1234567
# 2. 특정 브랜치 reflog
git reflog show feature/login
# rebase, commit, reset 등 해당 브랜치의 모든 변경
# 3. 상세 정보 포함
git reflog show --date=iso
# 2026-04-17 10:30:00 +0900 HEAD@{0}: reset: moving to HEAD~1
# 2026-04-17 10:25:00 +0900 HEAD@{1}: rebase -i (finish)
# 4. 특정 파일 변경 추적
git log -g --grep="login"
# reflog에서 "login" 키워드 검색
# 5. 삭제된 커밋 찾기
git reflog --all | grep "feat: 중요한 기능"
# 모든 ref (HEAD, 브랜치, 태그)의 reflog 검색
reflog 유지 기간:
# 기본 설정 확인
git config --get gc.reflogExpire
# default: 90일
git config --get gc.reflogExpireUnreachable
# default: 30일 (참조되지 않는 커밋)
# 유지 기간 변경
git config gc.reflogExpire "180 days" # 6개월
git config gc.reflogExpireUnreachable "60 days"
# 영구 보존
git config gc.reflogExpire "never"
# Git GC (Garbage Collection) 수동 실행:
git gc
# - 오래된 reflog 항목 삭제
# - 참조되지 않는 객체 정리
# - 팩 파일 최적화
reflog 복구 시나리오:
시나리오 1: rebase 후 커밋이 사라짐
1. rebase 전:
A --- B --- C --- D --- E (feature/login)
2. rebase 중 실수로 D 제거:
A --- B --- C --- E' (feature/login)
3. reflog 확인:
git reflog
# e5f6a78 HEAD@{0}: rebase -i (finish): returning to refs/heads/feature/login
# d4c3b2a HEAD@{1}: rebase -i (fixup): C
# c3b2a1f HEAD@{2}: commit: D ← 여기!
4. D 복구:
git cherry-pick c3b2a1f
# 또는
git reset --hard HEAD@{2} # rebase 시작 전으로 돌아감
시나리오 2: 브랜치를 잘못 삭제
1. 브랜치 삭제:
git branch -D feature/important
# Deleted branch feature/important (was abc1234).
2. reflog에서 찾기:
git reflog show --all | grep important
# abc1234 refs/heads/feature/important@{0}: commit: Important feature
3. 브랜치 복구:
git branch feature/important abc1234
# 또는
git checkout -b feature/important abc1234
시나리오 3: reset --hard로 작업 날림
1. 작업 중:
(수정한 파일들...)
2. 실수로 reset:
git reset --hard HEAD
# HEAD is now at abc1234
# ⚠️ 작업 내용 사라짐!
3. reflog로는 복구 불가:
# reset --hard는 커밋되지 않은 변경사항을 reflog에 기록 안 함
4. 예방책:
# reset 전 항상 stash 또는 커밋
git stash push -m "작업 중"
# 또는
git commit -m "WIP: work in progress"
Cherry-pick과 Rebase의 관계
Rebase는 내부적으로 Cherry-pick을 반복:
git rebase main 실행 시:
1. 공통 조상 찾기:
main: A --- B --- C
\
feature: D --- E --- F
공통 조상: B
2. feature의 커밋들을 임시 저장:
.git/rebase-apply/0001 # D의 패치
.git/rebase-apply/0002 # E의 패치
.git/rebase-apply/0003 # F의 패치
3. feature를 main으로 이동:
git reset --hard main
# feature는 이제 C를 가리킴
4. 각 커밋을 순차적으로 cherry-pick:
git cherry-pick D → D' 생성
git cherry-pick E → E' 생성
git cherry-pick F → F' 생성
결과:
main: A --- B --- C
\
feature: D' --- E' --- F'
Cherry-pick의 내부 메커니즘:
git cherry-pick abc1234 실행 시:
1. 커밋 abc1234의 diff 계산:
git show abc1234
diff --git a/src/login.js b/src/login.js
--- a/src/login.js
+++ b/src/login.js
@@ -10,6 +10,10 @@
+function validatePassword(pwd) {
+ return pwd.length >= 8;
+}
2. 현재 HEAD에 패치 적용:
git apply <패치>
3-way merge 사용:
- Base: abc1234의 부모 커밋
- Ours: 현재 HEAD
- Theirs: abc1234의 변경사항
3. 충돌 처리:
충돌 없음:
→ 자동으로 커밋 생성
→ 원본 커밋의 author 유지
→ committer는 현재 사용자/시간
충돌 발생:
→ 충돌 마커 표시
→ 사용자 해결 대기
→ git add → git cherry-pick --continue
4. 새 커밋 생성:
커밋 메시지: 원본과 동일
author: 원본 유지
committer: 현재 사용자/시간
parent: 현재 HEAD
tree: 패치 적용 결과
→ 완전히 새로운 커밋 해시
Rebase vs Cherry-pick 비교:
차이점:
Rebase:
- 여러 커밋을 자동으로 순차 적용
- 브랜치의 base를 변경
- 충돌 시 각 커밋마다 해결
- interactive 모드: 커밋 편집 가능
Cherry-pick:
- 특정 커밋만 선택적으로 가져옴
- 브랜치 base는 그대로
- 한 번에 하나씩 수동 적용
- 커밋 내용만 복사, 히스토리는 독립적
실전 활용:
Rebase 사용:
✅ feature 브랜치를 main에 맞춰 업데이트
✅ 커밋 히스토리 전체 정리
✅ 여러 커밋을 동시에 재배치
Cherry-pick 사용:
✅ 특정 버그 수정만 hotfix로 가져오기
✅ 실수로 잘못된 브랜치에 커밋한 경우
✅ 다른 브랜치의 특정 기능만 가져오기
실전 예시:
# 예시 1: hotfix cherry-pick
# main에 critical 버그 수정 필요
# feature 브랜치에 이미 수정됨
git checkout main
git cherry-pick abc1234 # feature의 버그 수정 커밋
git push origin main
# feature 브랜치는 그대로 유지
# main에만 해당 수정 적용됨
# 예시 2: 잘못된 브랜치에 커밋
# feature-A에 커밋했는데 feature-B에 해야 했음
git log
# fed9012 (HEAD -> feature-A) feat: 새 기능
git checkout feature-B
git cherry-pick fed9012
git push origin feature-B
git checkout feature-A
git reset --hard HEAD~1 # feature-A에서 제거
git push --force-with-lease origin feature-A
# 예시 3: 여러 커밋 선택적 가져오기
# feature 브랜치의 일부 커밋만 hotfix에 필요
git checkout hotfix/urgent
git cherry-pick abc1234 # 커밋 1
git cherry-pick def5678 # 커밋 2
git cherry-pick fed9012 # 커밋 3
# 순서 변경 가능:
git cherry-pick fed9012 # 3번 먼저
git cherry-pick abc1234 # 1번 나중
# 범위 지정:
git cherry-pick abc1234..fed9012
# abc1234는 제외, def5678~fed9012 포함
Cherry-pick 충돌 해결:
# cherry-pick 중 충돌
git cherry-pick abc1234
# CONFLICT (content): Merge conflict in src/login.js
# 1. 충돌 확인
git status
# both modified: src/login.js
# 2. 파일 수정
vim src/login.js
# 3. 해결 표시
git add src/login.js
# 4. 계속 진행
git cherry-pick --continue
# 또는 취소
git cherry-pick --abort
# 또는 건너뛰기 (cherry-pick에서는 skip 없음, 그냥 abort)
git cherry-pick --abort
문제 9: 충돌 해결 실수 검증
충돌을 잘못 해결했는지 확인:
# 1. 테스트 실행
npm test
# 실패하면 뭔가 잘못됨
# 2. 문법 체크
npm run lint
# 3. 빌드 확인
npm run build
# 4. 변경사항 확인
git diff HEAD^ # 이전 커밋과 비교
git show HEAD # 현재 커밋 내용
# 5. 문제 발견 시
# 방법 A: 마지막 커밋만 수정
git commit --amend
# 방법 B: rebase 중이면 abort하고 재시도
git rebase --abort
git rebase -i HEAD~3
자동 테스트로 예방:
# rebase 중 각 커밋마다 테스트 실행
git rebase -i HEAD~5
# 에디터에서:
pick abc1234 feat: 로그인 API
exec npm test # ← 테스트 자동 실행
pick def5678 feat: 회원가입 API
exec npm test
pick fed9012 feat: 토큰 발급
exec npm test
# 테스트 실패 시 rebase 자동 중단
# → 코드 수정 → git add → git rebase --continue
문제 10: rebase 중 특정 커밋 건너뛰기
상황:
# rebase 중 충돌 발생
CONFLICT (content): Merge conflict in src/deprecated.js
git status
# both modified: src/deprecated.js
# "이 파일은 더 이상 안 쓰는데... 그냥 무시하고 싶다"
해결: skip 사용
# 현재 커밋을 건너뛰기
git rebase --skip
# 효과:
# - 현재 커밋의 변경사항 무시
# - 다음 커밋으로 진행
# 주의: 나중에 문제 생길 수 있음
# 해당 커밋에 의존하는 코드가 있으면 버그 발생
더 나은 방법: drop 사용
# rebase 재시작
git rebase --abort
# 처음부터 drop으로 표시
git rebase -i HEAD~5
pick abc1234 feat: 로그인 API
drop def5678 feat: deprecated 기능 # ← 명시적으로 제거
pick fed9012 feat: 회원가입 API
문제 11: rebase 중간에 멈추고 작업하기
상황: rebase 중인데 급한 버그 수정 필요
해결: stash 활용
# 1. 현재 rebase 상태 저장
git status
# rebase in progress
# 충돌 해결 중이면
git add .
git stash
# 2. rebase 중단
git rebase --abort
# 3. 급한 작업
git checkout main
git checkout -b hotfix/urgent-bug
# ... 버그 수정 ...
git commit -m "fix: urgent bug"
git push origin hotfix/urgent-bug
# 4. 원래 작업으로 복귀
git checkout feature/login
# 5. rebase 재시작
git rebase -i main
# 6. stash 복구 (필요시)
git stash pop
마무리
핵심 원칙 (반드시 기억)
1. 사용 범위
✅ 개인 feature 브랜치
✅ PR 머지 전
✅ 로컬에서만 작업한 커밋
❌ main/develop 등 공유 브랜치
❌ 이미 머지된 커밋
❌ 팀원이 베이스로 사용 중인 브랜치
2. force push 규칙
# ❌ 절대 금지
git push --force
# ✅ 안전한 방법
git push --force-with-lease origin feature/login
# 효과: 원격 브랜치가 예상과 다르면 거부
# (다른 사람이 푸시한 경우 보호)
3. 백업 습관
# rebase 전 항상
git branch backup-$(date +%Y%m%d-%H%M%S)
# 또는
git tag backup-before-rebase
# 문제 발생 시
git reset --hard backup-before-rebase
4. 충돌 해결 원칙
- 한 번에 하나씩 해결
- 각 단계마다 테스트 실행
- 확신 없으면
git rebase --abort - reflog는 최후의 보루
5. autosquash 활용
# 전역 설정 (필수!)
git config --global rebase.autoSquash true
# 리뷰 반영 시
git commit --fixup=<commit-hash>
# 최종 정리
git rebase -i HEAD~10 # 자동 정렬
권장 워크플로
# 1. Feature 브랜치 생성
git checkout -b feature/new-feature main
# 2. 개발 (자유롭게 커밋)
git commit -m "wip"
git commit -m "add feature"
git commit -m "fix typo"
git commit -m "wip2"
# 3. 리뷰 준비: 로컬에서 정리
git rebase -i HEAD~10
# pick/squash/fixup으로 깔끔하게
# 메시지도 Conventional Commits 형식으로
# 4. PR 생성
git push origin feature/new-feature
# 5. 리뷰 받음
# 코멘트: "검증 로직 개선 필요"
# 6. fixup 커밋으로 반영
git commit --fixup=<해당-커밋-해시>
git push origin feature/new-feature
# 7. 리뷰 완료 후 최종 정리
git rebase -i --autosquash HEAD~15
git push --force-with-lease origin feature/new-feature
# 8. PR 머지
# GitHub에서 "Squash and merge" 또는 "Rebase and merge"
# 9. 정리
git checkout main
git pull
git branch -d feature/new-feature
팀 규칙 예시
# Git Rebase 가이드 (팀 규칙)
## 허용되는 경우
- 개인 feature 브랜치에서 PR 머지 전
- 로컬에서만 작업한 커밋 정리
- fixup/squash로 리뷰 반영 커밋 정리
## 금지되는 경우
- main, develop 등 보호된 브랜치
- 이미 머지된 커밋
- 다른 사람이 베이스로 사용 중인 브랜치
## Force Push
- `--force` 금지
- `--force-with-lease`만 허용
- force push 전 Slack에 알림
## 충돌 시
- 모르겠으면 `git rebase --abort`
- 팀원에게 도움 요청
- 절대 강제로 진행하지 말 것
유용한 Alias 모음
# ~/.gitconfig에 추가
[alias]
# rebase 단축
rb = rebase
rbi = rebase -i
rbc = rebase --continue
rba = rebase --abort
rbs = rebase --skip
# autosquash
fixup = commit --fixup
squash-all = "!f() { git rebase -i --autosquash ${1:-HEAD~10}; }; f"
# 안전한 force push
pushf = push --force-with-lease
# 백업
backup = "!f() { git branch backup-$(date +%Y%m%d-%H%M%S); }; f"
# 로그 보기
lg = log --oneline --graph --decorate --all
# 사용 예시:
# git backup # 백업 생성
# git rbi HEAD~5 # interactive rebase
# git fixup abc1234 # fixup 커밋
# git squash-all # 자동 정리
# git pushf # 안전한 force push
더 알아보기
관련 Git 개념:
- Git 브랜치와 병합 - 기본 브랜치 전략
- Git revert와 rebase - 되돌리기 방법
- Git 충돌 해결 사례 - 복잡한 충돌 해결
추가 학습 자료:
git help rebase- 공식 매뉴얼git rebase --interactive-diff- 변경사항 미리보기 (Git 2.35+)- Git Playground - 안전하게 연습하기
핵심 요약:
Git interactive rebase는 커밋 히스토리를 정리하는 강력한 도구입니다:
- pick: 커밋 유지
- reword: 메시지 수정
- edit: 내용 수정
- squash: 합치고 메시지 편집
- fixup: 합치고 메시지 버림
- drop: 커밋 제거
항상 개인 브랜치에서만 사용하고, reflog로 복구할 수 있다는 것을 기억하세요. 의심스러우면 git rebase --abort!
내부 동작과 핵심 메커니즘
이 글의 주제는 「Git rebase interactive 사용법 | pick·squash·fixup·충돌 해결·실수 복구」입니다. 앞선 튜토리얼을 구현·런타임 관점에서 다시 압축합니다. 구성 요소 간 책임 분리와 관측 가능한 지점을 기준으로 “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)·동시성이 어디서 터지는가”를 한 장면으로 그리면 장애 분석이 빨라집니다.
처리 파이프라인(개념도)
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
경계에서의 지연·실패(시퀀스 관점)
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(프로세스·런타임·게이트웨이) participant D as 의존성(외부 API·DB·큐) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
알고리즘·프로토콜·리소스 관점 체크포인트
- 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(버퍼 경계, 프로토콜 상태, 트랜잭션 격리, 파일 디스크립터 상한)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 동일 입력에 동일 출력이 보장되는 순수 층과, 시간·네트워크·스레드 스케줄에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합, GC·할당, 캐시 미스처럼 누적 비용을 의심 목록에 넣습니다.
- 백프레셔: 생산자가 소비자보다 빠를 때(소켓 버퍼, 큐 깊이, 스트림) 어디서 어떤 신호로 속도를 줄일지 정의합니다.
프로덕션 운영 패턴
실서비스에서는 기능과 함께 관측·배포·보안·비용·규제가 동시에 요구됩니다.
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율/지연 분위수(p95/p99), 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시 계층·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션 호환성·플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·파일 디스크립터·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 가능한 한 프로덕션에 가깝게 맞추는 것이 재현율을 높입니다.
확장 예시: 엔드투엔드 미니 시나리오
「Git rebase interactive 사용법 | pick·squash·fixup·충돌 해결·실수 복구」을 실제 배포·운영 흐름으로 옮긴 체크리스트형 시나리오입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드 표를 API 또는 이벤트 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 한 화면(로그+메트릭+트레이스)에서 추적한다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지(또는 피처 플래그) 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값이 기대 범위인지 본다.
의사코드 스케치(프레임워크 무관)
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request) // 경계에서 거절
authorize(validated, ctx) // 권한·테넌트
result = domainCore(validated) // 순수에 가까운 규칙
persistOrEmit(result, idempotentKey) // I/O: 멱등·재시도 정책
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성 불안정, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정이 로컬과 다름 | 프로필·시크릿·기본값, 지역 리전 | 단일 소스(예: 스키마 검증된 설정)와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. git rebase -i로 커밋을 정리하는 법: pick, squash, fixup, reword, edit와 충돌 해결, reflog로 되돌리기까지 실무 순서로 정리합니다. Git·rebase·interactive… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Git 머지 충돌 해결 실전 사례 | 대규모 리팩토링 브랜치 병합기
- C++ 채팅 서버 완성하기 | 인증·방 관리·메시지 히스토리 구현 [#50-1]
- C++ 핫 리로드 완벽 가이드 | 동적 라이브러리·파일 감시·안전한 교체 [#55-7]
이 글에서 다루는 키워드 (관련 검색어)
Git, rebase, interactive, 커밋 정리, squash, fixup, 이력 관리 등으로 검색하시면 이 글이 도움이 됩니다.