본문으로 건너뛰기
Previous
Next
Git rebase interactive 사용법 | pick·squash·fixup·충돌 해결·실수 복구

Git rebase interactive 사용법 | pick·squash·fixup·충돌 해결·실수 복구

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 (원격)
# → 머지 커밋 생성 또는 충돌

# 결과: 히스토리가 엉망이 됨

안전한 사용 원칙:

  1. 개인 feature 브랜치에서만 rebase
  2. PR 머지 전에만 사용
  3. 팀원과 공유하는 브랜치는 merge 사용
  4. 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

문제: wipfix: typo는 잡음 커밋

단계 1: rebase 시작

git rebase -i HEAD~3

단계 2: 에디터에서 fixup으로 변경

pick abc1234 feat: 로그인 API
pick def5678 fix: typo
fixup fed9012 wip

내부 동작:

  1. abc1234 적용 (feat: 로그인 API)
  2. def5678 적용 (fix: typo)
  3. fed9012의 변경사항만 def5678에 추가
  4. 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. 커밋에 파일 추가/삭제
  2. 코드 내용 수정
  3. 하나의 커밋을 여러 개로 분할
  4. 메시지와 내용 동시 수정

패턴 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;
  }
}

커밋 전략:

  1. async/await 변환
  2. 타입 검증 추가
  3. 에러 핸들링 개선

실행:

# 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와의 차이

비교표

항목mergerebase
히스토리병합 커밋이 남음선형에 가깝게 정리 가능
공유 브랜치안전히스토리 변경 시 협업자와 충돌
충돌한 번에 병합커밋마다 순차 해결 가능
커밋 정리불가능가능 (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·충돌 해결·실수 복구」을 실제 배포·운영 흐름으로 옮긴 체크리스트형 시나리오입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드 표를 API 또는 이벤트 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 한 화면(로그+메트릭+트레이스)에서 추적한다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지(또는 피처 플래그) 확인한다.
  5. 부하 후 검증: 피크 대비 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 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 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, rebase, interactive, 커밋 정리, squash, fixup, 이력 관리 등으로 검색하시면 이 글이 도움이 됩니다.