맥에서 압축해 보낸 파일이 거래처 윈도우에서 열렸는데, 파일명이 전부 "ㅇㅝㄹㄱㅏㄴㅂㅗㄱㅗ"처럼 자음·모음이 낱낱이 흩어져 있다는 연락을 받았다. 반대로 내가 받는 쪽일 때도 겪어봤다. 이게 그 유명한 맥 자소분리 현상인데, 겉보기엔 파일이 깨진 것 같아서 당황하게 된다.
먼저 안심해도 된다 — 파일 내용은 멀쩡하다. 깨진 건 파일명의 표기 방식뿐이고, 원인은 macOS와 윈도우가 한글을 저장하는 유니코드 방식(NFD vs NFC)이 다르기 때문이다. 그리고 매일 쓰는 압축 도구만 바꾸면 보내는 쪽에서 완전히 예방된다. 이 글에서는 "그렇다더라"로 끝내지 않고, 내 맥(macOS 26.5.1)에서 같은 한글 파일을 세 가지 방법으로 압축한 뒤 zip 내부 파일명을 파이썬으로 바이트 단위까지 검사했다. 그 결과를 가공 없이 실었는데, 좀 의외였다.
맥 자소분리는 왜 생기나 — NFD와 NFC
한 줄로 답하면, 맥은 한글 파일명을 자모 단위로 풀어서(NFD) 저장하는 경우가 많고 윈도우는 완성형(NFC)을 기대하기 때문이다.
유니코드에서 "한"이라는 글자는 두 가지 방식으로 표현할 수 있다. 완성형 코드 하나로 적는 방식이 NFC(완성형·Composed), ㅎ+ㅏ+ㄴ 세 개의 자모 코드를 이어 붙이는 방식이 NFD(분리형·Decomposed)다. 둘은 화면에 똑같이 "한"으로 보이는 게 정상인데, 윈도우 쪽 프로그램 상당수가 NFD를 조합해서 그려주지 않고 자모를 낱글자 그대로 나열해버린다. 그래서 "월간보고"가 "ㅇㅝㄹㄱㅏㄴㅂㅗㄱㅗ"로 보이는 것이다.
중요한 건 이게 이름 표기의 문제일 뿐이라는 점이다. 압축이 손상된 것도, 문서 내용이 날아간 것도 아니다. 파일을 열면 내용은 그대로 있다. 그러니 맥 자소분리 화면을 봤다고 파일을 다시 받을 필요는 없다 — 이름만 고치면 된다.
NFD는 어디서 생기나 — Finder와 터미널이 다르다
결과부터 보면, 같은 맥, 같은 디스크인데 파일명을 만든 주체에 따라 NFD가 되기도 NFC가 되기도 한다. 이건 직접 확인해보고 나도 좀 놀랐다.
흔히 "맥 파일시스템은 무조건 NFD"라고 알려져 있는데, 그건 옛날 HFS+ 시절 이야기다. 지금의 APFS는 정규화를 강제하지 않고 앱이 준 이름을 그대로 저장한다. 그래서 실험을 해봤다. Finder에서 "최종보고서"라는 폴더를 만들고, 터미널에서 "보고서_최종.txt"라는 파일을 만든 뒤, 파이썬으로 디스크에 실제로 저장된 이름을 검사했다.
import os, unicodedata
for n in os.listdir('.'):
form = 'NFC' if n == unicodedata.normalize('NFC', n) else \
('NFD' if n == unicodedata.normalize('NFD', n) else '혼합')
print(f'{form} {n!r} ({len(n)}자)')
NFC '보고서_최종.txt' (10자) ← 터미널(echo)로 만든 파일
NFD '최종보고서' (11자) ← Finder로 만든 폴더
보이는가. Finder가 만든 "최종보고서"는 화면엔 5글자인데 디스크엔 11개 코드포인트로 저장돼 있다 — "최종"만 봐도 ㅊ+ㅚ+ㅈ+ㅗ+ㅇ 식으로 자모가 풀린 NFD다. 반면 터미널에서 만든 파일은 10자(한글 5 + 언더바 1 + 확장자 4), 즉 완성형 NFC 그대로다. Finder가 파일명을 NFD로 만들어내는 게 맥 자소분리의 출발점이고, 우리가 파일을 만들고 이름 짓는 곳은 대부분 Finder다.
압축하면 어떻게 되나 — zip·ditto 실측
결론은 이렇다 — 맥 기본 압축은 NFD 파일명을 그대로 zip 안에 담는다. 심지어 문제가 하나 더 있다.
NFD 파일명("월간보고_최종.txt", 디스크에 20자모)을 두 가지 기본 도구로 압축해봤다. zip 명령과, Finder "압축하기"와 같은 형태의 zip을 만드는 ditto다. 그리고 zip 포맷 명세대로 내부 파일명 레코드를 검사하는 스크립트를 짰다.
import zipfile, unicodedata, glob
for zpath in sorted(glob.glob("*.zip")):
with zipfile.ZipFile(zpath) as z:
for info in z.infolist():
utf8 = bool(info.flag_bits & 0x800) # zip의 UTF-8 파일명 플래그
raw = info.filename.encode("utf-8") if utf8 \
else info.filename.encode("cp437")
name = raw.decode("utf-8")
if name.startswith("._"): # 맥 리소스포크 항목 제외
continue
form = "NFC(완성형)" if name == unicodedata.normalize("NFC", name) \
else "NFD(자소분리)"
print(f"{zpath}: {form}, 유니코드 {len(name)}자, UTF-8플래그 {utf8}")
nfd-zip-cli.zip: NFD(자소분리), 유니코드 20자, UTF-8플래그 False
nfd-ditto.zip: NFD(자소분리), 유니코드 20자, UTF-8플래그 False
두 가지가 확인된다. 첫째, NFD가 zip 내부까지 그대로 전파된다 — 압축 과정에서 아무도 이름을 완성형으로 바꿔주지 않는다. 둘째, UTF-8 플래그(zip 헤더의 비트 11)조차 설정하지 않는다. 이 플래그가 없으면 윈도우 압축 프로그램은 파일명을 UTF-8이 아니라 시스템 레거시 인코딩으로 해석하려 들 수 있어서, 자소분리를 넘어 아예 외계어로 깨질 여지까지 생긴다. 즉 맥 기본 압축은 윈도우 호환 관점에서 이중으로 불리하다.
Keka로 압축하면? — 실측 결과가 의외였다
답부터 — Keka는 압축하는 시점에 파일명을 NFC 완성형으로 바꾸고, UTF-8 플래그까지 설정한다. 받는 윈도우에서 깨질 요소를 둘 다 없앤다.
나는 맥에서 압축할 일이 있으면 Keka를 쓴다. 그런데 이 글을 쓰려고 검색해보니 "Keka 쓰면 한글 잘 된다"는 후기는 많은데, 정작 Keka가 파일명을 어떻게 처리하는지 확인한 데이터는 찾지 못했다. 그래서 위와 같은 NFD 파일을 Keka(1.6.7)에 드래그해서 zip으로 압축하고, 같은 스크립트로 검사했다.
# Keka로 압축한 zip을 같은 스크립트로 검사한 결과
월간보고_최종.txt.zip: NFC(완성형), 유니코드 11자, UTF-8플래그 True
원본 파일명은 디스크에서 20자모짜리 NFD였는데, Keka가 만든 zip 안에는 11자 완성형(NFC)으로 들어가 있다. UTF-8 플래그도 켜져 있다. 압축 단계에서 정규화를 해준다는 뜻이라, 받는 쪽이 어떤 윈도우 압축 프로그램을 쓰든 자소분리를 볼 일이 없다. 솔직히 나는 "덜 깨지는 정도"를 기대했는데, 바이트를 까보니 정석대로 처리하고 있었다.
같은 NFD 파일인데, 압축 방법에 따라 결과가 이렇게 갈린다.
| 압축 방법 | zip 내부 파일명 | UTF-8 플래그 | 윈도우에서 |
|---|---|---|---|
| zip 명령 | NFD 그대로 | ✗ | 자소분리 (+인코딩 깨짐 여지) |
| ditto (Finder 압축과 같은 형태) | NFD 그대로 | ✗ | 동일 |
| Keka 1.6.7 | NFC로 변환 | ✓ | 정상 표시 |
반디집과 반디네이머는 어떤가
반디집 macOS 버전도 공식 문서에서 "반디집 macOS는 파일명을 기본적으로 NFC로 처리하므로, 맥 반디집으로 압축해 윈도우에서 풀면 파일명이 올바르게 표시된다(Since Bandizip for macOS handles a filename using NFC by default…)"고 설명하고 있다. 방향은 Keka 실측 결과와 같다. 다만 맥 앱스토어의 반디집이 유료로 전환돼 있어서, 이 글의 바이트 검사는 직접 해보지 못했다 — 위 표에 반디집이 빠진 이유다. 문서 기준으로는 믿고 쓸 수 있는 선택지로 보인다.
같은 회사의 반디네이머(BandiNamer)는 무료인데 역할이 다르다. 압축 도구가 아니라 디스크의 파일명 자체를 NFD↔NFC로 변환해주는 도구다. 압축 없이 USB나 클라우드로 파일을 넘길 때는 압축 도구가 개입할 틈이 없으니, 이런 경우 보내기 전에 반디네이머로 이름을 완성형으로 바꿔두는 방식이 유효하다.
이미 깨진 채 받았다면
보내는 쪽을 못 바꾸는 상황, 즉 자소분리된 파일명을 이미 받아버린 윈도우 사용자라면 파일명을 일괄 교정해주는 도구를 쓰면 된다. 윈도우용 반디집 최신 버전은 압축 해제 시 자소분리 파일명을 처리해주고, "한글 자소 교정기" 같은 무료 유틸도 있다. 설치 없이 해결하고 싶으면 zip을 풀기 전에 브라우저에서 zip 파일명을 진단·복구해주는 도구에 올려서 정상 파일명 zip으로 다시 받는 방법도 있다 — 밝혀두자면 이 글을 쓰면서 내가 직접 만들어 운영하는 도구다(papernhoney). 파일이 서버로 올라가지 않고 브라우저 안에서만 처리된다. 파일 개수가 적으면 그냥 이름을 다시 타이핑해도 되고 — 어차피 내용은 멀쩡하니까.
개발자라면 파이썬 한 줄로 끝난다. 폴더 안 파일명을 전부 NFC로 바꾸는 코드다.
import os, unicodedata
for n in os.listdir('.'):
nfc = unicodedata.normalize('NFC', n)
if n != nfc:
os.rename(n, nfc)
참고로 이 반대 방향 사고도 있다. 윈도우에서 만든 zip이 맥에서 아예 안 풀리는 문제인데, 그건 자소분리가 아니라 압축 알고리즘(deflate64) 문제라 원인이 전혀 다르다. 그 삽질은 맥 압축 오류 79 해결기에 따로 정리해뒀다.
남은 궁금증 정리
파일 내용도 깨진 건가?
아니다. 맥 자소분리는 파일명 표기 문제일 뿐이고 데이터는 손상되지 않는다. 이름이 깨진 상태로도 파일은 정상적으로 열린다.
클라우드(드롭박스·구글드라이브)로 보내면 괜찮나?
일부 클라우드 서비스는 동기화 과정에서 파일명을 완성형으로 바꿔준다고 알려져 있지만, 서비스와 시점에 따라 동작이 다를 수 있어 일반화하기 어렵다. 확실한 건 보내는 쪽에서 이름을 NFC로 만들어 보내는 것 — 압축이라면 Keka류, 압축 없이 보낸다면 반디네이머로 변환 후 전송이다.
맥끼리 주고받을 때도 문제가 되나?
표시 문제는 없다. 맥은 NFD를 조합해서 정상적으로 그려준다. 다만 개발자라면 조심할 게 하나 있는데, 문자열 비교에서 NFC "한글"과 NFD "한글"은 다른 문자열이다. 서버에 업로드한 파일명 매칭이 안 되거나 스크립트에서 파일을 못 찾는 버그가 이 지점에서 나온다. 화면에 똑같이 보여서 원인을 찾기가 유난히 고약하다.
결론 — 뭘 바꾸면 되나
맥 자소분리 예방은 도구 하나 바꾸는 걸로 끝난다. 맥에서 윈도우로 한글 파일을 보낼 일이 있다면, 압축을 Finder 우클릭 대신 Keka로 하면 된다. 실측으로 확인했듯 Keka는 압축 시점에 파일명을 NFC로 정규화하고 UTF-8 플래그까지 챙긴다. 반디집(유료)도 문서상 같은 방식이고, 압축 없이 보낼 때는 반디네이머로 이름을 먼저 변환하면 된다.
그리고 이런 종류의 문제를 만났을 때 "어느 도구가 좋다더라"에서 멈추지 말고 위 파이썬 스크립트처럼 직접 까보는 걸 권한다. zip 안에 뭐가 들었는지 확인하는 데 열 줄이면 충분하고, 한 번 확인해두면 다음부터는 도구를 의심하느라 시간을 쓸 일이 없다. 기기 사이를 오가는 파일 문제는 이름 말고 포맷에서도 터지는데, 아이폰 HEIC 사진이 회사 PC에서 안 열리던 문제를 겪었을 때도 결국 같은 교훈이었다 — 증상만 보고 파일이 깨졌다고 단정하지 말 것.