Git: detached HEAD — 돌아가서 커밋 지키기
안녕하세요, BlueByte입니다. git checkout <something>을 실행했더니 Git이 "detached HEAD state"라는 긴 안내를 출력하고, 이제 git status가 브랜치 이름 대신 HEAD detached at 8fd6bda라고 말합니다. 깨진 것도, 아직 잃은 것도 없습니다 — 브랜치에서 내려와 특정 커밋 위에 선 것뿐입니다. 오늘은 이 상태가 뜻하는 것, 어쩌다 이렇게 됐는지, 안전하게 돌아가는 법, detached 상태에서 만든 커밋을 지키는 법, 그리고 이미 옮긴 뒤 되살리는 법까지 하나씩 짚어보겠습니다.
"detached HEAD"가 알려주는 것
평소 HEAD는 브랜치(예: main)를 가리키고, 브랜치가 커밋을 가리킵니다. "detached"는 중간에 브랜치 없이 HEAD가 커밋을 곧장 가리킨다는 뜻입니다. Git은 그 순간 경고합니다 — 아래는 풀어 쓴 게 아니라 실제 메시지입니다:
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>진짜 위험은 하나뿐입니다: 여기서 만든 커밋은 어느 브랜치에도 속하지 않아, 저장하지 않고 떠나면 Git의 일상적인 garbage collection이 결국 지웁니다. 마음껏 살펴보고 작업하되, 남기고 싶다면 떠나기 전에 작업에 브랜치를 붙이세요.
어쩌다 detached가 됐는가
로컬 브랜치가 아닌 것을 checkout할 때마다 Git은 HEAD를 detach합니다:
git checkout <sha>나git checkout HEAD~2— 특정 커밋.git checkout v1.4.0— 태그.git checkout origin/main— remote-tracking 참조(거의 항상git switch main을 의도한 것).- 중단된
git rebase,git bisect세션, 또는 커밋을 해시로 checkout하는 CI 작업.
이들 자체는 잘못이 아닙니다 — 태그를 checkout해 빌드하는 건 일상입니다. detached 상태임을 잊고 커밋을 시작할 때만 문제가 됩니다.
CI 안에서 git branch --show-current가 비어 나오는 걸 본 적이 있다면, 그것은 의도된 detached HEAD입니다: GitHub Actions, GitLab CI, 대부분의 러너는 재현 가능한 빌드를 위해 브랜치가 아니라 정확한 커밋 SHA를 checkout합니다. 그곳에서는 무해합니다 — 작업이 커밋하지 않으니까요 — 하지만 파이프라인에서 git rev-parse --abbrev-ref HEAD가 브랜치 이름 대신 HEAD를 찍는 이유입니다.
detached인지, 어디인지 확인
두 명령이 분명히 알려줍니다:
git status
# HEAD detached at 8fd6bda
git branch --show-current
# (아무것도 출력하지 않음 — 빈 줄은 현재 브랜치가 없다는 뜻)git branch도 목록 맨 위에 * (HEAD detached at 8fd6bda)로 표시합니다. 그 짧은 SHA가 지금 서 있는 위치입니다.
아무것도 바꾸지 않았다면 브랜치로 돌아가기
살펴보기만 했다면 브랜치로 다시 switch하면 됩니다. Git이 있던 자리로 되돌려 줍니다:
git switch main
git switch - # 또는: 이전 브랜치로 돌아가기흔한 경우엔 이게 전부입니다 — git checkout main도 같은 일을 합니다.
detached 상태에서 만든 커밋 지키기
경고를 알아채기 전에 커밋을 몇 개 만들었다고 합시다. 아직 옮기지 말고, 지금 커밋에 먼저 브랜치를 붙이세요:
git switch -c hotfix-cache # 브랜치가 작업을 가리키고 HEAD가 다시 붙음
git branch hotfix-cache # 또는: 커밋에서 옮기지 않고 브랜치만 생성git switch -c는 브랜치를 만들고 그 위로 한 번에 옮겨 주므로, 커밋이 이제 hotfix-cache에 안전히 놓입니다. 거기서 git switch main 후 git merge hotfix-cache로 브랜치에 합칩니다.
이미 옮긴 뒤 커밋 되살리기
이미 git switch main을 했고 detached 커밋이 사라진 듯 보여도 — 대개 아닙니다. reflog는 HEAD가 가리켰던 모든 위치를 기본 90일 동안 기록합니다:
git reflog -5 HEAD
# 8fd6bda HEAD@{0}: checkout: moving from a1b2c3d to main
# a1b2c3d HEAD@{1}: commit: fix cache key
# 9e8d7c6 HEAD@{2}: commit: add null guard마지막 detached 커밋의 SHA(여기서는 a1b2c3d)를 찾아 브랜치를 그 위에 고정하세요:
git branch recovered a1b2c3d
git log --oneline recovered -3 # 커밋이 거기 있는지 확인작업이 이름 있는 브랜치로 돌아왔습니다. "detached HEAD에서 커밋을 잃었다"가 거의 언제나 되살릴 수 있는 이유가 이것입니다 — reflog가 기억하기 때문입니다. 여러 detached 커밋 중 하나만 필요하다면, 대상 브랜치에서 git cherry-pick a1b2c3d가 작업 전체에 브랜치를 붙이는 대신 그 커밋 하나만 옮겨 옵니다.
reflog가 이미 사라진 경우
reflog는 영원하지 않습니다. 도달 불가능한 항목은 기본 30일 뒤 만료되고(gc.reflogExpireUnreachable), git gc가 더 일찍 지울 수도 있습니다. git reflog에 커밋이 더는 보이지 않아도, Git이 그 객체를 dangling(만들어졌지만 아무것도 가리키지 않는) 상태로 쥐고 있을 수 있습니다. 끊긴 끝을 물어보세요:
git fsck --lost-found --no-reflogs
# dangling commit a1b2c3d4e5f6...후보를 git show a1b2c3d로 살펴보며 본인 작업을 알아본 뒤, 앞과 똑같이 고정하세요: git branch recovered a1b2c3d. 이것이 더 깊은 안전망입니다 — 객체는 git gc가 실제로 돌기 전까지 지워지지 않으므로, reflog가 정리됐어도 커밋은 대개 아직 객체 저장소에서 이름 붙기를 기다리고 있습니다.
실제 사례: checkout한 태그 위에서 만든 두 개의 수정
버그를 재현하려 git checkout v1.4.0을 하고 두 커밋으로 고친 뒤 PR을 열려 git switch main을 했는데, 두 커밋이 없습니다. main의 git status는 깨끗해 사라진 듯 보입니다. git reflog -3 HEAD가 a1b2c3d에서 commit: fix null deref를 보여주고, git branch fix-v14 a1b2c3d 후 git switch fix-v14를 하면 두 커밋이 push할 수 있는 실제 브랜치에 놓입니다. 1분도 안 걸립니다. 아무것도 지워진 적 없이 참조만 없었기 때문입니다.
실수로 여기 다시 오지 않기
브랜치 사이를 옮길 때는 git checkout 대신 git switch를 쓰세요: --detach를 주지 않으면 detach하지 않으므로, 잘못 친 git switch origin/main은 조용히 detach하는 대신 오류를 냅니다. 커밋이나 태그를 탐색하고 싶을 때는 미리 브랜치를 만드세요: git switch -c try-upgrade v1.4.0. 그리고 advice.detachedHead는 켜 두세요(기본값 on). Git이 계속 알림을 출력하도록요 — git config advice.detachedHead false로 끄는 것이 나중에 조용한 사고로 이어집니다.
reset --hard로 잃은 것·평범한 checkout과 어떻게 다른가
평범한 git checkout main은 HEAD를 브랜치에 붙입니다 — 경고가 없습니다. 만든 커밋이 main에 놓이기 때문입니다. detached HEAD는 반대입니다: 이름을 붙이기 전까지 커밋이 어디에도 놓이지 않습니다. git reset --hard로 작업을 잃는 것과도 다릅니다 — 그건 브랜치가 이동하며 커밋을 버리는 것이지만, 되살리는 도구는 같습니다: git reflog가 reset 이전 SHA를 찾고 git branch(또는 git reset)가 되돌립니다. git status가 브랜치 이름을 말하면 붙어 있어 안전한 것이고, HEAD detached at ...라고 하면 switch 전에 작업을 저장하세요.
관련 질문
detached HEAD에서 커밋을 만들고 옮겼는데 사라졌나요?
거의 아닙니다. git reflog가 HEAD가 가리켰던 SHA를 보여줍니다(기본 90일 보관). git branch <name> <sha>로 그 커밋에 브랜치를 붙이면 작업이 돌아옵니다.
여기서 git switch와 git checkout의 차이는 무엇인가요?
git switch는 --detach를 주지 않으면 detach하지 않아 커밋/태그/remote 참조에서 조용히 detach하는 대신 오류를 냅니다. git checkout은 조용히 detach합니다. 브랜치 이동엔 git switch를 권합니다.
브랜치로 빨리 돌아가려면 어떻게 하나요?
git switch -는 이전 브랜치로, git switch main은 이름 있는 브랜치로 돌아갑니다. 바꾼 게 없다면 이게 전부입니다.
origin/main을 checkout했는데 왜 detach됐나요?
origin/main은 로컬 브랜치가 아니라 remote-tracking 참조라 HEAD가 브랜치가 아닌 커밋에 붙습니다. 그것을 추적하는 로컬 브랜치를 만들거나 checkout하는 git switch main을 쓰세요.
detached-HEAD 경고를 끌 수 있나요?
git config advice.detachedHead false로 끌 수 있지만 대개 실수입니다 — 그 경고가 아무것도 가리키지 않는 상태로 커밋하는 걸 막아 줍니다. 켜 두세요.
참고 자료
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
같은 카테고리 다른 글
Git: fatal: detected dubious ownership in repository
Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.
Kubernetes: Internal error occurred: failed calling webhook
admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)
V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.
Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트
컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.