A-7: rebase와 히스토리 재작성
목차 29
또 만났네요, 홍순구 튜터입니다. 지난 시간 우리는 팀의 협업 약속인 브랜치 전략 세 가지를 구분하고, 남의 공개 저장소에 fork 와 두 원격으로 기여하는 한 바퀴까지 돌려봤어요. 그 끝에서 fork 동기화를 할 때, 원본의 최신 변경을 내 브랜치에 합치려고 merge 를 썼던 거 기억하세요?
그런데 제가 그때 슬쩍 흘린 말이 있어요. "합치는 방법이 merge 만 있는 게 아니다" 라고요. 그리고 더 거슬러 올라가면, GitHub 가 PR 을 버튼으로 합쳐줄 때 합치는 방식 중에 "Rebase" 라는 게 있었죠. 오늘은 그 rebase(리베이스) 를 드디어 직접 손으로 다뤄요. merge 와 rebase 가 커밋의 역사를 어떻게 다르게 만드는지 나란히 놓고 보고, 지저분하게 쌓인 커밋을 깔끔하게 정리하는 법까지 배웁니다.
솔직히 말할게요. 오늘은 이 과목에서 처음으로 난이도가 한 단계 올라가는 시간이에요. rebase, cherry-pick, squash, force-push... 단어부터 좀 무섭게 들리죠? 현업 신입도 "rebase 하랬는데 그게 뭐죠" 하고 식은땀 흘리는 단골 주제예요. 그런데 걱정 마세요. 이름만 낯설지, 하나씩 천천히 보면 전부 "커밋을 옮기고 정리하는" 단순한 일이에요. 게다가 오늘 배울 모든 작업은 망쳐도 되돌릴 수 있어요. A-3 에서 배운 reflog 라는 안전망이 든든하게 받쳐주거든요. 그러니 안심하고 하나씩 가봅시다.
오늘의 여정 — 커밋의 역사를 다시 쓰는 법
① 합치는 길은 두 갈래 — merge vs rebase
② 가지를 옮겨 한 줄로 — git rebase
③ rebase 도중 멈췄을 때 — 충돌 해결
④ 커밋을 편집하는 화면 — rebase -i 기초
⑤ 지저분한 커밋을 깔끔히 — squash·fixup·reword
⑥ 그 커밋 하나만 가져올게 — cherry-pick
⑦ 절대 건드리면 안 되는 선 — rebase 황금률
⑧ 부득이할 땐 안전하게 — push --force-with-lease
①②③에서 rebase 가 무엇이고 어떻게 쓰는지, 도중에 멈추면 어떻게 빠져나오는지를 직접 익혀요. ④⑤에서 여러 커밋을 골라 합치고 다듬는 인터랙티브 rebase 를 다루고, ⑥에서 특정 커밋만 콕 집어 가져오는 cherry-pick 을 봐요. 마지막 ⑦⑧에서 "역사를 고쳐 쓰는 일" 의 안전 수칙 — 황금률과 안전한 강제 push — 을 짚으며 협업 git 의 마지막 고급 조각을 채웁니다.
💡 오늘 수업의 핵심 — "merge 는 합친 흔적을 남겨 역사를 사실 그대로 보존하고, rebase 는 가지를 옮겨 역사를 한 줄로 깔끔하게 고쳐 쓴다. rebase -i 로 지저분한 커밋을 합치고 다듬으며 cherry-pick 으로 특정 커밋만 가져올 수 있다. 단 역사를 고쳐 쓰는 일이라, 남과 공유한 브랜치는 절대 rebase 하지 않는다는 황금률을 지키고, 부득이할 땐 --force-with-lease 로 안전하게 push 한다."
🎯 학습 목표
- merge 와 rebase 가 커밋의 역사를 어떻게 다르게 만드는지 다이어그램으로 구분하고, 언제 무엇을 쓸지 판단합니다.
- 인터랙티브 rebase(
rebase -i) 로 지저분한 커밋을 합치고 다듬으며, cherry-pick 으로 특정 커밋만 골라 가져옵니다. - 역사를 고쳐 쓰는 일의 안전 수칙 — rebase 황금률 과
git push --force-with-lease— 을 이해하고, 망쳐도reflog로 되돌릴 수 있다는 안전망을 갖춥니다.
Step 1: "합치는 길은 두 갈래 — merge vs rebase"
두 브랜치를 합치는 방법은 사실 두 가지예요. 하나는 우리가 이미 잘 아는 merge, 다른 하나가 오늘의 주인공 rebase 예요. 둘 다 "갈라진 가지를 다시 하나로 모은다" 는 목적은 같은데, 합친 뒤의 역사 모양이 완전히 달라요. 이 차이를 그림으로 먼저 눈에 익히는 게 오늘의 출발점이에요.
상황을 하나 그려볼게요. main 에서 feature 브랜치를 따서 D, E 두 커밋을 작업하는 동안, 다른 동료가 main 에 C 커밋을 올려서 main 이 앞서갔어요. 이제 내 feature 작업을 main 에 모아야 합니다.
먼저 우리가 아는 merge 방식이에요. merge 는 두 갈래를 묶는 새 커밋(병합 커밋 M) 을 하나 만들어요. "여기서 두 갈래가 만났다" 는 흔적을 역사에 그대로 남기는 거예요.
merge — 합친 흔적(병합 커밋 M)을 남긴다. 갈래가 그대로 보인다.
main A───B───C───────M
\ /
feature D───E───┘
└ M = 두 갈래가 만난 병합 커밋
다음은 오늘 배울 rebase 방식이에요. rebase 는 병합 커밋을 만들지 않아요. 대신 내 feature 의 커밋 D, E 를 떼어다가 main 의 맨 끝(C 뒤)으로 옮겨 붙여요. 그러면 갈래가 사라지고 역사가 한 줄로 쭉 펴져요.
rebase — D·E를 main 끝으로 옮겨 한 줄로 편다. 깔끔하지만 역사를 고쳐 쓴다.
main A───B───C───D'───E'
└ D·E가 C 뒤로 옮겨지며 새 커밋 D'·E'로 다시 쓰인다
여기서 한 가지 중요한 걸 짚을게요. 옮겨진 커밋이 D, E 가 아니라 D', E'(프라임) 로 표시된 거 보이세요? rebase 는 커밋을 단순히 이사시키는 게 아니라, 옮겨 붙이면서 완전히 새로운 커밋으로 다시 만들어요. 내용은 같아도 커밋의 고유 이름표(해시)가 바뀌는 거예요. 바로 이게 "역사를 고쳐 쓴다" 는 말의 진짜 의미이고, 오늘 후반부 황금률의 씨앗이에요.
그럼 둘 중 뭘 써야 할까요? 정답은 "상황에 따라 다르다" 예요.
| merge | rebase | |
|---|---|---|
| 역사 모양 | 갈래가 그대로 보임 | 한 줄로 깔끔하게 펴짐 |
| 병합 커밋 | 생김(M) | 안 생김 |
| 커밋 해시 | 그대로 보존 | 새로 바뀜(D→D') |
| 장점 | 있었던 일을 사실 그대로 | 역사가 읽기 좋음 |
| 단점 | 갈래가 많으면 복잡 | 역사를 고쳐 써서 위험할 수 있음 |
⚠️ rebase 는 역사를 한 줄로 펴주는 매력적인 도구지만, 커밋을 새로 만든다(해시가 바뀐다) 는 점이 양날의 검이에요. 내 손 안에서 아직 남과 공유 안 한 커밋이면 마음껏 정리해도 되지만, 이미 남과 나눈 커밋을 rebase 하면 동료의 저장소와 엉켜요. 그래서 "언제 rebase 를 쓰면 안 되는지" 를 정하는 황금률을 오늘 Step 7 에서 꼭 정해둘 거예요.
💡 한 줄 정리
브랜치를 합치는 길은 두 갈래다. merge 는 병합 커밋을 남겨 갈래를 사실 그대로 보존하고, rebase 는 내 커밋을 상대 브랜치 끝으로 옮겨 붙여 역사를 한 줄로 깔끔하게 편다. 단 rebase 는 옮기면서 커밋을 새로 만들어(해시가 바뀐다) 역사를 고쳐 쓴다.
🙋 학생 질문 — "튜터님, 커밋 해시가 D 에서 D' 로 바뀐다는 게 정확히 무슨 뜻이에요?"
좋은 질문이에요. 이게 오늘 수업 전체를 이해하는 열쇠라 자세히 풀게요.
여태 봐왔듯이, 모든 커밋은 a1b2c3d 같은 고유한 이름표(해시)를 가져요. 이 해시는 "그 커밋이 어떤 변경을, 어떤 커밋 다음에 담고 있는지" 를 재료로 계산돼요. 즉 '바로 앞 커밋이 무엇인지' 도 해시 계산에 들어가요.
rebase 는 D 를 C 뒤로 옮기죠? 그러면 D 의 "앞 커밋" 이 원래의 B 에서 C 로 바뀌어요. 앞 커밋이 달라졌으니 해시도 새로 계산돼서, 내용은 똑같아도 전혀 다른 커밋 D' 가 새로 태어나는 거예요. 원래의 D 는 더 이상 역사에 없고요.
그래서 "옮긴다" 보다 "옮겨 붙이면서 다시 쓴다" 가 더 정확한 표현이에요. 이 사실 하나만 기억하면, 뒤에 나올 황금률("공유한 커밋은 rebase 금지")이 왜 그렇게 중요한지 저절로 이해돼요.
Step 2: "가지를 옮겨 한 줄로 — git rebase"
이제 직접 손으로 rebase 를 해봐요. Step 1 의 그 상황 그대로예요. 내가 feature 브랜치에서 작업하는 동안 main 이 앞서간 상태에서, 내 작업을 main 의 최신 위로 옮겨 붙일 거예요.
먼저 지금 역사가 어떻게 갈라져 있는지 봅시다. git log 에 --oneline --graph --all 을 붙이면 커밋 그래프를 터미널에서 직접 볼 수 있어요.
git switch feature
git log --oneline --graph --all
# * 3c4d5e6 (main) 동료가 올린 변경 ← main이 앞서감
# | * a1b2c3d (HEAD -> feature) 내 작업 E
# | * f6e5d4c 내 작업 D
# |/
# * 9a8b7c6 공통 조상 커밋
갈래가 보이죠? 이제 feature 브랜치에 선 채로 git rebase main 을 하면, 내 커밋 D, E 를 main 의 맨 끝으로 옮겨 붙여요.
git rebase main
# Successfully rebased and updated refs/heads/feature.
이 한 줄이 뜨면 성공이에요. 다시 그래프를 보면, 갈래가 사라지고 내 커밋이 main 뒤로 쭉 펴진 게 보여요.
rebase 전 — 갈래로 나뉨 rebase 후 — 한 줄로 펴짐
main ●───●───● (C) main ●───●───● (C)
\ \
feature ●───● (D·E) feature ●───● (D'·E')
└ 내 작업이 main 최신 뒤로 옮겨졌다
여기까지 하면 feature 는 main 보다 D', E' 두 커밋만큼 앞서 있어요. 이제 main 으로 돌아가서 합치면, 갈래가 없으니 병합 커밋도 없이 main 이 그냥 앞으로 쭉 따라가요. 이걸 A-2 에서 배운 fast-forward(빨리 감기) 병합 이라고 했죠.
git switch main
git merge feature
# Updating 3c4d5e6..e5f6a7b
# Fast-forward
결과적으로 main 의 역사가 병합 커밋 하나 없이 완벽하게 한 줄로 정리됐어요. 이게 많은 팀이 "내 기능 브랜치는 합치기 전에 rebase 로 한번 정리한다" 를 선호하는 이유예요. 나중에 git log 를 봤을 때 역사가 직선이라 읽기가 편하거든요.
🌟 GUI 로는 — GitHub Desktop 에서
Branch메뉴의Rebase current branch...를 고르면 같은 작업을 버튼으로 할 수 있어요. 다만 rebase 는 명령어로 익히는 걸 강하게 권해요. 화면마다 메뉴 위치가 다르고, 실무에선 결국 터미널에서 다루게 되거든요.
여기서 A-4 의 한 장면이 떠오르죠. push 가 거부돼 git pull 했을 때, 사실 거기에도 같은 선택이 숨어 있었어요. git pull --rebase 를 쓰면 병합 커밋을 만들지 않고 내 커밋을 원격 최신 뒤로 옮겨 붙여요. 말하자면 pull = fetch + rebase 인 셈이죠. 매번 이렇게 직선으로 받고 싶으면 git config --global pull.rebase true 로 기본값을 정해둘 수 있어요. 직선 역사를 좋아하는 팀이 즐겨 쓰는 설정이에요.
💡 한 줄 정리
feature 브랜치에서 git rebase main 을 하면 내 커밋을 main 최신 뒤로 옮겨 붙여 갈래를 없앤다. 그 뒤 main 으로 와서 git merge feature 하면 병합 커밋 없는 fast-forward 로 역사가 한 줄로 깔끔하게 정리된다.
🙋 학생 질문 — "튜터님, rebase 로 옮겼는데 왜 또 merge 를 해요? 한 번에 안 되나요?"
헷갈리기 딱 좋은 부분이에요. 둘은 역할이 달라요.
git rebase main 은 내 feature 브랜치의 커밋들을 main 최신 위로 옮겨 붙이는 작업이에요. 이걸 하면 feature 는 정리됐지만, 정작 main 브랜치의 이름표는 아직 옛 자리에 그대로 있어요. 즉 "내 가지만 깔끔해진" 상태예요.
그래서 git switch main 으로 돌아가 git merge feature 를 한 번 더 해줘야, main 이름표가 내 작업의 끝까지 따라와요. 이때는 이미 한 줄로 펴져 있어서 갈래가 없으니, 병합 커밋도 안 생기는 fast-forward 로 깔끔하게 끝나요.
정리하면 rebase 는 "줄 세우기", merge 는 "main 이름표 옮기기" 예요. 둘을 이어서 하면 병합 커밋 없는 직선 역사가 완성돼요.
Step 3: "rebase 도중 멈췄을 때 — 충돌 해결"
rebase 가 늘 매끄럽게 끝나면 좋겠지만, 가끔 도중에 충돌(conflict) 이 나서 멈춰요. A-2 에서 본 그 충돌, merge 할 때 같은 줄을 양쪽이 다르게 고치면 났던 거 기억하시죠? rebase 도 똑같이 충돌이 날 수 있어요. 다만 멈추는 방식이 조금 달라요.
merge 는 충돌을 한 번에 모아서 보여줬어요. 그런데 rebase 는 커밋을 하나씩 차례대로 다시 얹기 때문에, 충돌이 나는 그 커밋에서 멈춰요. D' 를 얹다 충돌나면 거기서 멈추고, 해결하면 다음 E' 를 얹다가 또 충돌날 수도 있어요. 즉 커밋 개수만큼 여러 번 멈출 수 있어요.
충돌이 나면 이런 안내가 떠요.
git rebase main
# CONFLICT (content): Merge conflict in notes.txt
# error: could not apply f6e5d4c... 내 작업 D
# hint: Resolve all conflicts manually, mark them as resolved with
# hint: "git add", then run "git rebase --continue".
해결 방법은 merge 충돌 때와 똑같아요. 충돌난 파일을 열면 <<<<<<< 표시가 있고, 그 안에 양쪽 버전이 들어 있어요. 원하는 내용으로 정리한 뒤 표시를 지우고, git add 로 "해결했다" 고 알린 다음, merge 때와 달리 commit 이 아니라 git rebase --continue 로 다음 커밋을 이어서 얹어요.
# 1. 충돌난 파일을 열어 원하는 내용으로 정리하고 <<<<<<< 표시를 지운다
# 2. 해결했다고 알린다
git add notes.txt
# 3. 멈춘 지점부터 rebase를 이어서 진행한다
git rebase --continue
여기서 가장 중요한 안심 장치를 알려드릴게요. rebase 도중에 "아, 이거 너무 꼬였다. 그냥 없던 일로 하고 싶다" 싶으면, 언제든 git rebase --abort 한 줄로 rebase 시작 전 상태로 완전히 되돌아가요. 충돌이 산더미여도, 뭘 잘못 건드린 것 같아도, abort 하면 깨끗하게 원위치예요. 그러니 rebase 도중 막혀도 절대 당황하지 마세요.
| 명령 | 언제 쓰나 |
|---|---|
git rebase --continue |
충돌을 해결하고 git add 한 뒤, 다음 커밋으로 이어갈 때 |
git rebase --abort |
도중에 그만두고 시작 전 상태로 완전히 되돌릴 때 (안심 장치) |
git rebase --skip |
지금 얹으려던 커밋을 건너뛸 때 (드물게 사용) |
⚠️ rebase 가 무섭게 느껴지는 가장 큰 이유가 바로 이 "도중에 멈춤" 이에요. 그런데 알고 보면 길은 셋뿐이에요 — 해결하고
--continue, 그만두고--abort, 건너뛰고--skip. 막히면 일단--abort로 원위치하고 처음부터 천천히 다시 해도 전혀 문제없어요.
💡 한 줄 정리
rebase 는 커밋을 하나씩 얹으므로 충돌 시 그 커밋에서 멈추고, 여러 번 멈출 수 있다. 충돌난 파일을 정리하고 git add 한 뒤 git rebase --continue 로 이어간다. 너무 꼬이면 git rebase --abort 한 줄로 시작 전 상태로 완전히 되돌아갈 수 있으니 안심해도 된다.
🙋 학생 질문 — "튜터님, merge 충돌이랑 rebase 충돌이랑 결국 같은 거 아니에요? 뭐가 다른지 헷갈려요."
해결하는 방법(파일 고치고 git add)은 똑같아요. 차이는 '몇 번 멈추느냐' 와 '멈춘 뒤 무슨 명령으로 잇느냐' 예요.
merge 는 양쪽을 한 번에 비교해서 충돌을 한 묶음으로 보여줘요. 그래서 충돌을 다 해결하고 git commit 한 번 하면 끝이에요.
rebase 는 내 커밋을 하나씩 다시 얹기 때문에, 커밋마다 충돌이 날 수 있어요. D' 얹다 멈추고, 해결하면 E' 얹다 또 멈추는 식이죠. 그리고 해결 후엔 git commit 이 아니라 git rebase --continue 로 이어가야 해요. 커밋은 rebase 가 알아서 다시 만들어주거든요.
쉽게 비유하면, merge 는 "한 번에 짐을 다 합치다 부딪힘", rebase 는 "짐을 한 상자씩 옮기다 상자마다 부딪힘" 이에요. 부딪히면 그 상자만 정리하고 계속 옮기면 돼요.
Step 4: "커밋을 편집하는 화면 — rebase -i 기초"
지금까지는 브랜치 전체를 옮기는 rebase 였어요. 이제 rebase 의 진짜 강력한 쓰임새를 봐요. 바로 인터랙티브 rebase(interactive rebase), 줄여서 rebase -i 예요. 이건 "내 최근 커밋 몇 개를 펼쳐놓고, 합치거나 메시지를 고치거나 지우는" 커밋 편집 도구예요.
언제 필요할까요? 혼자 작업하다 보면 커밋이 이렇게 지저분해지기 쉬워요. "로그인 폼 추가" → "오타 수정" → "ㅇㅇ" 같은 식으로요. A-1 에서 강조한 좋은 커밋 메시지와는 거리가 멀죠. 이런 걸 합쳐서 깔끔한 커밋 하나로 만들 때 rebase -i 를 써요.
먼저 화면을 띄우는 명령이에요. HEAD~3 은 "지금(HEAD)부터 뒤로 3개 커밋" 이라는 뜻이에요. 최근 3개를 편집하겠다는 거죠.
git rebase -i HEAD~3
이걸 실행하면 텍스트 편집기가 열리면서, 최근 3개 커밋이 오래된 것이 위로 쭉 나열돼요. 각 줄 맨 앞의 pick 이 "이 커밋을 어떻게 처리할지" 를 정하는 명령어예요.
pick f6e5d4c 로그인 폼 추가
pick a1b2c3d 오타 수정
pick 9z8y7x6 ㅇㅇ
# 명령:
# p, pick = 이 커밋을 그대로 둔다
# r, reword = 커밋 내용은 두고 메시지만 고친다
# e, edit = 이 커밋에서 잠깐 멈춰 내용을 고친다
# s, squash = 위 커밋에 합치되, 메시지도 합쳐서 다시 쓴다
# f, fixup = 위 커밋에 합치되, 이 커밋 메시지는 버린다
# d, drop = 이 커밋을 삭제한다
여기서 할 일은 단순해요. 각 줄의 맨 앞 단어 pick 을 원하는 명령으로 바꿔 쓰고, 저장하고 닫는 것 뿐이에요. 그러면 git 이 그 지시대로 커밋들을 다시 정리해줘요. 여섯 가지 명령을 표로 정리하면 이래요.
| 명령 | 하는 일 | 자주 쓰는 상황 |
|---|---|---|
pick |
그대로 둠 | 손댈 필요 없는 커밋 |
reword |
메시지만 고침 | 커밋 메시지를 다듬고 싶을 때 |
edit |
멈춰서 내용 수정 | 그 커밋의 코드 자체를 고칠 때 |
squash |
위와 합침 + 메시지 합침 | 두 커밋을 하나로, 메시지도 살릴 때 |
fixup |
위와 합침 + 메시지 버림 | 자잘한 수정 커밋을 흡수시킬 때 |
drop |
삭제 | 잘못 만든 커밋을 통째로 뺄 때 |
이번 Step 에서는 이 화면을 읽는 법과 여섯 명령의 뜻만 익혔어요. 실제로 지저분한 커밋을 합치는 건 다음 Step 에서 직접 해봐요.
💡 한 줄 정리
git rebase -i HEAD~N 은 최근 N 개 커밋을 편집기에 펼쳐놓고, 각 줄 앞의 명령(pick/reword/edit/squash/fixup/drop)을 바꿔 커밋을 합치거나 메시지를 고치거나 지우는 커밋 편집 도구다. 명령을 바꿔 저장하면 git 이 그대로 역사를 다시 정리한다.
🙋 학생 질문 — "튜터님, HEAD~3 의 물결표(~)랑 숫자 3 은 정확히 뭘 가리키는 거예요?"
HEAD 는 "지금 내가 보고 있는 가장 최신 커밋" 을 가리키는 이름이에요. 거기에 ~3 을 붙이면 "거기서 뒤로 3칸 떨어진 커밋" 이라는 뜻이고요.
그래서 git rebase -i HEAD~3 은 "HEAD~3 커밋을 기준점으로 삼고, 그 다음부터 지금까지의 커밋 3개를 편집하겠다" 는 의미예요. 최근 3개를 펼쳐보는 거죠.
숫자만 바꾸면 범위가 달라져요. 최근 5개를 정리하고 싶으면 HEAD~5, 2개면 HEAD~2 예요. 처음엔 "내가 지저분하게 쌓은 커밋이 몇 개더라?" 를 git log --oneline 으로 세어보고, 그 수만큼 적으면 돼요.
Step 5: "지저분한 커밋을 깔끔히 — squash·fixup·reword"
이제 Step 4 에서 익힌 화면으로 실제 정리를 해봐요. 상황은 이래요. 로그인 폼을 만들면서 커밋을 이렇게 쌓았어요.
git log --oneline
# 9z8y7x6 (HEAD -> feature) ㅇㅇ
# a1b2c3d 오타 수정
# f6e5d4c 로그인 폼 추가
"오타 수정" 과 "ㅇㅇ" 는 사실 "로그인 폼 추가" 작업의 일부인데, 따로 커밋되는 바람에 역사가 지저분해졌어요. 6개월 뒤 동료가 이 로그를 보면 "ㅇㅇ 이 대체 뭘 한 커밋이지?" 하고 당황하겠죠. 이 셋을 깔끔한 커밋 하나로 합쳐봅시다.
git rebase -i HEAD~3 으로 화면을 열고, 맨 위의 "로그인 폼 추가" 는 pick 으로 살리고, 나머지 둘은 그 위로 흡수시키게 fixup 으로 바꿔요. 자잘한 수정 커밋이라 메시지는 버려도 되니까 squash 가 아니라 fixup 이에요.
pick f6e5d4c 로그인 폼 추가
fixup a1b2c3d 오타 수정 ← 위로 흡수, 메시지 버림
fixup 9z8y7x6 ㅇㅇ ← 위로 흡수, 메시지 버림
이대로 저장하고 닫으면, git 이 세 커밋을 하나로 합쳐요. 로그를 다시 보면 깔끔하죠.
git log --oneline
# e5f6a7b (HEAD -> feature) 로그인 폼 추가
만약 합치면서 메시지도 더 좋게 다듬고 싶다면, 합친 뒤 그 커밋을 reword 로 한 번 더 손보거나, 처음부터 fixup 대신 squash 를 써서 메시지를 합치는 화면을 띄울 수 있어요. 정리하면 이런 그림이에요.
Before — 지저분한 세 커밋 After — 의미 있는 한 커밋
● ㅇㅇ ● 로그인 폼 추가
● 오타 수정 ──▶ (셋이 하나로 합쳐짐)
● 로그인 폼 추가
A-3 에서 배운 git commit --amend 기억나세요? 그건 바로 직전 커밋 하나만 고치는 도구였어요. rebase -i 는 그걸 여러 커밋으로 확장한 거라고 보면 돼요. amend 는 마지막 한 개, rebase -i 는 최근 여러 개. 이렇게 짝지어 기억하면 헷갈리지 않아요.
⚠️ 이렇게 커밋을 합치면 당연히 해시가 새로 바뀌어요(Step 1 에서 본 그 D→D' 예요). 그러니 아직 내 손 안에 있는, 남과 공유하지 않은 커밋에만 쓰세요. 이미 push 해서 동료와 나눈 커밋을 이렇게 합치면 사고가 나요. 이 경계가 바로 Step 7 의 황금률이에요.
한 걸음 더 나가볼게요. 지금까지는 이미 만들어진 커밋을 rebase -i 로 사후에 정리했어요. 그런데 애초에 커밋을 만들 때부터 깔끔하게 나누는 길도 있어요. git add -p 예요. 한 파일 안에서 여러 군데를 고쳤을 때, git add 파일이름 은 그 파일의 변경을 통째로 담지만, git add -p 는 고친 덩어리(hunk)를 하나씩 보여주며 "이건 담을까요?" 하고 물어봐요. y(담기)·n(건너뛰기)로 답하면, 같은 파일이라도 "로그인 버그 수정" 만 따로 골라 한 커밋으로 만들 수 있죠. 사후 정리가 rebase -i 라면, 사전 정리는 git add -p 예요.
💡 한 줄 정리
rebase -i 에서 흡수시킬 커밋을 fixup(메시지 버림) 또는 squash(메시지 합침)로 바꾸면 여러 커밋이 의미 있는 하나로 합쳐진다. 직전 한 커밋만 고치는 amend 의 여러 커밋 버전이라고 보면 된다. 합치면 해시가 바뀌므로 아직 공유 안 한 커밋에만 쓴다.
🙋 학생 질문 — "튜터님, squash 랑 fixup 이 둘 다 합치는 거라는데 정확히 뭐가 달라요?"
딱 하나, '합칠 때 메시지를 어떻게 하느냐' 만 달라요.
squash 는 합치면서 두 커밋의 메시지를 모두 보여주는 편집 화면을 띄워요. 그래서 "이 메시지들을 어떻게 합칠까?" 를 직접 정할 수 있어요. 두 커밋 다 의미 있는 메시지라 둘 다 살리고 싶을 때 좋아요.
fixup 은 합치면서 그 커밋의 메시지를 그냥 버려요. 편집 화면도 안 뜨고, 흡수당하는 쪽 메시지는 사라져요. "오타 수정" 이나 "ㅇㅇ" 처럼 어차피 버릴 메시지일 때 깔끔하고 빨라요.
쉽게 외우면 squash = 메시지도 합치기(물어봄), fixup = 메시지는 버리기(조용히 흡수) 예요. 자잘한 수정은 fixup, 둘 다 의미 있으면 squash 라고 기억하면 돼요.
Step 6: "그 커밋 하나만 가져올게 — cherry-pick"
지금까지는 브랜치 전체나 최근 커밋 묶음을 다뤘어요. 그런데 가끔은 다른 브랜치에 있는 딱 그 커밋 하나만 내 브랜치로 가져오고 싶을 때가 있어요. 브랜치 전체를 합치는 게 아니라, 체리를 콕 집어 따듯 커밋 하나만 골라오는 거예요. 그래서 이름도 cherry-pick(체리 피크) 이에요.
상황을 그려볼게요. 동료가 feature-A 브랜치에서 작업하다가, 거기서 급한 버그 하나를 고친 커밋을 만들었어요. 그런데 그 버그 수정은 지금 당장 main 에도 필요해요. feature-A 전체는 아직 안 끝나서 통째로 합칠 순 없고, 그 버그 수정 커밋 하나만 main 으로 가져오고 싶어요. 이때 cherry-pick 이에요.
쓰는 법은 간단해요. 가져갈 곳(main)으로 가서, 가져올 커밋의 해시를 적어주면 돼요.
git switch main
git cherry-pick a1b2c3d
# [main 7f8e9d0] 급한 버그 수정
# Date: ...
# 1 file changed, 2 insertions(+), 1 deletion(-)
그러면 a1b2c3d 커밋이 담고 있던 변경이 main 위에 새 커밋으로 복사돼서 얹혀요. 그림으로 보면 이래요.
feature-A ●───●───◆───● ◆ = 급한 버그 수정 커밋
cherry-pick 후:
main ●───●───◆' └ ◆ 하나만 main으로 복사됨(◆')
여기서도 가져온 커밋의 해시가 바뀌는 거 보이세요(다이어그램의 프라임 표시)? cherry-pick 도 커밋을 새로 복사해 만드는 거라 해시가 바뀌어요. rebase 와 같은 원리예요.
cherry-pick 도 가져오는 변경이 현재 브랜치와 겹치면 충돌이 날 수 있어요. 그때도 rebase 와 똑같이 길이 셋이에요 — 충돌을 해결하고 git cherry-pick --continue, 그만두고 git cherry-pick --abort.
⚠️ cherry-pick 은 편리하지만 남용하면 안 돼요. 같은 변경을 이 브랜치 저 브랜치에 자꾸 복사하면, 똑같은 내용이 해시만 다른 커밋으로 여기저기 흩어져서 나중에 역사가 헷갈려져요. "정말 이 커밋 하나만 급히 필요할 때" 의 도구로 아껴 쓰고, 평소엔 merge 나 rebase 로 브랜치를 통째로 합치는 게 정석이에요.
🌟 GUI 로는 — cherry-pick 은 VS Code 의 GitLens 확장에서 커밋을 우클릭해 고를 수 있어요. GitHub Desktop 은 cherry-pick 을 직접 지원하지 않아서, 이 작업은 명령어가 가장 확실해요.
💡 한 줄 정리
git cherry-pick <해시> 는 다른 브랜치의 특정 커밋 하나만 골라 현재 브랜치 위에 새 커밋으로 복사해 얹는다(해시는 바뀐다). 급한 버그 수정 한 건을 다른 브랜치에도 적용할 때처럼, 브랜치 전체가 아니라 커밋 하나만 필요할 때 아껴 쓴다.
🙋 학생 질문 — "튜터님, 그냥 merge 하면 되지 왜 cherry-pick 을 따로 써요?"
목적이 달라요. merge 는 브랜치를 통째로 합치는 것이고, cherry-pick 은 커밋 하나만 골라오는 것이에요.
예를 들어 동료의 feature-A 에 커밋이 10개 쌓여 있는데, 그중 9개는 아직 안 끝난 작업이고 딱 1개만 급한 버그 수정이라고 해봐요. 여기서 git merge feature-A 를 하면 안 끝난 9개까지 전부 main 에 딸려 들어와요. 큰일 나죠.
이럴 때 그 버그 수정 커밋 하나만 git cherry-pick 으로 가져오면, 나머지 9개는 그대로 두고 필요한 1개만 main 에 얹을 수 있어요. "전체는 아직, 이거 하나만 급히" 가 cherry-pick 의 자리예요.
다만 앞에서 말했듯 자주 쓸 도구는 아니에요. 평소 협업은 브랜치를 통째로 합치는 merge/rebase 가 기본이고, cherry-pick 은 이런 특수한 상황의 비상용이에요.
Step 7: "절대 건드리면 안 되는 선 — rebase 황금률"
오늘 내내 "rebase 는 역사를 고쳐 쓴다, 커밋 해시가 바뀐다" 를 반복해서 짚었죠? 이제 그 모든 경고가 모이는 단 하나의 황금률을 박을 시간이에요. 지난 시간 마지막에 예고했던 그 약속이에요.
이미 남과 공유한(push 한) 브랜치는, 절대 rebase 하지 않는다.
왜 이게 그렇게 중요할까요? 그림으로 보면 단숨에 이해돼요. 내가 main 의 커밋 D 를 동료와 공유한 상태에서, 나 혼자 rebase 로 D 를 D' 로 고쳐 썼다고 해봐요.
동료의 컴퓨터: A───B───C───D ← 동료는 아직 옛 D를 갖고 있다
내 컴퓨터: A───B───C───D' ← 나는 rebase로 D를 D'로 바꿈
→ 같은 작업인데 커밋이 D와 D'로 갈라졌다.
이제 둘을 합치면 역사가 꼬이고 충돌이 폭발한다.
보세요. 동료는 여전히 옛날 커밋 D 를 기준으로 작업하고 있는데, 나는 D 를 없애고 D' 를 만들어버렸어요. 이제 동료가 자기 작업을 push 하거나 내 변경을 받아오면, git 은 "D 가 맞아, D' 가 맞아?" 하고 혼란에 빠져요. 같은 내용인데 다른 커밋이 둘이 돼버렸으니까요. 결국 동료의 저장소와 내 저장소가 엉켜서, 풀어내느라 한참 고생하게 돼요. 현업 신입이 "내가 rebase 한 번 잘못했더니 팀 전체가 멈췄어요" 하고 식은땀 흘리는 사고가 바로 이거예요.
그래서 선을 이렇게 그어요.
| 이런 브랜치는 | rebase 해도 될까? |
|---|---|
| 아직 push 안 한, 나만 가진 로컬 브랜치 | ✅ 마음껏 정리 OK |
| 나 혼자 쓰는 내 PR 브랜치 (push 했어도 남이 안 건드림) | ✅ OK (단 push 는 Step 8 의 방법으로) |
main 처럼 모두가 공유하는 브랜치 |
❌ 절대 금지 |
| 동료가 같이 작업 중인 공용 브랜치 | ❌ 절대 금지 |
핵심은 "남이 그 커밋 위에서 작업하고 있느냐" 예요. 아무도 안 건드리는 내 작업이면 얼마든지 깔끔하게 정리해도 되고, 남이 딛고 선 커밋이면 절대 발밑을 바꾸면 안 돼요.
그런데 현업에서 "이 PR 브랜치 main 위로 rebase 해주세요" 라는 말은 정말 자주 들어요. 이건 황금률 위반이 아니에요. 보통 그 PR 브랜치는 나 혼자 쓰는 브랜치라, 최신 main 위로 rebase 해서 역사를 깔끔히 맞춰달라는 뜻이거든요. 겁먹을 필요 없어요. "내 브랜치는 OK, 공유 브랜치는 금지" 만 기억하면 돼요.
💡 한 줄 정리
rebase 의 황금률은 "남과 공유한(남이 그 위에서 작업하는) 브랜치는 절대 rebase 하지 않는다" 이다. rebase 는 커밋을 새로 만들어 역사를 갈라놓기 때문이다. 아직 나만 가진 로컬 브랜치나 혼자 쓰는 PR 브랜치는 마음껏 정리해도 되고, main 같은 공용 브랜치는 손대지 않는다.
🙋 학생 질문 — "튜터님, 제 PR 브랜치도 이미 push 해서 GitHub 에 올라가 있는데, 그것도 공유된 거 아닌가요? rebase 하면 안 되는 거 아니에요?"
아주 예리한 질문이에요. 그리고 이게 황금률을 진짜로 이해했는지 가르는 지점이에요.
"공유" 의 진짜 기준은 '단순히 원격에 올라갔느냐' 가 아니라 '남이 그 커밋 위에서 작업하고 있느냐' 예요. 내 PR 브랜치가 GitHub 에 올라가 있어도, 그 브랜치는 보통 나 혼자만 작업해요. 동료들은 그걸 받아서 자기 작업의 기반으로 삼지 않고, PR 페이지에서 리뷰만 하죠. 그러니 내가 rebase 로 정리해도 남의 작업이 엉킬 일이 없어요.
문제가 되는 건 main 처럼 모두가 받아다 그 위에서 새 작업을 시작하는 브랜치예요. 그건 발밑이 바뀌면 모두가 넘어져요.
그래서 정리하면 이래요. 혼자 쓰는 브랜치는 push 했어도 rebase OK, 모두가 딛고 서는 브랜치는 금지. 단, 내 PR 브랜치를 rebase 하면 이미 올라간 원격과 역사가 달라져서 보통 push 가 거부돼요. 그때 안전하게 밀어 올리는 방법을 바로 다음 Step 에서 배워요.
Step 8: "부득이할 땐 안전하게 — push --force-with-lease"
Step 7 의 끝에서 숙제가 하나 생겼죠. 내 PR 브랜치를 rebase 로 정리하면, 커밋 해시가 바뀌어서 이미 GitHub 에 올라간 원격과 역사가 달라져요. 이 상태로 평범하게 git push 를 하면 git 이 거부해요.
git push
# ! [rejected] feature -> feature (non-fast-forward)
# error: failed to push some refs to '...'
# hint: Updates were rejected because the tip of your current branch is behind
git 이 "어? 원격이랑 역사가 안 맞는데? 그냥 밀어 넣으면 위험해" 하고 막아주는 거예요. 이건 고마운 보호 장치예요. 이럴 때 "내가 일부러 역사를 고쳐 쓴 게 맞으니 밀어 넣겠다" 고 알려주는 게 강제 push 예요. 그런데 강제 push 에도 위험한 길과 안전한 길이 있어요.
위험한 길은 그냥 git push --force 예요. 이건 원격에 뭐가 있든 무조건 내 걸로 덮어써요. 만약 내가 모르는 사이 동료가 그 브랜치에 뭔가 올렸다면, 그 동료의 커밋이 소리 소문 없이 사라져요. 이게 "잘못된 force-push 로 동료 작업 날렸다" 는 그 무서운 사고예요.
안전한 길은 git push --force-with-lease 예요. 이름이 좀 어렵지만 뜻은 단순해요. "내가 마지막으로 본 원격 상태가 그대로일 때만 덮어쓴다" 는 조건을 거는 거예요. 즉 내가 모르는 새 변경이 원격에 있으면 push 를 거부하고 멈춰줘요. 동료가 뭔가 올렸다면 덮어쓰기 전에 알려주는 거죠.
git push --force-with-lease
# 원격이 내가 본 그대로면 → 안전하게 덮어쓰기 성공
# 그 사이 동료가 뭔가 올렸다면 → 거부하고 멈춤 (동료 작업 보호)
| 명령 | 동작 | 안전도 |
|---|---|---|
git push --force |
원격을 무조건 덮어씀 | ❌ 동료 커밋 날릴 수 있음 |
git push --force-with-lease |
내가 본 상태 그대로일 때만 덮어씀 | ✅ 모르는 변경 있으면 멈춤 |
🌟 한 끗 더 안전하게 —
--force-with-lease에--force-if-includes를 함께 쓰면(git push --force-with-lease --force-if-includes), 내가 원격 변경을 실제로 받아봤는지까지 확인해서 더 안전해요. 매번 치기 번거로우면git config --global push.useForceIfIncludes true를 한 번 켜두면--force-with-lease만 써도 이 보호가 자동으로 따라와요. 실무에선 이 설정을 켜두는 걸 권해요.
마지막으로 가장 든든한 안심 장치를 다시 꺼낼게요. 혹시 강제 push 든 rebase 든 뭔가 정말 잘못 만졌어도, A-3 에서 배운 git reflog 가 있어요. reflog 는 "내가 거쳐온 모든 상태" 를 기록해둔 구조대라, 망친 직전 상태의 해시를 찾아 git reset 으로 돌아갈 수 있어요. 그러니 오늘 배운 도구들, 무서워하지 말고 연습 저장소에서 마음껏 시험해보세요. 길을 잃어도 돌아올 지도가 있으니까요.
🌟 GUI 로는 — GitHub Desktop·VS Code 의 'Force push' 버튼은 대개 위험한
--force와 같게 동작해요. 그래서 GUI 로 강제 push 할 땐 더 조심하고, 가능하면 명령어로--force-with-lease를 쓰는 게 안전해요.
💡 한 줄 정리
rebase 로 정리한 브랜치를 push 하면 역사가 달라 거부되는데, 이때 git push --force 는 동료 커밋을 날릴 수 있어 위험하다. git push --force-with-lease 는 내가 본 원격 상태가 그대로일 때만 덮어써서, 모르는 변경이 있으면 멈춰 동료 작업을 지킨다. 그래도 망치면 reflog 로 되돌릴 수 있다.
🙋 학생 질문 — "튜터님, --force-with-lease 도 가끔 안전하지 않을 때가 있다고 들었는데, 정말이에요?"
맞아요, 아주 정확하게 들으셨어요. 미묘한 함정이 하나 있어요.
--force-with-lease 는 "원격 상태가 내가 마지막으로 안 그대로면 덮어쓴다" 고 했죠. 그런데 만약 내가 자동화 도구나 무심코 git fetch 를 해서 원격의 새 변경을 받아오기만 하고 보지는 않았다면, git 은 "내가 본 상태" 가 최신으로 갱신됐다고 착각해서 보호가 느슨해질 수 있어요. 동료 커밋을 받아만 놓고 덮어쓰는 사고가 드물게 가능한 거죠.
그래서 나온 게 위 팁에서 말한 --force-if-includes 예요. 이건 "원격 변경을 받아오기만 한 게 아니라 실제로 내 작업에 반영했는지" 까지 확인해서 그 틈을 막아줘요. git config --global push.useForceIfIncludes true 를 켜두면 자동으로 적용되고요.
입문 단계에서 여기까지 외울 필요는 없어요. "강제 push 는 --force 말고 --force-with-lease, 그리고 설정 하나 켜두면 더 안전" 정도만 기억하면 충분해요.
마무리
오늘 우리는 git 의 가장 강력하면서도 조심스러운 도구, 역사를 다시 쓰는 법을 익혔어요. merge 와 rebase 가 역사를 어떻게 다르게 만드는지 그림으로 구분했고, rebase 로 가지를 옮기고 충돌을 빠져나오는 법을 익혔죠. rebase -i 로 지저분한 커밋을 깔끔한 하나로 합치고, cherry-pick 으로 필요한 커밋만 골라오는 법도 배웠어요. 그리고 무엇보다, 이 모든 힘을 안전하게 쓰는 황금률과 --force-with-lease, 그리고 망쳐도 돌아올 reflog 라는 지도까지 챙겼어요. 처음엔 무섭던 단어들이 이제 조금은 익숙해졌길 바라요.
오늘 배운 핵심 세 가지
- 💡 하나 — merge 는 사실 그대로, rebase 는 깔끔하게. merge 는 병합 커밋을 남겨 갈래를 보존하고, rebase 는 커밋을 옮겨 붙여 역사를 한 줄로 편다. 단 rebase 는 커밋을 새로 만들어(해시가 바뀐다) 역사를 고쳐 쓴다.
- 💡 둘 — 커밋은 합치고 고르고 다듬을 수 있다.
rebase -i의squash·fixup·reword로 지저분한 커밋을 의미 있는 하나로 정리하고,cherry-pick으로 특정 커밋만 골라 가져온다. - 💡 셋 — 역사를 고쳐 쓸 땐 황금률과 안전한 push. 남과 공유한 브랜치는 절대 rebase 하지 않는다. 부득이 강제 push 가 필요하면
--force가 아니라--force-with-lease로, 그래도 망치면reflog로 되돌린다.
다음 시간 예고
오늘로 협업 git 의 고급 도구는 거의 다 챙겼어요. 다음 시간(A-8)은 이 과목의 마지막 모듈이자, 지금까지 배운 모든 걸 실전에서 묶는 시간이에요.
오늘 rebase 도중에 커밋이 하나씩 얹히는 걸 봤죠? 그 과정에서 가끔 HEAD 가 어느 브랜치에도 붙어 있지 않은 이상한 상태(detached HEAD) 에 빠질 수 있어요. 다음 시간엔 그게 뭔지, 어떻게 빠져나오는지를 다뤄요. 그리고 오늘 경고한 "잘못된 force-push 로 동료 커밋이 사라진" 사고를 reflog 로 실제로 복구하는 법, 흔한 협업 사고에서 빠져나오는 트러블슈팅도 함께 봐요.
마지막으로 살짝 설레는 예고 하나. PR 을 올리면 자동으로 코드를 검사해서 초록색 체크 표시가 뜨는 걸 본 적 있을 거예요. 그게 어떻게 동작하는지, GitHub Actions 로 아주 간단한 자동 검사 하나를 직접 만들어 체감해봐요. 본격적인 자동화는 인프라 과목의 몫이지만, "아, 이런 거구나" 하는 첫 감은 다음 시간에 잡아요. 이 과목의 피날레, 기대해주세요.
과제
오늘 배운 건 눈으로만 보면 절대 안 익어요. 특히 rebase 는 직접 망쳐보고 --abort 로 되돌려봐야 무섭지 않다는 걸 몸으로 알게 돼요. 꼭 여러분 연습 저장소에서 직접 쳐보세요. 안심하고요 — 연습 저장소는 망쳐도 아무 일도 안 일어나요.
[기초] 지저분한 커밋 세 개를 하나로 합치기
연습 저장소에서 일부러 지저분한 커밋을 쌓은 뒤 rebase -i 로 정리해보세요.
작은 파일 하나를 만들고, "메모 작성" → "오타 고침" → "ㅇㅇ" 처럼 세 번에 나눠 커밋하세요(메시지를 일부러 대충 지어보세요). 그다음 git rebase -i HEAD~3 으로 화면을 열어, 맨 위는 pick, 나머지 둘은 fixup 으로 바꿔 저장하세요. 마지막으로 git log --oneline 으로 커밋이 의미 있는 하나로 합쳐졌는지 확인하고, "왜 squash 가 아니라 fixup 을 골랐는지" 를 한 문장으로 적어보세요.
[응용] feature 브랜치를 main 최신 위로 rebase 하기
main 이 앞서간 상황을 직접 만들어, 내 기능 브랜치를 그 위로 rebase 해보세요.
main 에서 feature 브랜치를 따 커밋을 하나 만든 뒤, main 으로 돌아가 다른 파일에 커밋을 하나 더 올려 main 을 앞서가게 하세요(갈래가 생겨요). 그다음 git switch feature → git rebase main 으로 내 작업을 main 최신 뒤로 옮기고, git log --oneline --graph --all 로 갈래가 한 줄로 펴졌는지 확인하세요. 마지막으로 "rebase 전과 후의 그래프 모양이 어떻게 달라졌는지" 를 한두 문장으로 적어보세요.
상상해보세요 — 입사 후 내 PR 브랜치를 며칠 두는 사이 동료들이 main 에 여러 변경을 올린 상황이요. 그때 "PR 을 최신 main 위로 rebase 해서 올려주세요" 라는 요청을 받게 될 거예요. 오늘 이 연습이 그대로 그 상황이에요.
[심화] 황금률을 말로 설명하고, 안전한 강제 push 짚기
명령을 외우는 걸 넘어, 왜 그렇게 하는지를 스스로 설명해보는 과제예요.
먼저 응용 과제에서 rebase 한 feature 브랜치가 이미 GitHub 에 올라가 있었다고 가정하고, 왜 평범한 git push 가 거부되는지, 그때 git push --force 대신 git push --force-with-lease 를 써야 하는 이유가 무엇인지 두세 문장으로 적어보세요. 그다음 "그렇다면 main 브랜치는 왜 절대 rebase 하면 안 되는지" 를 황금률의 관점에서 설명해보세요. (여유가 되면 연습 저장소의 내 브랜치에서 --force-with-lease 를 실제로 한번 써봐도 좋아요. 어차피 나 혼자 쓰는 브랜치라 안전해요.)
생각해볼 주제
1. 우리 팀의 커밋 역사, 직선이 좋을까 갈래가 좋을까
어떤 팀은 "역사는 무조건 한 줄로 깔끔해야 읽기 좋다" 며 합치기 전에 늘 rebase 를 하고, 어떤 팀은 "있었던 일은 그대로 보여야 한다" 며 merge 의 병합 커밋을 일부러 남겨요. 둘 다 일리가 있어요. 여러분이 팀의 리드라면 어느 쪽을 택하겠어요? 6개월 뒤 버그를 추적하려고 git log 를 뒤지는 상황을 떠올리면, 직선 역사의 가독성과 갈래 역사의 사실성 중 무엇이 더 도움이 될지 견줘보세요.
2. 황금률을 어겼을 때, 실제로 동료에게 무슨 일이 생길까
내가 main 을 rebase 해서 push 해버렸다고 해봐요. 다음 날 출근한 동료가 평소처럼 git pull 을 하면 무슨 일이 벌어질까요? 동료의 화면엔 어떤 메시지가 뜨고, 동료의 로컬 작업은 어떻게 될까요? "커밋 해시가 바뀐다" 는 오늘의 핵심을, 동료의 입장에서 한 번 따라가 보세요. 왜 이 규칙이 '권장' 이 아니라 '황금률' 이라는 강한 이름을 가졌는지 몸으로 와닿을 거예요.
3. 커밋을 squash 로 합치면, 우리가 잃는 것은 없을까
rebase -i 로 커밋 다섯 개를 깔끔한 하나로 합치면 역사가 읽기 좋아져요. 그런데 합치는 순간, 그 다섯 단계로 나뉘어 있던 "작업의 과정" 은 사라져요. 어떤 순서로 고민하며 만들어갔는지가 한 커밋 안에 뭉개지는 거죠. 깔끔한 역사를 위해 작업 과정의 기록을 버리는 게 늘 이득일까요? "언제는 합치는 게 낫고, 언제는 단계를 남겨두는 게 나은지" 를 여러분 나름의 기준으로 정리해보세요.
✅ 예시 답안정답 보기
이 문서는 A-7 「rebase와 히스토리 재작성」의 과제와 생각해볼 주제에 대한 예시답안입니다. 정답을 외우는 용도가 아니라, 직접 손으로 해본 결과를 어떻게 정리하고 설명하는지 흐름을 참고하는 용도로 보세요. 특히 rebase 는 직접 망쳐보고 --abort 로 되돌려봐야 무섭지 않다는 걸 알게 됩니다. 답안을 보기 전에 꼭 먼저 여러분 연습 저장소에서 실제로 쳐보세요. 망쳐도 reflog 라는 안전망이 있으니 안심하고요.
과제 예시답안
🎯 [과제 1 예시답안] 지저분한 커밋 세 개를 하나로 합치기
채점 포인트
| 항목 | 배점 | 기준 |
|---|---|---|
| 커밋 쌓기 | 20% | 작은 파일로 커밋 3개를 나눠 만들었는가 |
| rebase -i 실행 | 30% | git rebase -i HEAD~3 으로 편집 화면을 열었는가 |
| fixup 적용 | 30% | 맨 위 pick + 나머지 fixup 으로 하나로 합쳤는가 |
| 선택 설명 | 20% | 왜 squash 가 아니라 fixup 인지 적었는가 |
풀이 예시
먼저 일부러 지저분한 커밋 세 개를 쌓아요.
echo "오늘의 메모" >> notes.txt
git add notes.txt
git commit -m "메모 작성"
echo "오타 고침" >> notes.txt
git add notes.txt
git commit -m "오타 고침"
echo "한 줄 더" >> notes.txt
git add notes.txt
git commit -m "ㅇㅇ"
이제 git rebase -i HEAD~3 으로 편집 화면을 열고, 맨 위는 pick 으로 살리고 나머지 둘은 fixup 으로 바꿔요.
pick 1a2b3c4 메모 작성
fixup 5d6e7f8 오타 고침
fixup 9a8b7c6 ㅇㅇ
저장하고 닫은 뒤 로그를 보면 세 커밋이 하나로 합쳐져 있어요.
git log --oneline
# e5f6a7b (HEAD -> main) 메모 작성
선택 설명에 대해: fixup 을 고른 건, "오타 고침" 과 "ㅇㅇ" 의 메시지가 어차피 버려도 되는 자잘한 것이었기 때문이에요. squash 를 쓰면 세 메시지를 합치는 편집 화면이 떠서 한 번 더 손봐야 하는데, 여기선 "메모 작성" 메시지 하나만 남기면 충분하니 메시지를 조용히 버리는 fixup 이 더 깔끔하고 빨라요.
💡 튜터의 한마디 — 이 과제의 진짜 목적은 "내가 만든 지저분한 역사를 내가 직접 깔끔하게 고칠 수 있다" 는 자신감을 얻는 거예요. 처음엔 편집기 화면이 낯설지만, 한 번 해보면 "아, 그냥 맨 앞 단어만 바꾸면 되는구나" 하고 금방 익숙해져요. 입사하면 PR 올리기 전에 이렇게 커밋을 한번 정리하고 올리는 동료를 자주 보게 될 거예요.
🎯 [과제 2 예시답안] feature 브랜치를 main 최신 위로 rebase 하기
채점 포인트
| 항목 | 배점 | 기준 |
|---|---|---|
| 갈래 만들기 | 30% | feature 와 main 양쪽에 커밋을 올려 갈래를 만들었는가 |
| rebase 실행 | 35% | git switch feature → git rebase main 을 했는가 |
| 그래프 확인 | 20% | --graph 로 한 줄로 펴진 것을 확인했는가 |
| 변화 설명 | 15% | rebase 전후 그래프 모양 차이를 적었는가 |
풀이 예시
먼저 갈래를 만들어요. feature 에 커밋 하나, main 에 커밋 하나를 올리면 역사가 두 갈래로 갈라져요.
git switch -c feature
echo "내 작업" >> feature.txt
git add feature.txt
git commit -m "기능 작업"
git switch main
echo "동료 작업" >> main.txt
git add main.txt
git commit -m "main 변경"
이제 feature 로 돌아가 main 위로 rebase 해요.
git switch feature
git rebase main
# Successfully rebased and updated refs/heads/feature.
git log --oneline --graph --all
# * 7f8e9d0 (HEAD -> feature) 기능 작업 ← main 최신 뒤로 옮겨짐
# * 3c4d5e6 (main) main 변경
# * 9a8b7c6 공통 조상
변화 설명에 대해: rebase 전에는 공통 조상에서 main 과 feature 가 Y 자로 갈라져 있었어요. rebase 후에는 그 갈래가 사라지고, 내 "기능 작업" 커밋이 main 의 "main 변경" 바로 뒤로 옮겨져 한 줄로 쭉 펴졌어요. 갈래(Y 모양)가 직선(I 모양)이 된 거예요.
💡 튜터의 한마디 — 이게 실무에서 가장 자주 만나는 rebase 예요. "PR 을 최신 main 위로 rebase 해주세요" 라는 요청이 정확히 이 작업이거든요. 내 작업을 main 의 가장 최신 위로 옮겨, 합칠 때 깔끔하게 들어가도록 미리 줄을 세워두는 거죠. 오늘 이 한 바퀴를 익혀두면, 입사 첫 PR 에서 그 요청을 받아도 당황하지 않아요.
🎯 [과제 3 예시답안] 황금률을 말로 설명하고, 안전한 강제 push 짚기
채점 포인트
| 항목 | 배점 | 기준 |
|---|---|---|
| push 거부 이유 | 25% | rebase 후 평범한 push 가 왜 거부되는지 설명했는가 |
| force-with-lease | 35% | --force 대신 --force-with-lease 를 쓰는 이유를 적었는가 |
| 황금률 설명 | 40% | main 을 rebase 하면 안 되는 이유를 황금률로 설명했는가 |
풀이 예시
push 가 거부되는 이유: rebase 는 커밋을 새로 만들어서 해시를 바꿔요. 그래서 내 로컬 feature 의 역사가 이미 GitHub 에 올라간 원격 feature 와 달라져요. git 은 "원격이랑 역사가 안 맞는데 그냥 밀면 위험하다" 며 평범한 push 를 거부해요(non-fast-forward).
--force 대신 --force-with-lease 를 쓰는 이유: git push --force 는 원격에 뭐가 있든 무조건 덮어써서, 그 사이 누가 올린 커밋을 소리 없이 날려버릴 수 있어요. --force-with-lease 는 "내가 마지막으로 본 원격 상태가 그대로일 때만 덮어쓰기" 라는 조건을 걸어요. 그래서 내가 모르는 새 변경이 원격에 있으면 멈춰주고, 남의 작업이 사라지는 걸 막아줘요.
git push
# ! [rejected] (non-fast-forward) ← 역사가 달라 거부됨
git push --force-with-lease
# 원격이 내가 본 그대로면 → 안전하게 성공
main 을 rebase 하면 안 되는 이유(황금률): main 은 모두가 받아다 그 위에서 작업하는 공유 브랜치예요. 내가 main 을 rebase 하면 커밋 해시가 바뀌는데, 동료들은 여전히 옛 해시의 커밋을 기준으로 작업 중이라 역사가 둘로 갈라져요. 결국 동료가 pull 하거나 push 할 때 저장소가 엉켜 큰 혼란이 생겨요. 그래서 "남이 그 위에서 작업하는 브랜치는 절대 rebase 하지 않는다" 가 황금률이에요. 내 PR 브랜치(나 혼자 씀)는 OK 지만, main 같은 공용 브랜치는 금지예요.
💡 튜터의 한마디 — 명령을 외우는 것보다 이렇게 "왜" 를 말로 설명할 수 있는 게 훨씬 중요해요. 실무에서 "왜 --force 말고 --force-with-lease 를 써요?" 같은 질문을 받으면, 오늘 적은 이 문장 그대로 답하면 돼요. 그리고 혹시 실수로 강제 push 를 잘못했어도 reflog 로 되돌릴 수 있다는 안전망까지 알고 있으면, git 앞에서 더 이상 겁먹지 않게 돼요.
생각해볼 주제 예시답안
🤔 [생각해볼 주제 1] 우리 팀의 커밋 역사, 직선이 좋을까 갈래가 좋을까
문제 상황 요약
어떤 팀은 합치기 전에 늘 rebase 로 역사를 한 줄로 펴고, 어떤 팀은 merge 의 병합 커밋을 일부러 남겨 갈래를 보존해요. 직선 역사의 가독성과 갈래 역사의 사실성 중 무엇을 우선할지 정하는 문제예요.
튜터의 가이드 및 해설
정답은 없고, 팀이 무엇을 더 중요하게 여기느냐의 선택이에요. 두 입장을 견줘볼게요.
직선(rebase 선호) 쪽: git log 가 깔끔한 한 줄이라 읽기 좋아요. "이 기능이 어떤 순서로 들어왔나" 를 따라가기 쉽고, 병합 커밋이 없어 역사가 단순해요. 다만 실제로 일이 동시에 진행됐다는 사실(갈래)은 사라져요.
갈래(merge 선호) 쪽: 병합 커밋이 "여기서 이 기능 브랜치가 합쳐졌다" 는 사실을 그대로 남겨요. 언제 어떤 작업이 병렬로 진행됐는지가 역사에 보존돼서, 사고를 추적할 때 "이 변경은 저 PR 에서 통째로 들어왔구나" 를 알기 좋아요. 다만 갈래가 많으면 그래프가 복잡해져요.
실무에서 흔한 절충안은 "기능 브랜치 안에서는 rebase 로 커밋을 깔끔히 정리하되, main 에 합칠 때는 merge(또는 squash merge)로 한 PR 을 한 단위로 남기는" 방식이에요. 내 작업은 직선으로 다듬고, 합쳐지는 경계는 흔적으로 남기는 거죠.
💡 핵심을 한마디로
"역사를 한 줄로 펴는 rebase 는 가독성을, 갈래를 남기는 merge 는 사실성을 택한 거예요. 둘 중 하나가 정답이 아니라, '6개월 뒤 버그를 추적할 때 어느 역사가 더 도움이 되는가' 로 팀이 고르는 거예요. 많은 팀이 브랜치 안은 rebase 로 정리하고 main 병합은 merge 로 남기는 절충을 써요."
🤔 [생각해볼 주제 2] 황금률을 어겼을 때, 실제로 동료에게 무슨 일이 생길까
문제 상황 요약
내가 공유된 main 을 rebase 해서 push 해버렸어요. 다음 날 동료가 평소처럼 git pull 을 하면 무슨 일이 벌어지는지를, 동료의 입장에서 따라가 보는 문제예요.
튜터의 가이드 및 해설
핵심은 오늘 내내 강조한 "rebase 는 커밋 해시를 바꾼다" 예요. 이걸 동료의 시점에서 따라가 볼게요.
동료의 컴퓨터엔 아직 옛 해시의 커밋들(예: D, E)이 그대로 있어요. 그런데 내가 main 을 rebase 하면서 그 커밋들을 D', E' 로 새로 만들어 원격에 덮어썼어요. 이제 원격의 main 과 동료의 로컬 main 은 같은 작업인데 해시가 다른 상태가 됐어요.
동료가 git pull 을 하면, git 은 두 역사를 합치려다 혼란에 빠져요. 보통 "역사가 갈라졌다" 며 수많은 충돌을 쏟아내거나, 옛 D·E 와 새 D'·E' 가 둘 다 역사에 남아 같은 변경이 중복되기도 해요. 동료는 영문도 모른 채 충돌 지옥에 빠지고, "왜 갑자기 다 깨졌지?" 하며 시간을 날려요. 팀원이 많을수록 피해가 커지고요.
그래서 이 규칙은 단순한 '권장' 이 아니라 '황금률' 이라는 강한 이름을 가졌어요. 어기면 나 하나가 아니라 팀 전체가 멈추기 때문이에요. 반대로, 나 혼자 쓰는 브랜치는 아무도 그 위에서 작업하지 않으니 rebase 해도 이런 일이 안 생겨요. 결국 기준은 늘 똑같아요 — "남이 그 커밋 위에 서 있느냐."
💡 핵심을 한마디로
"공유 브랜치를 rebase 하면 커밋 해시가 바뀌어서, 옛 커밋을 기준으로 일하던 동료의 역사와 갈라져요. 동료는 pull 하는 순간 충돌 지옥에 빠지죠. 나 한 명의 실수가 팀 전체를 멈추기 때문에 '권장' 이 아니라 '황금률' 이에요. 기준은 단순해요 — 남이 그 위에서 작업하는 브랜치는 절대 건드리지 않는다."
🤔 [생각해볼 주제 3] 커밋을 squash 로 합치면, 우리가 잃는 것은 없을까
문제 상황 요약
rebase -i 로 커밋 여러 개를 깔끔한 하나로 합치면 역사가 읽기 좋아져요. 하지만 합치는 순간 그 단계들로 나뉘어 있던 "작업의 과정" 은 사라져요. 깔끔함을 위해 과정의 기록을 버리는 게 늘 이득인지 따져보는 문제예요.
튜터의 가이드 및 해설
이건 "가독성" 과 "추적 가능성" 사이의 트레이드오프예요. 양쪽을 보죠.
합치면 좋은 경우: "오타 수정", "ㅇㅇ", "다시 고침" 같은 의미 없는 중간 커밋은 합치는 게 무조건 이득이에요. 그 단계들은 나중에 누구에게도 정보가 안 되고, 오히려 로그를 어지럽혀요. 최종 결과만 의미 있다면 하나로 합치는 게 맞아요.
단계를 남기면 좋은 경우: 각 커밋이 그 자체로 의미 있는 단계라면 얘기가 달라요. 예를 들어 "테스트 추가" → "기능 구현" → "리팩터링" 처럼 단계마다 분명한 의도가 있다면, 이 순서는 나중에 "왜 이렇게 만들었나" 를 이해하는 소중한 기록이에요. 버그를 추적할 때 "어느 단계에서 문제가 들어왔나" 를 좁히는 데도 도움이 되고요(git bisect 같은 도구가 작은 커밋일수록 잘 동작해요).
그래서 기준은 "이 커밋이 6개월 뒤 누군가에게 정보가 되는가" 예요. 정보가 되면 남기고, 군더더기면 합쳐요. 실무에서는 "PR 안에서는 의미 있는 단위로 커밋을 나눠두되, 어차피 버릴 자잘한 수정만 fixup 으로 흡수" 하는 균형을 자주 써요.
💡 핵심을 한마디로
"squash 는 가독성을 얻는 대신 작업 과정의 기록을 잃어요. 그래서 '오타 수정' 같은 군더더기 커밋은 합치는 게 이득이지만, '테스트 → 구현 → 리팩터링' 처럼 단계마다 의미가 있으면 남겨두는 게 나아요. 기준은 하나예요 — 이 커밋이 나중에 누군가에게 정보가 되느냐, 아니면 그냥 소음이냐."