회사에 입사한 지 약 8개월이 되었고, 어느덧 주니어 개발자로서 업무를 수행하고 있는 요즘이다. 최근에는 신입 개발자가 들어오면서 사수 역할을 맡아 업무를 가이딩하고 있다.
업무를 진행하다 보면, 종종 부사수의 Git이 꼬여 문제가 발생하는 경우가 있다. 이럴 때마다 함께 상황을 확인해보지만, 어떤 명령어를 어떻게 실행했는지 정확히 알지 못한채 내가 추측한 방식으로 해결 방법을 제시하게 된다. 내가 제시한 방법으로 해결될때도 있지만, 오히려 더 꼬일때도 있다. 꼬이게 되면 더 해결이 복잡해지며 AI도 점점 감을 못잡게 되어 어려움을 겪곤 한다.
이 과정에서 느낀 점은 두 가지였다. 하나는 Git 문제가 단순히 “명령어를 아느냐”의 문제가 아니라는 점, 다른 하나는 나 역시 Git을 제대로 이해하지 못한 채 사용하고 있었다는 점이다.
지금까지 add, commit, push 같은 기본 명령어는 익숙하게 사용해왔지만, Git이 실제로 변경 이력을 어떻게 저장하는지,
파일 상태를 어떤 기준으로 추적하는지에 대해서는 깊이 생각해본 적이 없었다.
특히 git init 명령어로 생성되어 프로젝트 루트에 존재하는 .git 디렉토리가 어떤 역할을 하는지,
그 안에 무엇이 들어 있고 어떻게 동작하는지는 제대로 알고 있지 못한 상태였다.
그래서 이번 기회에 Git을 조금 더 근본적으로 이해해보고자 한다.
이 글에서는 .git 디렉토리를 직접 살펴보며, Git이 변경 이력을 어떻게 관리하는지 정리해보려고 한다.
모든 것은 .git 폴더에서 시작된다
.git 폴더를 보면 다음과 같은 디렉토리 구조를 가지고 있다.
.git/
├── COMMIT_EDITMSG
├── FETCH_HEAD
├── HEAD
├── ORIG_HEAD
├── config
├── description
├── hooks/
│ ├── pre-commit.sample
│ ├── pre-push.sample
│ └── ...
├── index
├── info/
│ └── exclude
├── logs/
│ ├── HEAD
│ └── refs/
├── objects/
│ ├── info/
│ ├── pack/
│ ├── 00/
│ ├── 1a/
│ └── ...
├── packed-refs
└── refs/
├── heads/
│ └── main
├── remotes/
│ └── origin/
└── tags/
결국 이 디렉토리 내용을 기반으로 git은 파일을 추적, 관리하는 건데.. 간단하게 Git이 버전 관리를 수행하기 위해 사용하는 내부 저장소라고 이해하면 된다.
이 구조를 이해하려면, 각 파일을 하나씩 보는 것보다 역할 기준으로 묶어서 보는 것이 훨씬 이해하기 쉽다.
크게 생각해보면, 현재 상태를 가리키는 포인터, 커밋한 파일 상태를 관리하는 폴더, 실제 저장되는 데이터를 관리하는 폴더, 기타 설정 정보 이렇게 4개로 나눌 수 있다.
1. 현재 상태를 가리키는 포인터
먼저, 현재 상태를 가리키는 포인터를 알아보자. 이 포인터가 왜 필요할까?
현재 상태를 알아내기 위해서 모든 폴더를 탐색한다고 생각해보자. 너무 비효율적이지 않을까?
사람들이 git을 사용하면서 가장 많이 확인하고 궁금해하는 지점이 “현재 상태”이다.
예를 들어, checkout, commit, merge, rebase와 같은 대부분의 작업은 모두 현재 위치를 기준으로 수행된다.
만약 현재 상태를 빠르게 알 수 있는 방법이 없다면, 매번 전체 이력이나 구조를 따라가며 위치를 계산해야 할 것이다.
이는 비효율적일 뿐 아니라, Git의 빠른 동작을 어렵게 만드므로 Git은 현재 상태를 가리키는 포인터를 HEAD의 형태로 바로 알 수 있도록 설계되어 있는 것이다.
.git/
├── objects/
│ ├── info/
│ ├── pack/
│ ├── 00/
│ ├── 1a/
│ └── ...
├── packed-refs
└── refs/
├── heads/
│ └── main
├── remotes/
│ └── origin/
└── tags/
HEAD에 해당하는 파일들은 위와 같다.
먼저, HEAD는 내가 현재 작업 중인 브랜치를 가리킨다.
- 그냥 plain text라서
cat명령어로 확인할 수 있으며 - 실제 확인해보면 아래와 같이 내가 현재 어떤 브랜치(혹은 커밋)를 바라보고 있는지를 나타내는 포인터 역할을 한다는 것을 알 수 있다.
$ cat HEAD
ref: refs/heads/feature-19
위의 예시처럼 cat 명령어를 통해서 바로 실제 어떤 브랜치, 커밋을 바라보고 있는지 바로 확인할 수 있다.
다음으로 ORIG_HEAD는 이전 상태를 임시로 저장해두는 포인터이다.
- rebase, reset 같은 위험한 작업 전에 자동으로 기록되어 되돌리기용 안전장치 역할을 한다.
- 실제로 해당 파일을 확인해보면 아래와 같이 커밋 해시가 나오는 것을 확인할 수 있다.
$ cat ORIG_HEAD
588564ee9c99aa8a12dd1b7928ec7a5a036a2608
- 그리고 git show 명령어를 사용해서 해당 해시가 어떤 커밋을 가리키는지도 확인할 수 있다.
$ git show 588564ee9c99aa8a12dd1b7928ec7a5a036a2608
commit 588564ee9c99aa8a12dd1b7928ec7a5a036a2608
Merge: 2205050 1ba56d8
Author: Hunmin Kim <48405500+gnsals0904@users.noreply.github.com>
Date: Sun Apr 19 15:45:24 2026 +0900
Merge pull request #14 from gnsals0904/feature-13
feat: 관리자 용 게시물 / 사용자 게시물 구분 기능 추가
마지막으로, FETCH_HEAD는 git fetch 등을 했을 때 가져온 원격 브랜치의 정보이다.
- 해당 파일도 cat, git show 명령어를 통해서 확인해볼 수 있다.
- 원격 브랜치의 정보이니
fetch나pull을 한지 오래되었다면, 당연히 최신이 아닐 수 있다.
2. 커밋한 파일 상태를 관리하는 폴더
.git/
├── COMMIT_EDITMSG
├── index
다음으로는 커밋한 파일의 상태를 관리하는 폴더를 살펴보자.
먼저 index 폴더는 흔히 말하는 staging area를 의미한다.
- 흔히 말하는 staging area
- git add 하면 여기에 기록된다.
- 이 부분은 다음 글에서 더 자세히 작성할 예정이다.
COMMIT_EDITMSG는 마지막 커밋 메시지를 기록한다.
- git commit 할 때 작성했던 마지막 메시지가 기록된다.
$ cat COMMIT_EDITMSG
feat: about / guide 라우팅 링크 변경
3. Git의 진짜 데이터 저장소
refs/
├── objects/
│ ├── info/
│ ├── pack/
│ ├── 00/
│ ├── 1a/
│ └── ...
├── packed-refs
└── refs/
├── heads/
│ └── main
├── remotes/
│ └── origin/
└── tags/
objects/ 폴더의 내용이 git의 핵심이고 모든 커밋, 파일, 디렉토리가 여기에 저장된다.
- git은 파일을 SHA-1 해시 기반 객체(object)로 저장하는데 모든 커밋, 파일, 디렉토리가 여기에 SHA-1으로 변환되어 저장된다고 생각하면 된다.
index/폴더와 함께 다음 글에서 더 자세하게 다루어 보겠다.
refs/ 폴더는 브랜치와 태그 정보를 저장한다.
- 커밋을 가리키는 포인터 모음이라고 생각하면 된다.
refs/
├── heads/
├── remotes/
├── stash
└── tags/
- 위에서부터 설명하자면,
heads/폴더는 로컬 브랜치를 관리하는 폴더이다. - 여기에 있는 파일 하나가 로컬 브랜치 하나와 1대1 대응한다.
$ ll
-rw-r--r--@ 1 root staff 41B Apr 12 14:58 docs-2
-rw-r--r--@ 1 root staff 41B Apr 6 23:21 feature-1
-rw-r--r--@ 1 root staff 41B Apr 12 23:04 feature-11
$ cat feature-1
e1de7223fd13c6e4c993a...
cat명령어를 통해 확인한 것이 커밋의 해시 ID(SHA-1)이다.remotes/폴더는 원격 브랜치를 관리하는 폴더이다.- 스냅샷 형태로 관리하기 때문에, 실제 원격과 당연히 동기화되어있는 것은 아니고,
pull이나fetch명령어를 통해서 동기화되는 방식이다. tags/폴더는 github 에서 보면,release : v1.1.0와 같이 특정 커밋에 tag가 붙어있는 것을 볼 수 있는데 그 내용을 기록한다.
packed-refs는 refs를 압축해서 저장한 파일이다.
- refs에서 브랜치나 태그가 많아지면 성능 최적화를 위해 사용된다.
4. 기타 설정 정보
.git/
├── config
├── description
├── hooks/
│ ├── pre-commit.sample
│ ├── pre-push.sample
│ └── ...
├── info/
│ └── exclude
├── logs/
│ ├── HEAD
│ └── refs/
config 파일은 remote, user 정보 등 git 설정 파일이다.
cat config명령어를 통해서 저장된 내용을 살펴보면 커밋될때 이름, email 등등이 저장되어있는 것을 확인할 수 있다.
hooks/ 폴더는 git 이벤트에 반응하는 스크립트들이 저장되어있다.
- 예를 들면, commit 전에 실행된다던지, push 전에 실행된다던지 하는 것들이다.
- CI/CD에 활용할 수도 있다.
logs/ 폴더는 HEAD의 이동 기록이다.
git reflog명렁어의 데이터가 여기에 저장된다.git log는 커밋 이력만 보여주기 때문에,git이 꼬일때git log만으로는 추적하기가 힘들다.- 이때,
git reflog명령어로HEAD의 이동 기록을 추적해 확인하는데 이 내용도 추후에 정리하고자 한다.
git 폴더는 마법처럼 파일, 폴더를 관리해주는 것이 아니다.
이처럼 .git 폴더에는 정말 많은 내용이 저장되어 있다. git은 마법처럼 동작하는 것이 아니라, 결국 모든 상태를 관리하고 기록하고 있기 때문에 그 내용을 기반으로 버전과 파일들을 추적 관리하는 것을 알 수 있다. 다음 글에서는 git commit 과정을 해보고 그 과정에서 objects가 어떻게 바뀌는지 알아보자.