기본 콘텐츠로 건너뛰기

[2025-07-15] Mac, Window 파일 조합형식 차이 (Mac : NFD, Window: NFC)로 삽질한 썰!

Mac, Window 파일 조합형식 차이 (Mac : NFD, Window: NFC)

안녕하세요. 클스 입니다.


문제 상황

- 저는 회사에서 맥을 사용하고, 다른 사람은 윈도우를 사용합니다. 회사에 공유서버는 있어서 서로 공유서버를 통해 파일을 공유합니다.

- AI Bot를 만들고 있는데, 회사의 문서를 학습하여 질문에 답변을 제공하는 기능입니다.

- 문서(docx, pdf, txt)를 가져와서 Vector Store에 학습하는데, 문서가 자주 변하니 필요한 문서만 학습하도록 하기 위해, 로컬 폴더를 SQLite로 DB활르 했습니다.

- 폴더를 읽어 문서를 files 테이블에 넣고 이렇게 검색하니 잘 나왔습니다.

SELECT *

FROM vector_store_files vs

LEFT JOIN vector_store_files f ON vs.vs_id = f.vs_id

WHERE LOWER(f.file_name) LIKE '%취업%'

;


- 취업에 관련된 문서가 전부 나왔는데, 취업.doc 파일이 보이기에, 저는 맥에서 워드를 열어서 docx로 저장했습니다.


- 그리고 개발한 update_vector_store_file 을 이용해 취업.doc 파일을 취업.docx 파일로 교체해서 vector store에 추가했습니다. 물론 DB도 업데이트가 되었겠죠?


- 확인해보려고 아래 쿼리를 실행하니 결과가 나오지 않습니다. 파일이 안올라갔나? 내가 쿼리를 잘못 만들었나? 데이터 소스를 잘못 설정했나? 갖가지 고민을 했습니다.


SELECT *

FROM vector_store_files vs

LEFT JOIN vector_store_files f ON vs.vs_id = f.vs_id

WHERE LOWER(f.file_name) LIKE '%취업%'

;


- 그래서 docx 파일을 Remote Desktop으로 윈도우에 복사, 붙혀넣기를 해보니 ㅊ ㅜ ㅣ ㅇ ㅓ ㅂ.docx  두둥 

까맣게 잊고 있던 NFC, NFD 조립형식의 문제인걸 알았습니다.


- 아무생각없이 맥으로 열어서 저장하면 NFC --> NFD 형식으로 변경되어 겉으로 보기엔 같지만, 내부 저장되는 파일명은 달라지는 것이었습니다. 


해결 방법:


아래와 같이 체크하고, 변환하는 함수를 만들어, vector store에 올리기 전에 NFD -> NFC로 변환했습니다.

일관성만 유지하면 되는데.. 왜 NFC(윈도우)로 했느냐? 이유는 다수가 윈도우를 사용하기 때문입니다.


import unicodedata

#######################################################
# 유틸리티 함수들
#####################################################
def _check_normalization(text: str) -> str:
"""
주어진 문자열의 유니코드 정규화 형식을 판별합니다.

Args:
text (str): 확인할 문자열

Returns:
str: 'NFC', 'NFD', 또는 'Neither' 중 하나를 반환합니다.
ASCII 문자열처럼 NFC와 NFD 형식이 동일한 경우 'NFC'를 우선 반환합니다.
"""
if text == unicodedata.normalize('NFC', text):
return 'NFC'
elif text == unicodedata.normalize('NFD', text):
return 'NFD'
else:
return 'Neither'

def normalize_filenames_in_directory(root_dir: str, dry_run: bool = True):
"""
지정된 디렉토리와 그 하위의 모든 파일/폴더명 중 NFD 형식인 것을
NFC 형식으로 재귀적으로 변경합니다.

Args:
root_dir (str): 시작할 최상위 디렉토리 경로.
dry_run (bool): True이면 실제 변경 없이 시뮬레이션만 실행합니다.
"""
if not os.path.isdir(root_dir):
print(f"오류: '{root_dir}'는 유효한 디렉토리가 아닙니다.")
return

if dry_run:
print("--- [Dry Run] 모드로 실행합니다. 실제 파일명은 변경되지 않습니다. ---")
else:
print("--- [Live] 모드로 실행합니다. 실제 파일명이 변경됩니다. ---")
user_input = input(f"'{root_dir}' 내부의 모든 파일/폴더명을 변경합니다. 계속하시겠습니까? (yes/no): ")
if user_input.lower() != 'yes':
print("작업을 취소했습니다.")
return

# 깊이 우선 탐색을 위해 os.walk의 topdown=False 사용
# 이렇게 하면 하위 디렉토리부터 처리하여 경로 문제를 방지할 수 있습니다.
for dirpath, dirnames, filenames in os.walk(root_dir, topdown=False):

