Git 한글 파일명 자소분리: 맥에서 생기는 유령 변경 해결하기
맥에서 git status를 쳤는데 보고서.txt 같은 파일이 deleted로 뜨고, 바로 아래에 이름이 똑같은 파일이 untracked로 또 뜬다면 한글 파일명 자소분리 문제, 정확히는 NFD와 NFC 파일명 충돌입니다. 저장소에는 파일명이 조합형 유니코드(NFC, 보가 글자 하나)로 들어 있는데, macOS는 Git에 분해형(NFD, ㅂ+ㅗ처럼 자모가 따로)으로 넘겨줍니다. Git은 바이트 단위로 비교하기 때문에 이 둘을 서로 다른 파일로 봅니다. 맥 쪽 해결책은 설정 하나, git config core.precomposeunicode true이고, 분해형 이름이 이미 커밋된 상태라면 git mv로 조합형 이름으로 바꿔 주면 됩니다. 아래에서 진단을 확인하는 방법과, 스크립트·서버·업로드까지 NFD가 흘러들지 않게 막는 방법을 순서대로 설명합니다.
Git이 보고서.txt를 두 개로 보는 이유: 한글 파일명 자소분리와 NFD/NFC
유니코드는 같은 글자를 두 가지 방식으로 적을 수 있게 허용합니다. 보는 완성형 코드 포인트 하나(U+BCF4)로도 쓸 수 있고, 초성 ᄇ(U+1107)과 중성 ᅩ(U+1169)를 이어 붙인 두 개의 코드 포인트로도 쓸 수 있습니다. 앞쪽이 NFC, 뒤쪽이 NFD입니다. 화면에서는 둘 다 똑같이 보로 보입니다. 윈도우 탐색기에서 ㅂㅗㄱㅗㅅㅓ.txt처럼 자음과 모음이 흩어져 보이는 현상이 바로 NFD 파일명을 윈도우가 그대로 보여 준 결과입니다. 일본어 가나의 탁점(が가 か+゛), 프랑스어·스페인어의 악센트 문자(é가 e+◌́)도 같은 식으로 갈라집니다.
윈도우, 리눅스, 대부분의 웹 서버와 편집기는 NFC를 만들어 냅니다. 반면 macOS는 오랫동안 NFD를 만들어 왔습니다. 예전 HFS+ 파일 시스템은 모든 이름을 분해형으로 정규화했고, APFS로 바뀐 지금도 많은 macOS API와 Finder가 분해형 이름을 돌려줍니다. Git은 경로를 전혀 정규화하지 않습니다. 작업 디렉터리가 주는 바이트를 그대로 저장하고, 그 바이트를 인덱스와 비교할 뿐입니다.
그래서 보통 이런 순서로 일이 벌어집니다.
- 윈도우나 리눅스를 쓰는 동료가
docs/보고서.txt를 NFC 이름으로 커밋합니다. - 내가 맥에서 클론합니다. Git은 NFC 이름으로 파일을 쓰지만, 파일 시스템이나 맥의 도구들이 그 이름을 NFD로 바꿔서 돌려줍니다.
- Git이 인덱스의
\xEB\xB3\xB4...와 작업 디렉터리의\xE1\x84\x87\xE1\x85\xA9...를 비교합니다. 다르니까 NFC 파일은 삭제됐다고, NFD 파일은 추적되지 않는다고 보고합니다.
반대 방향도 있습니다. 맥 사용자가 NFD 이름으로 커밋하면 리눅스 쪽에서는 똑같이 생긴 "두 번째" 파일이 생기고, 소스 코드에 NFC로 적힌 경로로 보고서.txt를 찾는 빌드가 파일을 못 찾아 실패합니다.
실제로 어떤 형식인지 확인하기
짐작하지 말고 바이트를 직접 보는 게 가장 빠릅니다.
작업 디렉터리(macOS 또는 리눅스)에서:
ls docs | grep 보고서 | xxd
- 출력에
eb b3 b4가 보이면 NFC입니다(보가 코드 포인트 하나). e1 84 87 e1 85 a9처럼e1 84~e1 87로 시작하는 3바이트 묶음이 이어지면 NFD입니다(한글 자모 U+1100~U+11FF 영역).
Git이 실제로 커밋한 내용, 즉 인덱스에서는:
git ls-files -z docs | xxd
두 결과를 비교합니다. 인덱스에는 eb b3 b4가 있고 디렉터리 목록에는 e1 84 87이 있다면 저장소는 멀쩡하고 내 맥이 이름을 분해하고 있는 것입니다. 인덱스 자체에 e1 84~e1 87 묶음이 들어 있다면 분해형 이름이 이미 커밋된 상태라 고쳐야 합니다.
저장소 전체에서 이미 커밋된 NFD 이름을 한 번에 찾으려면:
git ls-files -z | perl -0ne 'print "$_\n" if /[\x{300}-\x{36f}\x{3099}\x{309a}\x{1100}-\x{11ff}]/'
라틴 결합 부호, 일본어 결합형 탁점·반탁점, 한글 조합형 자모가 들어 있는 경로를 모두 출력합니다.
Git에서 고치기
1단계: macOS의 Git에게 이름을 다시 조합하라고 설정하기
git config --global core.precomposeunicode true
이 설정을 켜면 Git이 파일 시스템에서 읽은 NFD 이름을 인덱스와 비교하기 전, 그리고 커밋하기 전에 NFC로 되돌립니다. 최근 Git은 git init이나 git clone 때 파일 시스템을 탐지해서 저장소 설정에 자동으로 넣어 주는 경우가 많은데, 그래서 이 문제가 컴퓨터마다 생겼다 안 생겼다 하는 것처럼 보입니다. 전역으로 켜 두면 그 복불복이 사라집니다. 설정을 바꾼 뒤 git status를 다시 실행하면 deleted/untracked 짝이 사라져야 합니다.
이 설정은 Git의 macOS 빌드에만 있습니다. 리눅스와 윈도우에서는 아무 동작도 하지 않지만, 그 시스템들은 애초에 이름을 분해하지 않으니 문제없습니다.
2단계: 이미 NFD로 커밋된 이름 고치기
인덱스에 분해형 경로가 들어 있다면 NFC로 바꿔 줍니다. 이름이 바이트 단위로 달라져야 하므로 두 번에 나눠 옮깁니다. 대소문자나 정규화를 구분하지 않는 파일 시스템에서는 Git이 "같은 이름"으로의 변경을 거부하기 때문입니다.
# 경로의 NFC 버전을 만든다
nfc=$(printf '%s' 'docs/보고서.txt' | iconv -f utf-8-mac -t utf-8)
git mv 'docs/보고서.txt' docs/tmp-rename
git mv docs/tmp-rename "$nfc"
git commit -m "파일명을 NFC로 정규화"
위의 경로는 git ls-files 출력에 나온 분해형 경로로 바꿔 넣으세요. 터미널에서 복사하면 보통 분해형 바이트가 그대로 유지됩니다. utf-8-mac은 macOS의 iconv가 알아듣는 인코딩 이름입니다. 리눅스에서는 다음 섹션의 Python을 쓰거나 convmv 도구를 씁니다: convmv -f utf-8 -t utf-8 --nfc --notest docs/보고서.txt
파일이 많다면 손으로 하나씩 바꾸지 말고, 위 탐지 명령의 출력을 반복문으로 돌리면 됩니다.
3단계: 머지할 때 다시 터지지 않게 하기
이름을 통일한 뒤에는 .gitattributes가 도움이 되지 않습니다. 그 파일은 줄바꿈과 diff를 다루지 경로 정규화는 다루지 않기 때문입니다. 결국 보호 장치는 설정 하나와 습관입니다. 맥을 쓰는 사람은 전부 core.precomposeunicode를 켜 둡니다. 운영체제가 섞인 팀이라면 위의 탐지 한 줄을 pre-commit 훅에 넣고, 하나라도 걸리면 커밋을 막는 것이 비용 대비 효과가 좋습니다.
스크립트, 서버, 파일 업로드에서 정규화하기
Git은 문제가 눈에 띄는 곳일 뿐입니다. 똑같은 NFD 이름이 배포 스크립트, 정적 사이트 빌드, 문자열 비교로 경로를 찾는 업로드 처리기도 망가뜨립니다. 원칙은 간단합니다. 이름이 내 시스템에 들어오는 경계에서 NFC로 정규화합니다.
Python:
import unicodedata, os
for name in os.listdir("uploads"):
nfc = unicodedata.normalize("NFC", name)
if nfc != name:
os.rename(os.path.join("uploads", name), os.path.join("uploads", nfc))
Node.js(예를 들어 업로드 처리기에서 디스크나 오브젝트 스토리지에 쓰기 전에):
const safeName = file.originalname.normalize("NFC");
셸에서 맥에서 리눅스 서버로 동기화할 때는 rsync가 변환하게 해서 서버가 NFD를 아예 보지 못하게 합니다:
rsync -av --iconv=utf-8-mac,utf-8 ./site/ user@server:/var/www/site/
이미 리눅스 서버에 올라가 있는 디렉터리를 한꺼번에 고치려면:
convmv -r -f utf-8 -t utf-8 --nfc --notest /var/www/uploads
알아 두면 좋은 점 두 가지입니다. 첫째, S3 같은 오브젝트 스토리지는 키를 그냥 바이트 덩어리로 취급하므로, 맥에서 올린 보고서.jpg와 데이터베이스에서 참조하는 보고서.jpg가 조용히 서로 다른 키가 될 수 있습니다. 키를 만들기 전에 정규화하세요. 둘째, 브라우저는 multipart 업로드의 파일명을 정규화하지 않습니다. 사이트의 업로드 폼은 사용자의 맥이 만든 이름을 그대로 받습니다. 서버 쪽 정규화가 이를 확실히 잡을 수 있는 유일한 지점입니다.
내 파이프라인 밖에서 온 파일인 경우
위 코드는 내가 통제하는 파이프라인을 다룹니다. 맥을 쓰는 디자이너가 한글 이름 이미지 300장을 이슈에 던져 넣거나, 거래처에서 카카오톡이나 메일로 보낸 zip을 윈도우에서 풀었더니 파일명이 전부 ㅎㅏㄴㄱㅡㄹ처럼 자소분리되어 나올 때는 소용이 없습니다. 이런 파일은 저장소, CMS, 서버 근처에 가기 전에 이름부터 바꿔야 하는데, 보낸 사람에게 "NFC로 저장해 주세요"라고 부탁해 봐야 그쪽 화면에서는 차이가 보이지 않으니 잘 통하지 않습니다.
이럴 때는 무료 FilenameFixer 웹앱을 쓰면 스크립트 없이 변환할 수 있습니다. 파일을 브라우저에 끌어다 놓으면 NFD에서 NFC로 한꺼번에 이름을 바꿔 주고, zip 파일(200 MB까지)을 통째로 넣으면 압축 안의 파일명을 복원해 줍니다. 붙여 넣은 텍스트도 변환해 주므로 설정 파일에 적힌 경로 목록을 고칠 때도 쓸 수 있습니다. 처리는 브라우저 안에서 로컬로 이루어지고 파일 자체는 서버로 올라가지 않기 때문에, 고객사 자료처럼 밖으로 내보내기 곤란한 파일에도 쓸 수 있습니다. 팀이 웹 폼으로 업로드하는 경우라면 Chrome 확장 프로그램이 업로드 시점에 파일명을 자동으로 고쳐 주어, 브라우저가 NFD를 그대로 넘기는 구멍을 막아 줍니다.
이미 Git 안에 들어 있는 파일은 core.precomposeunicode와 git mv로 처리하세요. Git에 알리지 않고 외부 도구로 작업 디렉터리의 파일명을 바꾸면, 처음에 봤던 deleted/untracked 짝이 그대로 다시 생깁니다.