이번에 맥북을 사고나니 너무 오래 붙들고 있어서 목 통증이 오는 거 같다.
그래서 이번에 거치대를 구매했다.
키보드나 마우스는 원래 쓰던 키보드가 블루투스를 지원해서 그대로 쓰는 데,
문제는 마우스다.
무선이긴 한데, USB가 A타입이다 ...
그래서 거치대에 아싸리 포트까지 사서 모니터 쓰던 모니터 연결까지 할 예정이다.
개발 혹은 편집 쪽에서 맥이 정말 1황이라는 말이 많아서
제대로 환경을 갖추기 시작한건데,
어째 배보다 배꼽이 더 큰 거 같다...
그래도 벌써 뭔가 적응이 됐는지
윈도우를 쓸 때도 한영키 바꾸려고 캡스락부터 누르고 본다.
이 또한 개발자로 한 걸음이 아니겠는가 !
그럼 오늘 포스팅도 Leshgo.
Git이 무서워지는 순간이 무엇이 있을까.
정도로 꼽을 수 있을 것 같다.
이 순간들의 공통점이 있는데,
바로 히스토리를 건드리는 순간이다.
Git은 파일 관리 도구처럼 보이지만
실제로는 커밋 히스토리 관리 도구다.
그래서 이번에는 "히스토리를 건드리는 행위들"에 대해 다룬다.
stash의 형태를 알아보기 전에 먼저 다뤄야 할 개념이 하나 있다.
바로 Git에서의 파일 상태인데 Git의 파일 형태는 크게 3가지로 나뉜다.
git add 된 적 있는 파일git status에 빨간색으로 뜨는 파일들이 파일 상태 개념을 이해해야 stash 동작을 정확히 이해할 수 있다.
실무 상황을 하나 생각해보자.
로그인 기능을 작업 중인데, 아직 완성은 안됐다.
근데 dev 브랜치로 급하게 이동해야 한다.
이때 선택지는 두 가지다:
여기서 "의미 없는 커밋 남기기 싫을 때" 사용 하는 게 stash다.
stash는 커밋이 아니다.
히스토리에 기록되지 않는 임시 저장이다.
commit과 stash 이 둘을 비교해보자.
그래서 Stash는
"미완성 작업을 잠깐 숨겨두는 기능"이다.
git stash # 현재 작업 임시 저장
git stash list # stash 목록 확인
git stash pop # 가장 최근 stash 적용 + 삭제
git stash apply stash@{1} # 특정 stash 적용 (삭제 안 함)
이 정도면 충분하다.
너무 많은 옵션은 오히려 해가 된다.
-u의 중요성기본 git stash는 수정된 파일은 저장하고
새로 만든 파일(untracked)은 남겨둔다.
그래서 실무에서는
새 파일까지 포함해서 완전히 비워두기 위해
-u 옵션을 자주 쓴다.
git stash -u
Stash는 히스토리를 깨끗하게 유지하기 위한 도구이지,
기록을 대신하는 도구는 아니다.
의미 있는 작업 단위라면 commit으로 남기는 것이 원칙이다.
실무에서 자주 나오는 상황:
이 외에도 방금 한 커밋만 살짝 고치고 싶어질 때가 있다.
이때 등장하는 게 이제부터 다룰 --amend다.
많이 오해하는 부분부터 정리하자면,
amend는 커밋을 수정하는 게 아니라,
새로운 커밋을 다시 만드는 것이다.
이것이 amend의 가장 중요한 특징이다.
Git 커밋은 단순 메시지가 아니다.
커밋 ID에는:
이 정보가 모두 포함된다.
이 중 하나라도 바뀌면
완전히 다른 ID가 만들어진다.
그래서:
git commit --amend -m "수정된 메시지"
하면
"같은 커밋을 고친 것처럼 보이지만"
실제로는 새로운 커밋인 걸 알 수 있다.
git commit --amend -m "수정된 메시지"
git add forgotten.js
git commit --amend
git add fix.js
git commit --amend --no-edit
여기까지면 충분하다.
stash에서도 후에 서술했지만,
옵션은 더 깊게 들어가면 오히려 독이 된다.
이미 push한 상태라면?
원격에는 "옛날 커밋 ID"가 있다.
그런데 amend 하면
로컬에는 "새 커밋 ID"가 생긴다.
그럼 히스토리가 달라지면서 push가 거부된다.
원격 저장소는 기존 커밋 ID를 기준으로 기록을 유지하고 있기 때문이다.
이때 필요한 게:
git push --force
즉, amend는 force와 세트가 될 수 있다는 말이다.
실무에서 이런 상황이 생긴다.
이때 등장하는 명령어가 reset이고,
그 중에서도 가장 강력한 방식이 --hard다.
git reset --hard는 단순히 "되돌리기"가 아니다.
HEAD를 특정 커밋으로 이동시키고,
작업 디렉토리까지 그 커밋 상태로 강제로 맞춘다.
즉, 현재 작업 중이던 변경사항까지 포함해서
지정한 시점으로 돌아간다.
amend와 reset은 둘 다 히스토리를 건드린다.
그래서 자주 헷갈리는데, 방향이 다르다.
--hard를 쓰면 작업 디렉토리까지 강제로 맞춘다git reset --hard HEAD~1
HEAD~1은 "현재(HEAD)에서 한 단계 이전 커밋"을 의미한다.
git reset --hard abc1234
커밋 ID(해시)를 지정하면 그 시점으로 돌아간다.
git reset --hard HEAD
이건 "커밋을 이동"시키는 게 아니라
작업 디렉토리를 현재 커밋 상태로 덮어씌우는 용도로 자주 쓴다.
--hard는 강력하다.
변경사항이 즉시 사라진 것처럼 보인다.
그래서 실수하면 "끝난 것 같다"는 착각이 들 수 있다.
하지만 Git에는 마지막 안전장치가 있다.
reflog는 내가 HEAD를 어디로 이동시켰는지 기록으로 남긴다.
그래서 reset을 실수했을 때는:
git reflog
로 이전 상태를 찾고,
git reset --hard HEAD@{2}
처럼 특정 시점으로 다시 돌아올 수 있다.
실무에서 이런 상황이 생긴다.
이때 쓰는 게 rebase다.
rebase는 병합이 아니라,
기존 커밋들을 새로운 기준 위에 다시 쌓는 것이다.
다시 말해 기존 커밋을 복사해서
다른 기준점(base) 위에 다시 만든다.
그래서 커밋 ID가 전부 바뀐다.
rebase를 이해하려면 merge와의 차이를 정리해야 한다.
둘 다 브랜치를 합치는 방법이지만,
히스토리 모양이 완전히 다르다.
사실 merge를 소개할 때도 rebase가 단골 개념인 거 같다.
git merge dev
→ 기록을 그대로 유지하는 방식
git rebase dev
→ 히스토리를 재작성하는 방식
merge는 "합친다"이고,
rebase는 "다시 쌓는다"다.
이렇게 구분하는 경우가 많다.
git checkout feature/login
git rebase dev
내 feature 브랜치의 커밋들을
dev 최신 커밋 위에 다시 쌓으라는 의미를 담는다.
rebase 중 충돌이 나면
git add .
git rebase --continue
중단하고 싶으면:
git rebase --abort
보통 이런 상황이다.
이럴 때 사용하는 게:
git rebase -i HEAD~3
→ 최근 3개의 커밋을 편집 모드로 연다.
작은 수정 커밋들을 하나로 합친다.
pick a1b2c3 로그인 기능 구현
squash d4e5f6 콘솔 로그 제거
squash g7h8i9 오타 수정
→ 깔끔한 하나의 커밋
실무에서 가장 많이 쓰는 기능이다.
reword a1b2c3 로그인 기능 구현
→ 메시지를 다시 입력하게 된다.
PR 올리기 전에 메시지 정리할 때 자주 사용된다.
drop d4e5f6 테스트 코드 임시 추가
→ 해당 커밋을 히스토리에서 제거한다.
단, 이미 push한 브랜치에서는 매우 조심해야 한다.
rebase는 커밋을 "다시 만드는 것"이다.
그래서 이미 push한 브랜치에서 사용하면
커밋 ID가 전부 바뀌고
force push가 필요해진다.
즉, 팀 브랜치에서는 매우 조심해야 한다.
우리는 이미 이런 상황을 만들었다:
amend → 커밋 ID 변경rebase → 여러 커밋 ID 변경reset → 히스토리 이동이런 작업을 하고 나면
로컬 히스토리가 원격과 달라진다.
그 상태에서 그냥 push 하면 거부된다.
왜냐하면 원격에는
"이전 커밋 ID 기준으로 기록이 남아 있기 때문이다."
이때 사용하는 게:
git push --force
force push는 원격 히스토리를
내 로컬 기준으로 덮어쓰는 행위다.
즉, 원격에 있던 기록을 무시하고
지금 내 상태를 기준으로 다시 쓴다.
이게 위험한 이유로 자세한 건 바로 다음에 다루겠다.
공유 브랜치라면
다른 사람이 pull 받아간 커밋이
갑자기 사라질 수 있고,
충돌이 대량으로 발생할 수 있다.
그래서 force는 단순한 옵션이 아니라
협업 리스크다.
처음 Git을 배울 때는
"파일 변경을 기록하는 도구" 정도로 생각한다.
하지만 지금까지 다룬 기능들을 떠올려보자.
이 기능들의 공통점은 파일을 직접 수정하는 게 아니다.
커밋의 연결 구조를 다루는 것이다.
Git은 파일 자체를 저장하는 시스템이 아니다.
Git이 관리하는 것은:
즉, Git은 파일 관리 도구가 아니라
"히스토리 관리 시스템"에 가깝다.
reset, rebase, force push가 위험한 이유는
파일을 지워서가 아니라
히스토리 구조를 바꾸기 때문이다.
협업에서 중요한 건 파일 내용뿐 아니라
"누가, 언제, 어떤 흐름으로 작업했는가"이기 때문이다.
여기까지 오면 보이는 게 있다.
Git을 잘 다룬다는 건
명령어를 많이 아는 것이 아니라
히스토리를 어떻게 유지할지 판단하는 것이다.
이 관점이 잘 잡히면
merge와 rebase를 선택하는 기준도 달라진다.
기능은 많지만, 어찌됐던 결론은 하나로 볼 수 있다.
Git은 파일을 수정하는 도구가 아니라
커밋 히스토리를 설계하고 관리하는 도구다.
결국 전 파트에서 언급했던 것과 이어지는데,
명령어를 많이 아는 것이 아니라,
언제 히스토리를 건드려도 되는지
혹은 언제 건드리면 안 되는지
공유된 기록을 어떻게 다뤄야 하는지
이러한 판단 기준을 가지는 것이 중요하게 작용할 것 같다.
롤백은 언제든 발생할 수 있다.
하지만 그 동작이 협업 구조에 어떤 영향을 미치는지는 알고 사용해야 한다.
Git은 되돌릴 수 있지만, 히스토리는 가볍게 다룰 수 있는 것이 아니다.