PHP에서 문자열을 자르는 작업은 일상적이다. 그런데 한글, 일본어, 중국어 같은 멀티바이트 문자를 다룰 때 substr()을 썼다가 문자가 깨지는 경험을 해봤을까? 대부분의 개발자는 이 문제가 왜 발생하는지, substr()mb_substr()의 정확한 차이가 뭔지 모른 채 인터넷에서 찾은 답을 그냥 적용한다.

이번에는 바이트 단위와 문자 단위의 차이, 왜 멀티바이트 문자에서 substr()이 망가지는지, 그리고 실무에서 언제 어느 함수를 써야 하는지 완벽하게 정리해서 소개하겠다.

 

1단계. substr()과 mb_substr()의 근본적 차이

substr()은 문자열을 바이트 단위로 센다. 반면 mb_substr()은 문자 단위로 센다. 이 차이가 멀티바이트 문자에서 얼마나 치명적인지 보자.

한글 한 글자는 UTF-8 인코딩에서 3바이트를 차지한다. "안녕하세요"라는 5글자 문자열은 실제로는 15바이트다. substr()으로 앞에서 3바이트를 잘라내면 "안" 한 글자만 남지만, 4바이트를 자르려고 하면 "안"의 일부 바이트만 자르게 되어 완전히 깨진 문자가 나타난다.

 

substr() - 바이트 단위 자르기
<?php
// ✗ 잘못된 코드
$text = "안녕하세요";
echo substr($text, 0, 5);
// 출력: 안녕
// 왜? 한글은 3바이트씩이므로 0~2바이트("안"), 3~5바이트("녕"의 일부)
// 결과: "안녕" 같아 보이지만 사실 뒤에 깨진 문자가 섞여 있을 수 있음

echo substr($text, 0, 7);
// 출력: 안녕?? (깨진 문자)
// 왜? 7바이트는 "녕"(3~5바이트)까지 완성되고 6~7바이트만 자르기 때문에
// "하"의 일부(3바이트 중 2바이트)만 포함되어 깨짐
?>

 

mb_substr() - 문자 단위 자르기
<?php
// ✓ 올바른 코드
$text = "안녕하세요";
echo mb_substr($text, 0, 2);
// 출력: 안녕
// 왜? "안"(1번째 문자), "녕"(2번째 문자) = 2글자
// 인코딩을 고려해서 정확히 2글자만 자음

echo mb_substr($text, 1, 3);
// 출력: 녕하세
// 왜? 1번째 문자("녕")부터 3글자 = "녕" + "하" + "세"
?>

 

2단계. 실무에서 자주 사용하는 시나리오

멀티바이트 문자를 다루는 상황을 구체적으로 보자. API 응답 텍스트를 요약하거나, 사용자 입력을 제한하는 등의 작업에서 이 차이가 드러난다.

 

시나리오 1. 게시물 제목 30글자까지만 표시하기
<?php
$title = "PHP 멀티바이트 문자 처리 완벽 가이드 - 한글/일본어/중국어까지";

// ✗ 잘못된 코드
echo substr($title, 0, 30) . "...";
// 출력: PHP 멀티바이트 문자 처리 완벽 가이 (30글자가 아니라 30바이트)
// 뒤에 깨진 문자가 섞여 있을 가능성 높음

// ✓ 올바른 코드
echo mb_substr($title, 0, 30) . "...";
// 출력: PHP 멀티바이트 문자 처리 완벽 가이드 - 한글/일본어/중국어까  
// 정확히 30글자
?>

 

시나리오 2. 문자열 끝에서 특정 길이만큼 자르기
<?php
$phone = "01012345678";
$nickname = "김철수";

// ✗ 잘못된 코드 - 한국 번호는 ASCII이므로 우연히 작동하지만
// 닉네임은 망가짐
echo substr($phone, -4);      // "5678" (우연히 맞음)
echo substr($nickname, -2);   // "?" (깨진 문자)

// ✓ 올바른 코드
echo mb_substr($phone, -4);   // "5678"
echo mb_substr($nickname, -2); // "수" (마지막 2글자)
?>

 