# 1. 파일명 변경
for filename in filenames:
# NFD 형식인지 확인
if filename != unicodedata.normalize('NFC', filename):
new_filename = unicodedata.normalize('NFC', filename)
old_path = os.path.join(dirpath, filename)
new_path = os.path.join(dirpath, new_filename)

# 이름 충돌 처리
counter = 1
base_name, extension = os.path.splitext(new_filename)
while os.path.exists(new_path):
new_filename_with_suffix = f"{base_name}_normalized_{counter}{extension}"
new_path = os.path.join(dirpath, new_filename_with_suffix)
counter += 1

print(f"[파일 변경] '{old_path}' -> '{new_path}'")
if not dry_run:
try:
os.rename(old_path, new_path)
except OSError as e:
print(f" ㄴ 오류: 파일명 변경 실패 - {e}")

# 2. 디렉토리명 변경
for dirname in dirnames:
if dirname != unicodedata.normalize('NFC', dirname):
new_dirname = unicodedata.normalize('NFC', dirname)
old_path = os.path.join(dirpath, dirname)
new_path = os.path.join(dirpath, new_dirname)

# 이름 충돌 처리
counter = 1
while os.path.exists(new_path):
new_path = os.path.join(dirpath, f"{new_dirname}_normalized_{counter}")
counter += 1

print(f"[폴더 변경] '{old_path}' -> '{new_path}'")
if not dry_run:
try:
os.rename(old_path, new_path)
except OSError as e:
print(f" ㄴ 오류: 폴더명 변경 실패 - {e}")

print("--- 모든 작업이 완료되었습니다. ---")




물론 일괄적으로 바꿀수도 있고, vector store에 파일을 올릴때 파일경로, 파일명을 변환해서 올려도 됩니다.

실제 적용은 이 방법으로 했습니다. 


많은 것을 머리에 담고 이런 상황을 사전에 인지하고 있었다면 쉽게 해결되었겠지만,

익숙함에 이런건 다 잊어 먹고 살아 가네요.


당연한 말이겠지만, 검색어를 nfc, nfd 두개를 만들어서 SQL에 or 로 둘다 조회하면 된다.


감사합니다.



댓글

이 블로그의 인기 게시물

[quaser.dev][2014-09-14] 윈도우즈(10, 64bit)에 개발환경 설정하기

[quaser.dev][2014-09-14] 윈도우즈(10, 64bit)에 개발환경 설정하기

[2024-10-19] iPhone, iPad에서 ChatGPT로 PDF 생성시 한글 깨짐 해결 방법

iPhone, iPad에서 ChatGPT로 PDF 생성 시 한글 깨짐 해결 방법

[2025-04-16(수)] OpenAI gpt-4.1 시리즈 발표, Anthropic Claude에 대한 생각

OpenAI gpt-4.1 시리즈 발표, Anthropic Claude에 대한 생각 안녕하세요. 클스 입니다. 4/15일자로 openai가 gpt-4.1 시리즈를 발표 했습니다. 현재는 api로만 사용가능합니다. 점차 웹/앱 사용자에게 오픈 될거라 생각 됩니다. 비용상 문제로 4.1-mini, nano를 사용해서 chatbot을 만들어 보고 있습니다. 4o 시리즈 보다는 확실히 빠르고, 답변의 정확도는 올라간 것 같습니다. 앤트로픽 클로드와 비교를 많이 하는데, 업무 시스템 혹은 AI 솔루션을 개발하는 입장에서는 어떤 생태계를 제공하는가가 주요한 결정 입니다. AI관련 인력을 충분히 보유한 회사의 경우는 어떤걸 사용해도 좋을 결과를 가지겠지만 일반적인 챗봇 개발 절차를 보면 다음과 같이 볼 수 있습니다. 1. 문서를 준비한다. 대부분 pdf, text, markdown 2. 문서를 파싱해서 vectordb에 올린다.     - 별도 벡터디비 구성 필요. 어떤 db를 선택할지 고민 필요     - 어떤 Parser를 사용할지, 텍스트 오버래핑은 얼마가 적당한지 고민 필요        (회사의 문서가 워낙 많고, 다양하면 하나하나 테스트 해서 좋은걸 선택하는 것이 어렵다)     - 유사도 측정은 어떤 알고리즘을 써야할지 고민 필요     - llamaindex도 고민해야 함. 3. RAG flow를 만든다.     - langchain을 쓸지, 각 AI 벤더에서 제공하는 sdk를 쓸지 고민 필요       (대부분 락인이 되지 않으려면 langchain을 사용하면 좋지만, 벤더에 특화면 기능 적용이 늦음) 4. 챗봇 UI 앱을 만든다.     - 답변이 text 로 구성되다 보니. 그래프, 이미지등 복합적인 컨텐츠를 재배치 하여 표현하기 상당히 어렵네요. (이건 제가 실력이 모자라서 .. 패스) ...