Git이 실제로 생각하는 방식 (그리고 대부분의 개발자들이 틀리는 이유)
Source: Dev.to
위에 제공된 내용만으로는 번역할 텍스트가 없습니다. 번역을 원하는 본문을 알려주시면 한국어로 번역해 드리겠습니다.
Part 1 of the Git Mastery Series
다음은 거의 매달 모든 개발 팀에서 일어나는 대화입니다:
- 누군가가 다른 의도로
git reset --hard를 실행합니다. - 그들은 리베이스를 하고 히스토리가 완전히 엉망이 됩니다.
- 그들은 브랜치를 병합했지만 특정 변경 사항이 왜 반영되지 않았는지 알 수 없습니다.
그리고 채팅에 이렇게 입력합니다: “Git을 망가뜨린 것 같아요.”
Git을 망가뜨릴 수는 없습니다. 하지만 Git이 실제로 하는 일과 맞지 않는 정신 모델로 작업하면 스스로를 충분히 혼란스럽게 만들 수 있습니다.
대부분의 Git 튜토리얼은 명령어를 가르칩니다. Git이 어떻게 생각하는지 가르치는 경우는 거의 없습니다. 이 글은 그 격차를 메워줍니다 — 모델이 이해되면 명령어가 Stack Overflow에서 복사해오는 주문이 아니라 의도적으로 내리는 결정이 되기 때문입니다.
Git에 대해 가장 중요한 이해 사항이며, 대부분의 튜토리얼이 건너뛰거나 10장에 묻어두는 내용입니다.
git commit 을 실행할 때, Git은 “마지막 이후에 무엇이 바뀌었는지”를 저장하지 않습니다. 그 순간 프로젝트에 있는 모든 추적 파일의 전체 스냅샷을 저장합니다. 파일이 변경되지 않았다면 Git은 복제하지 않고 이전 스냅샷의 동일한 콘텐츠를 가리키기만 합니다. 개념적으로 각 커밋은 변경 목록이 아니라 프로젝트 전체의 완전한 그림입니다.
왜 중요한가?
사람들을 혼란스럽게 하는 거의 모든 것을 설명해 주기 때문입니다.
- 커밋을 cherry‑pick 할 때, Git은 변경을 “옮기는” 것이 아니라 해당 커밋과 그 부모 사이의 차이를 현재 상태에 적용합니다.
- rebase 할 때, Git은 커밋을 “옮기는” 것이 아니라 일련의 차이를 새로운 기반 위에 다시 재생합니다.
- reset 할 때, 포인터를 다른 스냅샷으로 이동시키는 것입니다. 실제로 파일이 삭제되는 것은 Git의 가비지 컬렉터가 실행될 때까지 일어나지 않습니다.
이것이 Git이 이렇게 강력한 이유이며, 파괴적으로 들리는 작업들이 보통은 그렇지 않은 이유입니다.
Git의 세 영역
| 영역 | 무엇인지 | 무엇을 하는지 |
|---|---|---|
| 작업 디렉터리 | 실제 파일들 — 에디터에서 보는 것, 실행하고 테스트할 수 있는 것. | Git은 이 영역을 인식하지만 직접 관리하지는 않습니다. |
| 스테이징 영역 (인덱스) | 다음 커밋을 준비하는 곳. | git add는 변경 사항을 대기실로 옮겨, “다음 스냅샷에 포함하세요”라고 말합니다. |
레포지토리 (.git 폴더) | 커밋이 영구적으로 저장되는 곳. | git commit은 스테이징 영역에 있는 모든 것을 가져와 새로운 스냅샷으로 감싸습니다. |
왜 이것이 중요한가
대부분의 개발자는 git add .와 git commit -m "…"를 한 번에 사용하며 스테이징 영역을 전혀 생각하지 않습니다. 이는 다음과 같은 상황이 필요할 때까지는 잘 작동합니다:
- 파일의 일부만 커밋하기
- 변경 사항 중 일부만 되돌리기
- 예상치 못한 내용이 커밋에 포함된 이유 파악하기
그때 모델이 여러분을 도와줍니다.
세 영역을 한 번에 비교해 보기
git diff # 작업 디렉터리 vs 스테이징 영역
git diff --staged # 스테이징 영역 vs 마지막 커밋
git status # 세 영역 전체 개요
변경을 만든 후에 이 명령들을 실행하세요. 출력 결과를 주의 깊게 읽으세요. 어떤 변경이 “보류 중”이고 어떤 것이 커밋됐는지 바로 알 수 있습니다.
브랜치는 단지 포인터일 뿐
Git 브랜치는 복잡한 개념처럼 들릴 수 있습니다 — 코드의 평행 우주, 별도의 개발 트랙 등. 실제 구현은 거의 우스꽝스럽게 단순합니다: 브랜치는 40자 커밋 해시가 들어 있는 텍스트 파일일 뿐입니다. 그게 전부입니다.
- 브랜치는 커밋을 가리키는 포인터입니다.
- 브랜치에서 새 커밋을 만들면 포인터가 새 커밋으로 앞으로 이동합니다.
- 브랜치를 만들면 Git은 그 포인터를 새로운 파일에 복사합니다. 코드 복사도, 평행 우주도 없습니다 — 단지 포인터일 뿐입니다.
브랜치가 정확히 무엇인지 확인해 보기
cat .git/refs/heads/main
# Output: a3f8c9d1e2b4f6a8c0d2e4f6a8b0c2d4e6f8a0b2
그 해시가 바로 여러분의 전체 브랜치입니다. 브랜치 이름은 그 커밋에 대한 인간이 읽을 수 있는 별명일 뿐입니다.
결과
- 브랜치를 만드는 것은 비용이 들지 않습니다 — 파일을 쓰는 것뿐이므로 즉시 이루어집니다.
- 브랜치를 전환하는 것도 빠릅니다 — 포인터를 이동하고 작업 디렉터리를 대상 스냅샷에 맞게 업데이트하는 것뿐이기 때문입니다.
“이 작업을 위해 브랜치를 만들겠다”는 무거운 결정처럼 느껴져서는 안 됩니다.
HEAD: 지금 내가 어디에 있는가
HEAD는 하나의 파일입니다. 여기에는 브랜치 이름이나 커밋 해시가 들어 있습니다. 이것은 한 가지 질문에 답합니다: “지금 나는 어디에 있나요?”
main에 있을 때 git log를 실행하면 HEAD가 main을 가리키고, main이 최신 커밋을 가리키기 때문에 main의 히스토리가 표시됩니다. 커밋을 하면 HEAD가 자동으로 앞으로 이동합니다.
cat .git/HEAD
# Output: ref: refs/heads/main
HEAD에 브랜치 이름이 들어 있으면 일반 모드에 있는 것입니다.
HEAD에 직접 커밋 해시가 들어 있으면 — 브랜치 이름이 아닌 — detached HEAD 상태에 있는 것입니다.
Detached HEAD는 위협적으로 들릴 수 있지만, 실제로는 브랜치가 아니라 히스토리의 특정 커밋을 보고 있다는 의미일 뿐입니다. 이 상태에서 커밋을 하면, 어떤 브랜치 포인터도 함께 움직이지 않기 때문에 결국 가비지 컬렉션에 의해 삭제될 수 있습니다. Detached HEAD에서 작업한 내용을 보존하려면 브랜치를 만들면 됩니다:
git switch -c my-new-branch
커밋과 그 내부 구조
첫 번째 커밋을 제외한 모든 커밋은 부모를 가리킵니다. 이것이 Git이 “무엇이 먼저였는지”를 알 수 있는 방법입니다. 커밋은 다음을 포함하는 객체입니다:
- 스냅샷(tree)에 대한 포인터
- 부모 커밋(들)에 대한 포인터
- 커밋 메시지
- 작성자와 타임스탬프
어떤 커밋이든 원시 내용 보기
git cat-file -p HEAD
# Output:
# tree 8a3f2d9c...
# parent 1b4e7a2f...
# author Shakil 1709123456 +0530
# committer Shakil 1709123456 +0530
#
# feat(auth): add OTP login
부모 포인터들의 체인이 바로 여러분의 히스토리입니다. git log를 실행하면 Git은 HEAD에서 시작해 부모 체인을 거꾸로 따라갑니다. git merge를 실행하면 Git은 두 체인을 거꾸로 탐색하여 공통 조상을 찾습니다.
이것이 Git 작업이 마법처럼 보이지만 — 병합 충돌 찾기, 블레임 표시, 이분 탐색 실행 — 실제로는 기계적으로 동작하는 이유입니다. Git은 단지 연결 리스트를 순회하고 있을 뿐입니다.
복구 가능성
대부분의 Git 불안은 무엇을 복구할 수 있는지 모르는 데서 비롯됩니다. 사실은 거의 모든 것이 복구 가능하다는 것입니다. reset을 할 때 커밋이 삭제되는 것이 아니라 참조되지 않음 상태가 됩니다. Git의 가비지 컬렉터가 이를 정리할 때까지 객체 데이터베이스에 계속 존재합니다.
(원본 텍스트는 여기서 갑자기 끝났지만, 핵심 메시지는 Git이 데이터를 명시적으로 정리할 때까지 보관한다는 것입니다.)
삭제된 커밋 복구
git reflog는 HEAD가 지금까지 있었던 모든 위치를 보여주며, 현재 어떤 브랜치도 가리키고 있지 않은 커밋도 포함합니다. 가비지 컬렉션이 실행되기 전까지 대략 90일 정도의 복구 기간이 있습니다.
# See everything HEAD has ever pointed to
git reflog
샘플 출력
a3f8c9d HEAD@{0}: commit: fix login bug
1b4e7a2 HEAD@{1}: reset: moving to HEAD~1
c5d2f8a HEAD@{2}: commit: add OTP login
git reset --hard로 “삭제한” 커밋은 여전히 HEAD@{1}에 존재합니다. 이를 복원하세요:
git reset --hard HEAD@{1}
사고 전환
Git 작업은 Git을 스냅샷 데이터베이스로 바라볼 때 더 이상 불안정하게 느껴지지 않습니다:
- Commits – 부모를 알고 있는 불변 스냅샷.
- Branches & HEAD – 그 데이터베이스를 가리키는 포인터일 뿐.
- Working directory – 하나의 스냅샷을 보는 당신의 시점.
- Staging area – 다음 스냅샷을 구성하는 곳.
이를 이해하면:
- 커밋은 영구적이다.
- 브랜치는 이동 가능하다.
- HEAD는 단지 “현재 위치”일 뿐이다.
…Git은 무서운 명령어들의 집합이 아니라, 논리적으로 이해할 수 있는 도구가 됩니다.
모든 Git 명령은 스냅샷, 포인터, 혹은 세 영역(working tree, index, repository)을 조작합니다. 사람들을 혼란스럽게 하는 명령들—rebase, reset, cherry-pick, reflog—도 그래프에 미치는 영향을 상상하면 즉시 이해할 수 있습니다.
실습
새 폴더(또는 테스트 프로젝트)를 열고 아래 단계들을 따라 하세요. 각 출력 결과를 꼼꼼히 읽으면서 진행합니다:
# Initialise a repository
git init
# Create a file
echo "hello" > file.txt
# Show untracked file
git status # → untracked file
# Stage the file
git add file.txt
# Show staged file
git status # → staged file
# Commit it
git commit -m "first commit"
# View the commit
git log --oneline # → see the commit hash and message
브랜치 만들고 확인하기
git branch feature # create branch
cat .git/refs/heads/feature # → just a hash
cat .git/HEAD # → shows current ref (e.g., ref: refs/heads/master)
git switch feature # switch to the new branch
cat .git/HEAD # → now points to refs/heads/feature
변경하고 diff 살펴보기
echo "world" >> file.txt
git diff # working directory vs. index (unstaged changes)
git add file.txt
git diff # nothing – changes are staged
git diff --staged # index vs. last commit
feature 브랜치에 커밋하기
git commit -m "add world"
git log --oneline --all --graph # visualize the branch split
이 과정을 의도적으로, 각 명령의 출력과 무엇이 바뀌었는지, 왜 바뀌었는지를 스스로 질문하면서 진행하면, 수동적인 읽기보다 훨씬 빠르게 탄탄한 머릿속 모델을 구축할 수 있습니다.
다음: 파트 2 – 의도적인 커밋: 좋은 커밋의 예술 →
이 내용이 도움이 되었다면, 전체 시리즈를 23‑page PDF reference로 정리했습니다. 포함 내용:
- 체크리스트
- 훅 템플릿
- 80개 이상의 유용한 명령
reflog와bisect에 대한 심층 탐구- 실제 상황 12가지에 대한 복구 플레이북
Git Mastery Field Guide → (link placeholder)