Mac에서 올린 한글 파일명(NFD 정규화) 문제

이전 유니코드를 다뤘던 글을 올렸을 때 모던 PHP 그룹에서 댓글로 잠시 언급되었던,

NFD 정규화 문제.

맥에서 올린 파일이 다운로드 안된다는 VOC(고객의 소리)가 들어와서 원인 파악 겸 정리해봤다.

맥에서 ‘한글.txt’ 파일을 올리고 윈도에서 다운로드 받으면 ‘ㅎㅏㄴㄱㅡㄹ.txt’로 보이게된다.

유니코드 상에서 한글을 표현하는 방법은 첫가끝(처음가운데끝)이라고도 하는 한글 자모를 조합하는 방식과 완성형으로 표시하는 방법 등이 있다.

맥에서는 파일명을 저장할 때 NFD 방식. 즉, 한글 자모를 따로따로 받아 조합하는 방식으로 정규화를 하는데,

이 현상에 관해선 다른 분들이 열심히 글을 써주셨으니 더이상 설명하진 않겠다.

Normalizer Class 설치

PHP 5.3부터 Normalizer Class를 사용할 수 있는데,

Internationalization Functions에 포함되어 이 확장 모듈을 설치해야 한다.

  • 이건 ICU 라이브러리의 wrapper인 관계로 ICU를 먼저 설치해야할 수 있다(libicu-devel , libicu 등)

  • Windows 시스템에선 php_intl.dll을 구해야하고, XAMPP를 쓴다면 모듈은 이미 ext에 존재할 것이다.

  • Linux 기반 시스템이라면

    • apt-get install php-intl (for ubuntu-based linux)
    • yum install php-intl (for CentOS)
    • php7.x-intl 등 php 버전에 따라 다를 수 있으니 알맞게 설치할 것
  • Mac에서 brew를 사용한다면 brew install php71-intl

이후 php.ini에서 extension=php_intl.(dll|so)을 활성화 시켜야 한다. 즉, 맨 앞의; 제거하기.

처리 방법

잘 설치됐는지 확인해보자.

<?php

function testNormalizer($str)
{
echo '-------------------------' . PHP_EOL;
$urlencodedInitialStr = urlencode($str);

echo $str . PHP_EOL;

echo ( Normalizer::isNormalized($str, Normalizer::FORM_C) ) ? "normalized : FORM_C" : "not normalized : FORM_C";
echo PHP_EOL;
echo ( Normalizer::isNormalized($str, Normalizer::FORM_D) ) ? "normalized : FORM_D" : "not normalized : FORM_D";
echo PHP_EOL;

$str = Normalizer::normalize($str, Normalizer::FORM_C);
echo 'normalize to FORM_C : ' . $str . PHP_EOL;

echo 'urlencoded before: ' . $urlencodedInitialStr . PHP_EOL;
echo 'urlencoded after~: ' . urlencode($str) . PHP_EOL;
echo PHP_EOL;
}

$name1='recruit/recruit/201709/21/owlupr_58qh-1meg1s6_recruit.pdf';//일반 ASCII 문자열
$name2=urldecode('%E1%84%86%E1%85%A2%E1%86%A8%E1%84%8B%E1%85%A6%E1%84%89%E1%85%A5%E1%84%90%E1%85%A6%E1%84%89%E1%85%B3%E1%84%90%E1%85%B3_%E1%84%8C%E1%85%AE%E1%86%BC%E1%84%8B%E1%85%B5%E1%84%82%E1%85%A6.pdf');//NFD로 정규화된 파일명을 urlencode해서 받을 경우
$name3='테스트.pdf';//일반 한글 문자열

testNormalizer($name1);
testNormalizer($name2);
testNormalizer($name3);

결과는

-------------------------
recruit/recruit/201709/21/owlupr_58qh-1meg1s6_recruit.pdf
normalized : FORM_C
normalized : FORM_D
normalize to FORM_C : recruit/recruit/201709/21/owlupr_58qh-1meg1s6_recruit.pdf
urlencoded before: recruit%2Frecruit%2F201709%2F21%2Fowlupr_58qh-1meg1s6_recruit.pdf
urlencoded after~: recruit%2Frecruit%2F201709%2F21%2Fowlupr_58qh-1meg1s6_recruit.pdf

-------------------------
맥에서테스트_중이네.pdf
not normalized : FORM_C
normalized : FORM_D
normalize to FORM_C : 맥에서테스트_중이네.pdf
urlencoded before: %E1%84%86%E1%85%A2%E1%86%A8%E1%84%8B%E1%85%A6%E1%84%89%E1%85%A5%E1%84%90%E1%85%A6%E1%84%89%E1%85%B3%E1%84%90%E1%85%B3_%E1%84%8C%E1%85%AE%E1%86%BC%E1%84%8B%E1%85%B5%E1%84%82%E1%85%A6.pdf
urlencoded after~: %EB%A7%A5%EC%97%90%EC%84%9C%ED%85%8C%EC%8A%A4%ED%8A%B8_%EC%A4%91%EC%9D%B4%EB%84%A4.pdf

-------------------------
테스트.pdf
normalized : FORM_C
not normalized : FORM_D
normalize to FORM_C : 테스트.pdf
urlencoded before: %ED%85%8C%EC%8A%A4%ED%8A%B8.pdf
urlencoded after~: %ED%85%8C%EC%8A%A4%ED%8A%B8.pdf

소스에는 아래와 같이 적용 해놓으면 된다.

if (class_exists('Normalizer')) {
if (Normalizer::isNormalized($filename, Normalizer::FORM_D)) {
$filename = Normalizer::normalize($filename, Normalizer::FORM_C);
}
}

파일명을 DB에 입력할 때 NFC로 넣는 것이 깔끔하겠지만,
이미 NFD 정규화된 파일명이 들어가있다면, 다운로드 시에도 파일명을 바꿔주면 된다.

한숨

이 밖에 파일 다운로드 관련 코드를 보면 별별 예외처리가 많이 들어가 세월의 흔적을 느낄 수 있다.

IE라면 파일명을 또 MS949로 변경해줘야 해야만 한다.
OS 별로 파일명에 들어갈 수 있는 특수문자도 다르다.
기타 등등.

특정 OS는 점유율이 낮아서, 시스템이 낡아서, 인력이 부족해서, 개발자 역량이 부족해서, 지금은 더 급한 게 있어서…
cross-platform 대응은 항상 어렵다.

참고