이전 유니코드를 다뤘던 글을 올렸을 때 모던 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'; $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'); $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 대응은 항상 어렵다.
참고