Language/PHP
[PHP] 워드·아웃룩·엑셀에서 복사한 유니코드 특수문자 처리 오류와 PHP 변환 함수
워드·아웃룩·엑셀에서 복사한 유니코드 특수문자 처리 오류와 PHP 변환 함수
최근 워드(Word), 아웃룩(Outlook), 엑셀(Excel) 등에서 작성된 문서를 시스템에 입력하거나 UTF-8로 저장한 후, 일부 특수문자 때문에 오류가 발생하는 현상을 확인하였다.
대표적으로 아래와 같은 문자가 문제가 되었다.
⓵ ⓶ ⓷
a b c
A B C
가 나 다
(가) (나) (다)
– — “ ” ‘ ’ …
처음에는 “유니코드와 UTF-8 간에 매핑되지 않는 문자가 있어서 발생하는 문제”로 생각할 수 있다.
하지만 정확히는 UTF-8 자체의 문제라기보다, UTF-8로 저장된 일부 유니코드 특수문자가 이후 CP949, EUC-KR, ANSI 기반 시스템이나 레거시 DB, 구형 컴포넌트, 리포트 출력 모듈 등에서 처리될 때 발생하는 호환성 문제로 보는 것이 맞다.
1. 발생 원인
유니코드(Unicode)는 전 세계 문자를 표현하기 위한 문자 표준이고, UTF-8은 유니코드 문자를 저장하기 위한 인코딩 방식이다.
따라서 ⓵ 같은 문자도 UTF-8에서는 정상적으로 표현된다.
문제는 해당 문자를 처리하는 다음 단계에서 발생한다.
예를 들어 다음과 같은 상황이다.
Word / Outlook / Excel
↓
UTF-8 문자열 저장
↓
PHP / DB / 리포트 / 메일 / 레거시 컴포넌트 처리
↓
CP949 또는 ANSI 기반 변환·출력
↓
표현 불가 문자로 인한 오류 발생
즉, 문제의 핵심은 다음과 같다.
UTF-8에 없는 문자가 있어서가 아니라,
후속 처리 시스템이 해당 유니코드 문자를 처리하지 못해서 발생한다.
예를 들어 ⓵와 1은 비슷하게 보이지만 서로 다른 문자이다.
⓵ : Double Circled Digit One
1 : Circled Digit One
화면에서는 거의 비슷하게 보일 수 있으나, 내부 문자 코드는 다르다.
따라서 특정 시스템에서는 1은 정상 처리되지만 ⓵은 오류가 발생할 수 있다.
2. 해결 방향
해결 방법은 여러 가지가 있다.
- 전체 시스템을 UTF-8 기준으로 정비한다.
- DB 문자셋(Character Set), 컬럼 타입, 커넥션 인코딩을 점검한다.
- 입출력 모듈에서 문자 인코딩을 명확히 지정한다.
- 문제가 되는 특수문자를 사전에 안전한 문자로 치환한다.
가장 근본적인 방법은 전체 시스템을 UTF-8 기준으로 정비하는 것이다.
하지만 기존 시스템, 구형 DB, 리포트 모듈, 외부 연계 시스템 등이 CP949 또는 ANSI 기반으로 동작하는 경우에는 현실적으로 한 번에 모두 수정하기 어렵다.
그래서 이번에는 문제가 되는 유니코드 특수문자를 CP949 호환 문자 또는 일반 텍스트 형태로 강제 변환하는 방식을 적용하였다.
3. 변환 기준
변환 기준은 다음과 같다.
| 구분 | 원본 예시 | 변환 예시 |
|---|---|---|
| 이중 원숫자 | ⓵ |
1 |
| 원문자 소문자 | a |
a) |
| 원문자 대문자 | A |
A) |
| 원문자 한글 | 가 |
가) |
| 괄호 한글 | (가) |
가) |
| 긴 대시 | –, —, ― |
- |
| 스마트 따옴표 | “”, ‘’ |
", ' |
| 말줄임표 | … |
... |
| 글머리 기호 | • |
- |
| 원화 기호 | ₩ |
₩ |
4. PHP 변환 함수
아래 함수는 UTF-8 문자열을 입력받아, 문제가 될 수 있는 일부 유니코드 특수문자를 안전한 문자로 변환한다.
/**
* 유니코드 특수문자를 CP949 호환 또는 안전한 형태로 변환
*
* - UTF-8 자체의 문제를 해결하는 함수가 아니라,
* CP949/ANSI/레거시 시스템에서 문제가 될 수 있는 문자를 사전 치환하는 용도이다.
*
* @param string|null $text UTF-8 문자열
* @return string|null 변환된 문자열
*/
function getUnicodeChar2SafeChar($text)
{
if ($text === null || $text === '') {
return $text;
}
$map = array(
// Double Circled Digit → 일반 Circled Digit
'⓵' => '1',
'⓶' => '2',
'⓷' => '3',
'⓸' => '4',
'⓹' => '5',
'⓺' => '6',
'⓻' => '7',
'⓼' => '8',
'⓽' => '9',
'⓾' => '10',
// 원문자 소문자 → 일반 텍스트
'a' => 'a)', 'b' => 'b)', 'c' => 'c)', 'd' => 'd)', 'e' => 'e)',
'f' => 'f)', 'g' => 'g)', 'h' => 'h)', 'i' => 'i)', 'j' => 'j)',
'k' => 'k)', 'l' => 'l)', 'm' => 'm)', 'n' => 'n)', 'o' => 'o)',
'p' => 'p)', 'q' => 'q)', 'r' => 'r)', 's' => 's)', 't' => 't)',
'u' => 'u)', 'v' => 'v)', 'w' => 'w)', 'x' => 'x)', 'y' => 'y)',
'z' => 'z)',
// 원문자 대문자 → 일반 텍스트
'A' => 'A)', 'B' => 'B)', 'C' => 'C)', 'D' => 'D)', 'E' => 'E)',
'F' => 'F)', 'G' => 'G)', 'H' => 'H)', 'I' => 'I)', 'J' => 'J)',
'K' => 'K)', 'L' => 'L)', 'M' => 'M)', 'N' => 'N)', 'O' => 'O)',
'P' => 'P)', 'Q' => 'Q)', 'R' => 'R)', 'S' => 'S)', 'T' => 'T)',
'U' => 'U)', 'V' => 'V)', 'W' => 'W)', 'X' => 'X)', 'Y' => 'Y)',
'Z' => 'Z)',
// 원문자 한글 → 일반 텍스트
'가' => '가)', '나' => '나)', '다' => '다)', '라' => '라)', '마' => '마)',
'바' => '바)', '사' => '사)', '아' => '아)', '자' => '자)', '차' => '차)',
'카' => '카)', '타' => '타)', '파' => '파)', '하' => '하)',
// 괄호문자 한글 → 일반 텍스트
'(가)' => '(가)', '(나)' => '(나)', '(다)' => '(다)', '(라)' => '(라)', '(마)' => '(마)',
'(바)' => '(바)', '(사)' => '(사)', '(아)' => '(아)', '(자)' => '(자)', '(차)' => '(차)',
'(카)' => '(카)', '(타)' => '(타)', '(파)' => '(파)', '(하)' => '(하)',
// 원문자 자음 → 일반 텍스트
'ᄀ' => 'ᄀ)', 'ᄂ' => 'ᄂ)', 'ᄃ' => 'ᄃ)', 'ᄅ' => 'ᄅ)', 'ᄆ' => 'ᄆ)',
'ᄇ' => 'ᄇ)', 'ᄉ' => 'ᄉ)', 'ᄋ' => 'ᄋ)', 'ᄌ' => 'ᄌ)', 'ᄎ' => 'ᄎ)',
'ᄏ' => 'ᄏ)', 'ᄐ' => 'ᄐ)', 'ᄑ' => 'ᄑ)', 'ᄒ' => 'ᄒ)',
// 괄호문자 자음 → 일반 텍스트
'(ᄀ)' => '(ᄀ)', '(ᄂ)' => '(ᄂ)', '(ᄃ)' => '(ᄃ)', '(ᄅ)' => '(ᄅ)', '(ᄆ)' => '(ᄆ)',
'(ᄇ)' => '(ᄇ)', '(ᄉ)' => '(ᄉ)', '(ᄋ)' => '(ᄋ)', '(ᄌ)' => '(ᄌ)', '(ᄎ)' => '(ᄎ)',
'(ᄏ)' => '(ᄏ)', '(ᄐ)' => '(ᄐ)', '(ᄑ)' => '(ᄑ)', '(ᄒ)' => '(ᄒ)',
// 자주 문제되는 기호류
'–' => '-',
'—' => '-',
'―' => '-',
'“' => '"',
'”' => '"',
'‘' => "'",
'’' => "'",
'…' => '...',
'•' => '-',
'·' => 'ᆞ',
'₩' => '₩',
);
return strtr($text, $map);
}
5. 사용 예시
$text = '⓵ 테스트 “문자열”입니다. 가 항목 – 설명 …';
$result = getUnicodeChar2SafeChar($text);
echo $result;
출력 결과는 다음과 같다.
1 테스트 "문자열"입니다. 가) 항목 - 설명 ...
6. 적용 시 주의사항
이 방식은 모든 유니코드 문제를 해결하는 범용 인코딩 변환 함수는 아니다.
특정 업무 시스템에서 자주 문제가 되는 문자를 사전에 정리하기 위한 실무형 보정 함수에 가깝다.
따라서 다음 사항을 함께 점검하는 것이 좋다.
- DB 문자셋(Character Set)
- DB 컬럼 타입
- PHP 파일 저장 인코딩
- 웹 페이지 charset 설정
- DB 커넥션 인코딩
- 메일 발송 인코딩
- 리포트 출력 모듈의 문자셋
- 외부 연계 시스템의 수신 인코딩
특히 기존 시스템이 CP949, EUC-KR, ANSI 기반으로 동작하는 경우에는 UTF-8 문자열을 저장하더라도 출력, 변환, 전송 단계에서 오류가 발생할 수 있다.
7. iconv 사용 시 참고
PHP에서는 iconv()를 사용하여 문자 인코딩을 변환할 수도 있다.
예를 들어 다음과 같은 방식이다.
$text = iconv('UTF-8', 'CP949//TRANSLIT//IGNORE', $text);
다만 //TRANSLIT 동작은 시스템의 iconv 구현 방식에 따라 결과가 달라질 수 있다.
따라서 특정 문자를 반드시 원하는 문자로 바꿔야 하는 경우에는 위와 같이 명시적인 매핑 테이블을 두는 방식이 더 예측 가능하다.
8. 정리
이번 문제는 UTF-8 자체의 한계라기보다, 워드·아웃룩·엑셀 등에서 복사된 유니코드 특수문자가 레거시 시스템 또는 CP949 호환 환경에서 처리되면서 발생한 문자 호환성 문제이다.
가장 좋은 해결책은 전체 시스템을 UTF-8 기준으로 정비하는 것이다.
하지만 기존 업무 시스템에서는 현실적으로 어려운 경우가 많기 때문에, 문제가 되는 문자를 사전에 안전한 문자로 치환하는 방식도 실무적으로 유용하다.
이번 함수는 PHP로 작성되었지만, 전체적인 개념은 C#, JavaScript, Python, Java 등 다른 언어에서도 동일하게 적용할 수 있다.
-----------
본 글은 직접 초안 작성 후 ChatGPT 5.5의 도움을 받아 완성하였습니다.
'Language > PHP' 카테고리의 다른 글
| [PHP]구글드라이브를 이용한 OCR 기능 (0) | 2021年09月09日 |
|---|---|
| [알고리즘] 날짜 - 이번주에 시작일과 마지막일 구하기 (0) | 2009年12月08日 |
| array_walk를 이용한 배열 함수 (0) | 2008年07月18日 |
'Language/PHP'의 다른글
- 이전글[PHP]구글드라이브를 이용한 OCR 기능
- 현재글[PHP] 워드·아웃룩·엑셀에서 복사한 유니코드 특수문자 처리 오류와 PHP 변환 함수