본문 바로가기
개발환경

Git 커밋 히스토리에서 파일 제거하기 (git filter-branch)

by 굿햄 2026. 3. 6.

들어가며

Git으로 프로젝트를 관리하다 보면,
올리지 말았어야 할 파일이 이미 커밋에 포함되어 있는 상황 을 마주할 때가 있습니다.

 

저의 경우에는 Obsidian으로 관리하는 블로그 글 중
블로그/게시완료 폴더가 Git 히스토리에 남아 있는 게 문제였습니다.

 

이미 여러 커밋에 걸쳐 추적되고 있었기 때문에,
단순히 삭제하는 것만으로는 히스토리에서 완전히 지워지지 않습니다.

 

이번 글에서는 git filter-branch를 사용해
커밋 히스토리에서 특정 폴더를 완전히 제거한 과정과,
그 과정에서 만난 에러들을 정리합니다.


git rm만으로는 부족한 이유

"그냥 git rm으로 지우면 되는 거 아닌가?" 라고 생각할 수 있습니다.

git rm -r --cached "블로그/게시완료"
git commit -m "블로그/게시완료 폴더 제거"

이렇게 하면 현재 커밋 이후로는 해당 폴더가 추적되지 않습니다.


하지만 과거 커밋에는 여전히 파일이 남아 있습니다.


누군가 이전 커밋을 체크아웃하거나, 히스토리를 탐색하면
해당 파일을 그대로 볼 수 있습니다.

 

방법 현재 이후 제거 과거 히스토리 제거
git rm O X
git filter-branch O O

민감한 데이터나 더 이상 추적하고 싶지 않은 파일이라면,
히스토리 자체를 재작성해야 합니다.


이것이 git filter-branch가 필요한 이유입니다.


git filter-branch란?

Git에는 커밋 히스토리를 재작성(rewrite)하는 기능이 내장되어 있습니다.


그 중 하나가 git filter-branch입니다.

 

git filter-branch는 저장소의
모든 커밋을 처음부터 끝까지 순회하면서, 각 커밋에 지정한 필터(명령어)를 적용하는 도구입니다.


쉽게 말해, 과거의 모든 커밋을 하나씩 열어서 특정 파일을 빼거나,
커밋 정보를 수정한 뒤, 새로운 히스토리로 다시 쌓는 작업입니다.

 

이 과정에서 각 커밋의 내용이 바뀌기 때문에,
커밋 해시도 전부 새로 생성됩니다.


결과적으로 "처음부터 그 파일이 없었던 것처럼" 히스토리가 깔끔하게 재작성됩니다.

 

주로 다음과 같은 상황에서 사용합니다:

  • 민감한 데이터 제거: API 키, 비밀번호, 개인정보가 담긴 파일이 실수로 커밋된 경우
  • 대용량 파일 제거: 바이너리 파일이나 빌드 산출물이 히스토리에 쌓여 저장소 크기가 비대해진 경우
  • 특정 폴더/파일의 완전 삭제: 더 이상 추적하고 싶지 않은 경로를 히스토리에서 완전히 지우고 싶은 경우

저의 경우는 세 번째에 해당했습니다.


블로그/게시완료 폴더를 히스토리에서 완전히 지우기 위해 이 기능을 사용했습니다.


git filter-branch 명령어 분석

제가 사용한 명령어는 다음과 같습니다.

git filter-branch --force --index-filter \
  'git rm -rf --cached --ignore-unmatch "블로그/게시완료"' \
  --prune-empty --tag-name-filter cat -- --all

 

옵션 하나하나가 각각의 역할을 가지고 있으니,
표로 정리해 보겠습니다.

옵션 역할
--force 이전에 filter-branch를 실행한 백업이 남아 있어도 강제로 다시 실행
--index-filter 각 커밋의 인덱스(스테이징 영역) 에 대해 명령어를 실행. 워킹 디렉토리를 건드리지 않아 --tree-filter보다 빠름
git rm -rf --cached 인덱스에서 해당 경로를 재귀적(-r)으로 강제(-f) 삭제
--ignore-unmatch 해당 파일이 존재하지 않는 커밋에서도 에러 없이 넘어감
--prune-empty 파일 제거 후 변경사항이 없어진 빈 커밋을 자동으로 제거
--tag-name-filter cat 태그도 새로운 커밋 해시에 맞게 갱신
-- --all 모든 브랜치와 참조에 대해 실행

 

--index-filter는 모든 커밋을 순회하면서
각 커밋의 인덱스에서 해당 파일을 제거하고,
새로운 커밋 해시로 히스토리를 재작성합니다.


커밋이 많을수록 시간이 걸리지만, --tree-filter보다는 훨씬 빠릅니다.


실행 과정에서 만난 에러와 해결

에러 1: Cannot rewrite branches — unstaged changes

명령어를 실행하자마자 다음 에러가 발생했습니다.

Cannot rewrite branches: You have unstaged changes.

 

filter-branch는 히스토리를 재작성하는 작업이기 때문에,
워킹 디렉토리가 깨끗한 상태여야 합니다.


수정 중이던 파일이 있으면 실행을 거부합니다.

 

해결 방법: git stash로 현재 변경사항을 임시 저장한 뒤 진행했습니다.

# 1. 현재 변경사항을 임시 저장
git stash

# 2. filter-branch 실행
git filter-branch --force --index-filter \
  'git rm -rf --cached --ignore-unmatch "블로그/게시완료"' \
  --prune-empty --tag-name-filter cat -- --all