3단계. 멀티바이트 함수 모음 - 실무 필수 세트

PHP의 mb_* 함수들은 substr()뿐만 아니라 strlen(), strpos() 등도 멀티바이트 문자를 올바르게 처리한다. 어떤 함수들이 있는지 표로 정리했다.

일반 함수 멀티바이트 함수 차이점
strlen() mb_strlen() strlen은 바이트 수, mb_strlen은 글자 수
substr() mb_substr() substr은 바이트 단위 자르기, mb_substr은 문자 단위
strpos() mb_strpos() strpos는 바이트 위치, mb_strpos는 글자 위치
strtoupper() mb_strtoupper() 일부 멀티바이트 문자의 대문자 변환 지원
strtolower() mb_strtolower() 일부 멀티바이트 문자의 소문자 변환 지원
str_replace() mb_ereg_replace() 정규표현식 기반 멀티바이트 문자 치환

 

4단계. 멀티바이트 모드 설정 필수

mb_* 함수를 쓰려면 반드시 인코딩을 명시해야 한다. 기본값이 php.ini의 mbstring.internal_encoding 설정값을 따르는데, 명시적으로 지정하는 게 안전하다.

 

인코딩 미지정 시 발생하는 문제
<?php
// ✗ 잘못된 코드
$text = "안녕하세요";
echo mb_strlen($text);
// 출력: 5 또는 15 (php.ini 설정에 따라 불확실)
// 왜? 인코딩이 명시되지 않아 시스템 설정을 따름

// ✓ 올바른 코드
echo mb_strlen($text, 'UTF-8');  // 5 (글자 수)
echo mb_substr($text, 0, 2, 'UTF-8'); // "안녕" (정확히 2글자)
?>

 

5단계. 주의사항 - 성능과 호환성

mb_* 함수는 항상 더 느리다. 바이트를 세는 것보다 멀티바이트 문자를 식별하고 글자를 세는 게 연산 비용이 더 크기 때문이다. 따라서 대용량 데이터를 처리할 때는 필요한 부분에만 선택적으로 사용하자.

또한 mb_* 함수를 쓰려면 PHP에 mbstring 확장이 설치되어 있어야 한다. 대부분의 호스팅 서비스에는 기본 포함되지만, 혹시 모르니 다음과 같이 확인하자.

<?php
// mbstring 확장 확인
if (extension_loaded('mbstring')) {
    echo "mbstring 설치됨";
} else {
    echo "mbstring 설치 안 됨 - 호스팅사에 문의";
}
?>

 

실무 팁. 바이트 제한이 필요한 경우
<?php
// 데이터베이스 VARCHAR(50)에 한글만 저장해야 한다면?
// UTF-8에서 한글은 3바이트이므로 50바이트 = 최대 16글자

$text = "안녕하세요반갑습니다감사합니다";
$maxBytes = 50;
$maxChars = intdiv($maxBytes, 3); // 대략적인 계산

// ✗ 부정확한 코드
echo substr($text, 0, $maxBytes);
// 끝에 깨진 문자가 섞여 있을 수 있음

// ✓ 올바른 코드
while (strlen($text) > $maxBytes) {
    $text = mb_substr($text, 0, -1, 'UTF-8');
}
echo $text; // 안전하게 50바이트 이내로 자름
?>

 

마무리 - substr() vs mb_substr() 선택의 기준

멀티바이트 문자 처리는 국제화된 서비스에서 피할 수 없는 숙제다. 한글, 일본어, 중국어, 이모지 등 다양한 인코딩을 다루는 프로젝트라면 처음부터 mb_substr()을 기본으로 삼자. 이 작은 습관이 모여서 버그 없는 멀티바이트 문자 처리라는 큰 효과를 만든다는 점을 잊지 말자.

이 글의 "시나리오 1, 2"와 "성능 팁" 부분을 참고해서 본인의 프로젝트에 맞는 함수를 선택하고, 반드시 인코딩을 명시해서 사용하면 한글 깨짐 문제는 완벽히 해결할 수 있을 것이다.