작업하다 보면 커밋 목록이 오타 수정, 일단 저장, 다시 오타 수정 이런 부끄러운 기록으로 가득 차요. 저도 급할 땐 생각나는 대로 커밋을 툭툭 던지거든요. 그런데 이걸 그대로 동료에게 보내면, 리뷰하는 사람은 이 어수선한 발자국을 하나하나 밟으며 무슨 일이 있었는지 짐작해야 해요. 이번 편은 그 발자국을 깔끔하게 다듬는 방법이에요.
85.1 커밋을 왜 굳이 정리하나요?
히스토리는 나 혼자 보는 메모가 아니라 팀이 함께 읽는 이야기예요. 나중에 버그를 쫓거나 "이 코드는 왜 이렇게 됐지?"를 되짚을 때, 커밋 하나가 의미 있는 한 단위로 묶여 있으면 이야기가 술술 읽혀요. 반대로 일단 저장이 스무 개 쌓여 있으면 아무 단서도 못 주죠. 특히 몇 달 뒤 문제가 생겼을 때, 잘 정리된 커밋 하나가 범인을 바로 짚어 주기도 해요. 어느 커밋에서 버그가 들어왔는지 되짚어 갈 때, 커밋이 기능 단위로 깔끔하면 범위를 훨씬 빨리 좁힐 수 있거든요.
그래서 저는 작업할 땐 편하게 막 커밋하고, 남에게 보내기 직전에 히스토리를 한 번 빗질해요. 지저분한 중간 과정을 지우고, 완성된 결과를 읽기 좋은 커밋 몇 개로 정리하는 거죠. 이 빗질에 쓰는 도구가 amend, rebase, 그리고 reset이에요. 세 개가 하는 일이 조금씩 다른데, 마지막 커밋을 고칠지, 여러 개를 묶을지, 아예 되감을지에 따라 골라 쓰면 돼요. 하나씩 볼게요.
85.2 방금 한 커밋만 고치려면요?
커밋 메시지에 오타를 냈거나, 파일 하나를 깜빡하고 빠뜨린 채 커밋했을 때예요. 이럴 때 새 커밋을 또 만들면 목록만 지저분해지죠. 마지막 커밋을 그 자리에서 고쳐 덮는 방법이 있어요.
git commit --amend -m "feat: 로그인 기능 구현"
이러면 마지막 커밋의 메시지가 새 문구로 바뀌어요. 빠뜨린 파일이 있었다면 그 파일을 add한 다음 amend하면, 그 파일까지 마지막 커밋 안에 슬쩍 끼워 넣어져요. 딱 하나만 기억하면 돼요. amend는 커밋을 새로 다시 만드는 것이라, 이미 남에게 보낸 커밋에 쓰면 뒤에서 문제가 생겨요. 아직 나만 가진 마지막 커밋에만 쓰세요. 이 규칙은 뒤에 나올 rebase에도 똑같이 걸려요.
85.3 여러 커밋을 하나로 합치려면요?
일단 저장 커밋 세 개를 기능 완성 하나로 합치고 싶을 때, 대화형 리베이스(interactive rebase)를 써요. 최근 세 개를 손보겠다고 이렇게 부르면요.
git rebase -i HEAD~3
편집 화면이 열리고 대상 커밋들이 위에서부터 죽 나와요. 각 줄 맨 앞은 처음엔 다 pick(그대로 두기)인데, 합치고 싶은 줄의 pick을 squash(위 커밋에 합치기)로 바꿔요. 맨 위 하나만 pick으로 두고 나머지를 squash로 바꾼 뒤 저장하면, 이어서 합친 커밋의 메시지를 새로 쓰라고 물어봐요. 다 마치면 이렇게 뜨죠.
Successfully rebased and updated refs/heads/main.
이제 목록을 보면 세 줄이 깔끔한 한 줄로 합쳐져 있어요. 저는 기능 하나를 끝내면 이 과정으로 어수선한 wip 커밋들을 의미 있는 커밋으로 묶은 다음 리뷰에 올려요. squash 말고 reword(메시지만 고치기)나 drop(그 커밋 통째로 빼기) 같은 명령도 같은 화면에서 쓸 수 있어요.
85.4 rebase가 정확히 뭘 하는 거예요?
이름이 어렵게 들리는데 그림은 단순해요. rebase는 내 커밋들을 잠깐 옆에 빼 뒀다가, 지정한 자리에 다시 하나씩 얹는 일이에요. 그 과정에서 순서를 바꾸거나, 합치거나, 빼는 손질을 하는 거죠. 그래서 결과물이 가지가 얽히지 않은 곧은 한 줄로 정리돼요. 합침 표시가 여기저기 얽힌 지저분한 그래프보다 훨씬 읽기 좋아요. 그래서 저는 리뷰에 올리기 전, 그동안 main에 쌓인 남의 변경 위로 내 커밋을 다시 얹어 최신 상태에서 곧게 정리하는 데도 rebase를 자주 써요. 다만 얹는 도중 충돌이 나면 그 자리에서 하나씩 풀어 줘야 하니, 커밋이 잘게 나뉘어 있을수록 풀기가 수월해요.
여기서 반드시 알아야 할 게 있어요. 다시 얹는다는 건 커밋을 새로 만든다는 뜻이라, 겉보기엔 같아도 속의 정체(커밋 아이디)가 통째로 바뀌어요. 그래서 rebase는 아직 나만 들고 있는 커밋에 쓰면 최고의 청소 도구지만, 이미 팀과 나눈 커밋에 쓰면 남들이 가진 것과 어긋나 큰 혼란을 불러요. 이 선을 넘느냐 마느냐가 히스토리 정리의 전부라고 해도 돼요.
85.5 reset은 rebase랑 뭐가 달라요?
reset은 지금 위치를 과거 커밋으로 되감는 도구예요. 세 종류를 알아 두면 든든해요. --soft는 커밋만 풀고 작업 내용은 그대로 담아 둬요. 방금 커밋을 취소하고 다시 묶고 싶을 때 딱이죠.
$ git reset --soft HEAD~1
$ git status
M design.psd
보다시피 커밋은 사라졌지만 고친 내용은 M 표시로 살아 있어요. --mixed는 커밋과 담아 둔 것까지 풀되 파일 내용은 남기고, --hard는 파일 내용까지 통째로 되돌려 버려요. hard는 안 커밋한 작업이 흔적 없이 날아가니 저는 늘 숨을 한 번 고르고 써요. 실수로 hard를 쳐서 반나절 작업을 날린 뒤로 생긴 버릇이에요. 되감을 때 얼마나 지울지 이 셋으로 조절한다고 생각하면 편해요. 정리하면 soft는 커밋만, mixed는 담아 둔 것까지, hard는 파일 내용까지 되돌린다고 외워 두면 헷갈리지 않아요. 헷갈릴 땐 언제나 덜 지우는 soft부터 시작하는 게 안전하고요.
85.6 정리할 때 절대 규칙은요?
딱 하나만 뼈에 새기면 돼요. 남과 나눈 히스토리는 다시 쓰지 않는다. amend든 rebase든 reset이든, 이미 팀에 보낸 커밋을 바꾸면 동료의 저장소와 어긋나 서로 덮어쓰는 사고가 나요. 그래서 이 도구들은 내 손안에만 있는 아직 안 보낸 커밋에 쓰는 게 원칙이에요.
정말 보낸 것까지 정리해야 하는 예외라면, 함께 쓰는 사람들에게 미리 알리고, 남의 최신 작업을 실수로 지우지 않는 안전한 밀어넣기 방식으로만 조심스럽게 처리해요. 정리의 목적은 멋 부리기가 아니라, 나중에 이 코드를 읽을 사람이 이야기를 편하게 읽게 돕는 거예요.
85.7 실무에서는 어떤 흐름으로 써요?
제 하루를 예로 들어 볼게요. 기능 하나를 잡으면 작업 브랜치를 따고, 진행하는 동안엔 일단 저장 같은 커밋을 마음 편히 툭툭 던져요. 이때는 히스토리가 지저분해도 신경 안 써요. 흐름이 끊기지 않는 게 더 중요하거든요. 중간에 파일 하나를 빠뜨린 걸 알면 그 자리에서 amend로 마지막 커밋에 끼워 넣고요.
기능이 다 되면 리뷰에 올리기 전에 rebase -i로 어수선한 커밋들을 의미 있는 몇 개로 squash해 묶어요. 커밋을 통째로 취소하고 다시 담고 싶으면 reset --soft로 되감고요. 이 세 도구를 내 손안의 커밋에만 쓴다는 선만 지키면 사고는 안 나요. 처음엔 낯설어도 몇 번 해 보면, 어수선하게 커밋하다가도 보내기 전에 손이 저절로 히스토리를 다듬게 돼요. 그렇게 남긴 깔끔한 이야기 하나가, 반년 뒤의 나와 동료를 여러 번 구해 줄 거예요.