# 3. 변경사항 복원 (이 단계에서 문제가 발생합니다)
git stash pop

 

git stashfilter-branch는 정상적으로 완료되었습니다.


여기까지는 순조로웠습니다.

에러 2: stash pop 실패 — 참조 깨짐

filter-branch가 성공적으로 끝난 후,
git stash pop으로 임시 저장한 변경사항을 복원하려 했습니다.

fatal: 'refs/stash@{0}' is not a stash-like commit

 

이 에러는 filter-branch모든 커밋 해시를 재작성하면서 발생한 문제입니다.

 

git stash는 내부적으로 현재 커밋을 기반으로 임시 커밋을 만듭니다.


그런데 filter-branch가 히스토리를 전부 재작성하면서,
stash가 참조하던 원본 커밋이 더 이상 존재하지 않게 됩니다.


참조가 깨졌기 때문에 stash를 복원하는 것은 불가능합니다.

stash에 넣었던 변경사항은 복구할 수 없습니다.
filter-branch 전에 stash 대신 별도의 브랜치에 커밋하거나,
변경사항을 다른 곳에 백업해 두는 편이 안전합니다.

다행히 제 경우에는 stash에 넣었던 파일이 크리티컬하지 않아서
수동으로 다시 수정하는 것으로 마무리할 수 있었습니다.


실행 후 정리 작업

filter-branch가 끝난 뒤에도,
Git 내부에는 원본 커밋의 참조가 reflog에 남아 있고,
실제 객체도 바로 삭제되지 않습니다.


디스크 공간을 확보하고 민감한 데이터를 완전히 제거하려면 정리 작업이 필요합니다.

# reflog 만료 — 원본 커밋에 대한 참조 제거
git reflog expire --expire=now --all

# 가비지 컬렉션 — 참조가 없는 객체를 실제로 삭제
git gc --prune=now --aggressive

 

이 작업까지 마쳐야 비로소
로컬 저장소에서 해당 파일이 완전히 제거됩니다.

 

그리고 원격 저장소에도 반영하려면 강제 푸시가 필요합니다.

git push --force --all

 


GitHub 공식 권장 도구 — git filter-repo

사실 GitHub 공식 문서에서는
git filter-branch 대신 git filter-repo를 권장하고 있습니다.
git filter-branch는 이미 deprecated(사용 중단 권고) 상태입니다.

# 설치
pip install git-filter-repo

# 실행
git filter-repo --invert-paths --path "블로그/게시완료"

 

두 도구의 차이를 정리하면 다음과 같습니다.

  git filter-repo git filter-branch
상태 현재 공식 권장 도구 deprecated
속도 빠름 느림 (커밋마다 쉘 명령 실행)
안전성 백업/정리 자동 처리 reflog/gc 수동 처리 필요
설치 pip install git-filter-repo Git 기본 포함
민감 데이터 전용 옵션 --sensitive-data-removal (v2.47+) 해당 없음

 

가능하다면 git filter-repo를 사용하는 것을 추천합니다.


저는 환경 문제로 filter-branch를 사용했지만,
filter-repo가 모든 면에서 더 나은 선택입니다.


주의사항 정리

filter-branchfilter-repo든,
히스토리를 재작성하는 작업은 여러 가지 부작용이 따릅니다.

  1. 강제 푸시 필수:
    히스토리가 바뀌었으므로 git push --force가 필요합니다.
    협업 중인 저장소라면 팀원에게 반드시 공유해야 합니다.
  2. 모든 커밋 해시가 변경됨:
    히스토리를 재작성하면 해당 커밋 이후의 모든 커밋 해시가 바뀝니다.
    기존에 특정 커밋을 참조하고 있던 PR, 이슈 링크 등이 깨질 수 있습니다.
  3. stash가 손실될 수 있음:
    위에서 겪었듯이, filter-branch 전에 git stash를 사용하면 이후 복원이 불가능합니다.
    별도 브랜치에 커밋하거나 파일을 복사해 두세요.
  4. .gitignore 등록 필수:
    히스토리에서 제거한 뒤에는 .gitignore에 해당 경로를 추가해서,
    이후 다시 커밋되는 것을 방지해야 합니다.
# .gitignore
블로그/게시완료/

돌아보며

단순히 파일을 삭제하는 것과,
히스토리에서 완전히 제거하는 것은 전혀 다른 작업이라는 걸 체감한 경험이었습니다.

 

특히 git stash로 임시 저장한 변경사항이
filter-branch 이후에 복구가 안 되는 상황은,
직접 겪어보지 않으면 예상하기 어려운 부분이었습니다.


"히스토리 재작성은 모든 참조를 바꾼다"는 사실을 머리로는 알고 있었지만,
stash까지 영향을 받을 거라고는 생각하지 못했습니다.

 

앞으로 히스토리 재작성 작업을 할 때는,
작업 전에 변경사항을 별도 브랜치에 커밋해 두거나 파일을 외부에 백업해 두는 습관을 들여야겠다고 느꼈습니다.


그리고 가능하다면 git filter-repo를 사용하는 것이 훨씬 안전하고 편리합니다.

 

비슷한 상황을 겪고 계신 분들께 조금이나마 도움이 되었으면 합니다.

 

ⓒ 굿햄 2026. daryeou@gmail.com all rights reserved.

댓글