한글을 포함한 데이터를 다룰 때 문자 깨짐이나 인코딩 오류를 경험해봤을까? 특히 외부 API에서 받은 데이터나 사용자 입력값을 처리할 때 갑자기 '?'나 알 수 없는 기호로 변환되는 현상을 목격한 개발자라면 알 것이다. 다만 대부분은 그 원인이 정확히 뭔지, 왜 인코딩이 깨지는지, 어떻게 해야 하는지 제대로 알지 못한 채 인터넷에서 가져온 코드를 대충 복사해 붙인다. 이번에는 PHP에서 문자 인코딩 변환이 실패하는 정확한 원인, 언제 필요한지, 그리고 mb_convert_encoding으로 완벽하게 해결하는 방법을 차근차근 설명하겠다.
문자 인코딩은 '문자'라는 추상적인 개념을 컴퓨터가 이해할 수 있는 '숫자(바이트)로 변환하는 규칙'이다. 같은 '한'이라는 글자도 인코딩 방식에 따라 전혀 다른 바이트로 저장된다.
흔히 마주치는 인코딩 방식들:
- UTF-8: 가장 현대적인 표준. 문자마다 1~4바이트를 사용. 한글 한 글자는 보통 3바이트.
- EUC-KR: 한국에서 오래 쓰던 인코딩. 한글 한 글자는 2바이트. 일부 구형 시스템에서 아직도 사용.
- CP949: 윈도우의 한국어 인코딩. EUC-KR의 확장 버전.
- ISO-8859-1(Latin-1): 영문/서유럽 문자용. 한글 미지원.
웹 개발에서 대부분은 UTF-8을 사용하지만, 외부 API나 CSV 파일, 구형 데이터베이스에서는 다른 인코딩으로 데이터가 들어온다. 이때 변환을 놓치면 문자가 깨진다.
문자 깨짐은 이런 상황에서 발생한다:
| 상황 | 원인 | 증상 |
|---|---|---|
| 외부 API 응답 | API가 보낸 인코딩과 PHP 기본 인코딩이 다름 | '한글' → '????' |
| 파일 업로드 | 사용자 OS의 기본 인코딩(Windows는 CP949)으로 인코딩된 파일명 | 파일명 깨짐 |
| CSV/Excel 임포트 | Excel의 기본값(CP949/EUC-KR)과 웹서버(UTF-8) 불일치 | 한글 문자 깨짐 |
| 오래된 DB | MySQL이 EUC-KR로 설정, 하지만 PHP는 UTF-8 기대 | 저장/조회 시 '?'로 변환 |
| 수동 변환 누락 | 받은 데이터의 원래 인코딩을 명시하지 않음 | 모르는 기호 또는 ? |
PHP의 mb_convert_encoding() 함수는 문자열을 한 인코딩에서 다른 인코딩으로 변환한다. 기본 문법은 이렇다:
mb_convert_encoding($string, $to_encoding, $from_encoding);
- $string: 변환할 문자열
- $to_encoding: 변환할 대상 인코딩 (보통 'UTF-8')
- $from_encoding: 원본 인코딩 (예: 'EUC-KR', 'CP949', 'ISO-8859-1')
가장 흔한 사용 예시를 보자.
예제 1: Windows에서 업로드된 파일명 변환
✗ 잘못된 코드 - 인코딩을 명시하지 않음:
// 사용자가 Windows에서 "한글파일.txt"를 업로드
$filename = $_FILES['file']['name']; // "????.txt" 상태
echo $filename; // 화면에 깨진 글자 출력
✓ 올바른 코드 - 원본 인코딩을 CP949로 명시:
$filename = $_FILES['file']['name'];
// Windows 기본값인 CP949에서 UTF-8로 변환
$filename = mb_convert_encoding($filename, 'UTF-8', 'CP949');
echo $filename; // "한글파일.txt" 정상 출력
예제 2: 외부 API 응답이 EUC-KR로 올 때
✗ 잘못된 코드:
$response = file_get_contents('http://example-api.com/data?query=한글');
// API가 EUC-KR로 응답했는데 변환하지 않음
$data = json_decode($response, true);
echo $data['name']; // "????" 깨짐
✓ 올바른 코드:
$response = file_get_contents('http://example-api.com/data?query=한글');
// API 응답이 EUC-KR라면 UTF-8로 변환
$response = mb_convert_encoding($response, 'UTF-8', 'EUC-KR');
$data = json_decode($response, true);
echo $data['name']; // "한글" 정상 출력
예제 3: 원본 인코딩을 모를 때 - 자동 감지
✗ 잘못된 코드:
$unknown_string = '혼란스러운 데이터';
$converted = mb_convert_encoding($unknown_string, 'UTF-8'); // 원본 인코딩을 모름
echo $converted; // 예측 불가
✓ 올바른 코드 - mb_detect_encoding()으로 자동 감지:
$unknown_string = '혼란스러운 데이터';
// 자동으로 인코딩 감지 (감지 우선순위 지정)
$from_encoding = mb_detect_encoding(
$unknown_string,
['UTF-8', 'EUC-KR', 'CP949', 'ISO-8859-1'],
true
);
$converted = mb_convert_encoding($unknown_string, 'UTF-8', $from_encoding);
echo $converted; // 정상 출력
결과: UTF-8로 정상 변환됨
Excel에서 다운로드한 CSV는 보통 Windows의 기본 인코딩(CP949)으로 저장된다. 이를 웹에 올바르게 표시하려면:
<?php
// CSV 파일 읽기
if (($handle = fopen('upload/data.csv', 'r')) !== false) {
$data = [];
while (($row = fgetcsv($handle)) !== false) {
// CSV의 각 셀이 CP949로 인코딩되어 있음
// UTF-8로 변환
$converted_row = array_map(function($cell) {
return mb_convert_encoding($cell, 'UTF-8', 'CP949');
}, $row);
$data[] = $converted_row;
}
fclose($handle);
}
// 결과: 한글이 정상적으로 표시됨
foreach ($data as $row) {
echo $row[0] . ' | ' . $row[1] . '<br />';
}
?>
실행 결과: 원본 CSV의 "김철수|서울"이 정상 출력됨
실수 1: mb_ 함수를 쓰기 전에 라이브러리 확인 안 함
✗ 잘못된 코드:
// mb_convert_encoding 함수가 없을 수도 있음
$result = mb_convert_encoding($string, 'UTF-8', 'EUC-KR');
✓ 올바른 코드:
// 함수 존재 확인
if (!extension_loaded('mbstring')) {
die('Error: mbstring extension not loaded');
}
$result = mb_convert_encoding($string, 'UTF-8', 'EUC-KR');
실수 2: 이미 UTF-8인 문자열을 다시 변환
✗ 잘못된 코드:
$string = '이미 UTF-8인 한글';
// 다시 UTF-8로 변환하면 깨질 수 있음
$result = mb_convert_encoding($string, 'UTF-8', 'EUC-KR');
echo $result; // 예측 불가능한 결과
✓ 올바른 코드:
$string = '이미 UTF-8인 한글';
// 먼저 원본 인코딩을 확인
$detected = mb_detect_encoding($string, ['UTF-8', 'EUC-KR'], true);
if ($detected !== 'UTF-8') {
$string = mb_convert_encoding($string, 'UTF-8', $detected);
}
echo $string; // 항상 정상 출력
실수 3: 인코딩 이름 오타
✗ 잘못된 코드:
// 'UTF8'은 잘못된 이름
$result = mb_convert_encoding($string, 'UTF8', 'EUC-KR');
✓ 올바른 코드:
// 정확한 인코딩명 (하이픈 포함)
$result = mb_convert_encoding($string, 'UTF-8', 'EUC-KR');
// 또는 'utf-8', 'euc-kr'도 동작 (대소문자 무관)
매번 mb_convert_encoding을 쓰기보다는, 프로젝트 입구에서 일괄 처리하는 게 낫다:
<?php
// config.php 또는 index.php 맨 처음
header('Content-Type: text/html; charset=utf-8');
mb_internal_encoding('UTF-8');
mb_http_input('UTF-8');
mb_http_output('UTF-8');
// 모든 $_GET, $_POST 데이터를 자동으로 UTF-8로 변환
foreach ($_POST as $key => $value) {
$_POST[$key] = mb_convert_encoding($value, 'UTF-8', mb_detect_encoding($value, ['UTF-8', 'EUC-KR', 'CP949'], true));
}
foreach ($_GET as $key => $value) {
$_GET[$key] = mb_convert_encoding($value, 'UTF-8', mb_detect_encoding($value, ['UTF-8', 'EUC-KR', 'CP949'], true));
}
?>
문자 인코딩 변환은 국제화 웹 개발에서 필수다. mb_convert_encoding()은 단순해 보이지만, 원본 인코딩을 정확히 알고 사용해야만 제대로 작동한다.
체크리스트:
- ✓ 데이터의 원본 인코딩이 뭔지 명확히 파악했는가?
- ✓ mb_detect_encoding으로 자동 감지하거나, API 문서에서 명시된 인코딩을 확인했는가?
- ✓ 변환 후에는 정말 UTF-8인지 테스트했는가?
- ✓ 프로젝트 입구(config/index)에서 일괄 처리하도록 구조화했는가?
이 글의 예제 2(API 응답 변환)와 예제 3(자동 감지)를 참고해서 지금 당신의 프로젝트에서 문자가 깨지는 부분을 찾아내고 mb_convert_encoding을 적용하면, 한글 포함 모든 인코딩 문제를 깔끔하게 해결할 수 있을 것이다.