먼저 용어를 분리하자
문자열 깨짐의 원인은 대부분 용어 혼동에서 시작됩니다.
| 용어 | 의미 |
|---|
| 문자 집합(Character Set) | 어떤 문자를 다루는가 (예: ASCII, Unicode) |
| 코드 포인트(Code Point) | 문자에 부여된 번호 (예: U+AC00) |
| 인코딩(Encoding) | 그 번호를 바이트로 저장하는 방식 (UTF-8, UTF-16 등) |
핵심:
- Unicode는 "인코딩"이 아니라 문자 체계(번호 체계)입니다.
- UTF-8/UTF-16은 Unicode 코드 포인트를 저장하는 인코딩 방식입니다.
ASCII
- 7비트(0~127) 문자 집합.
- 영어/기호 중심이며 현대 다국어 서비스에는 부족합니다.
- ASCII는 UTF-8의 앞부분과 호환됩니다(0~127은 동일 바이트 값).
MBCS / 코드 페이지 기반 방식의 한계
- 과거 윈도우 로컬 코드 페이지(예: CP949)는 환경 의존성이 큽니다.
- 같은 바이트라도 코드 페이지가 다르면 다른 문자로 해석됩니다.
- 따라서 파일/네트워크/DB를 오갈 때 깨짐(mojibake)이 자주 발생합니다.
예시 문제:
- 서버는 UTF-8, 클라이언트는 CP949로 해석 -> 한글 깨짐
- 개발자 PC마다 기본 코드 페이지가 달라 재현이 어려운 버그 발생
Unicode + UTF-8/UTF-16 비교
| 항목 | UTF-8 | UTF-16 |
|---|
| 단위 | 1바이트 코드 유닛 | 2바이트 코드 유닛 |
| 영어(ASCII) | 1B | 2B |
| 한글(대부분 BMP) | 3B | 2B |
| 이모지/일부 확장 문자 | 4B | 4B(서로게이트 페어) |
| 장점 | 웹/파일/네트워크 표준, ASCII 호환 | BMP 문자 처리 효율, 일부 플랫폼 API 친화 |
중요 포인트:
- UTF-16도 항상 2바이트 고정이 아닙니다(이모지 등은 4바이트).
- UTF-8은 가변 길이이므로
size()가 곧 "글자 수"가 아닐 수 있습니다.
C++ 문자열 타입 감각
| 타입 | 의미 |
|---|
std::string | 바이트 문자열(인코딩은 별도 약속 필요) |
std::wstring | wide 문자열 (wchar_t 크기는 플랫폼 의존) |
std::u16string | UTF-16 코드 유닛 시퀀스 |
std::u32string | UTF-32 코드 유닛 시퀀스(코드 포인트 접근 쉬움) |
주의:
- Windows에서
wchar_t는 보통 2바이트(UTF-16 계열), Linux에서는 보통 4바이트입니다.
- 즉,
wstring을 네트워크/파일 포맷으로 바로 쓰면 이식성 문제가 생길 수 있습니다.
std::u8string utf8 = u8"AAA 루키스입니다";
std::wstring wide = L"AAA 루키스입니다";
실무 권장 패턴
- 외부 경계(파일, DB, 네트워크, 로그)는 UTF-8로 통일하는 전략이 가장 흔합니다.
- 플랫폼 API 경계에서만 필요한 인코딩으로 변환합니다.
- 문자열 길이 정책은 "바이트 수 제한인지, 사용자 글자 수 제한인지"를 명확히 구분해야 합니다.
게임/툴 체감 팁:
- 엔진/OS API와 맞추기 위해 내부에서 UTF-16을 쓰더라도,
외부 저장 포맷은 UTF-8로 두면 상호운용이 쉽습니다.
깨짐 디버깅 체크리스트
문자열이 깨지면 아래 순서로 확인하면 빠릅니다.
- 소스 파일 인코딩(UTF-8 BOM/without BOM) 일관성
- 컴파일러/IDE 문자셋 설정(MSVC라면
/utf-8 여부)
- 파일 입출력 시 사용한 인코딩과 해석 인코딩 일치 여부
- 네트워크 프로토콜에서 문자열 인코딩 명시 여부
- 콘솔/로그 뷰어가 해당 인코딩을 올바르게 렌더링하는지
짧은 원칙:
- "저장 인코딩"과 "표시 인코딩"은 다를 수 있다.
- 깨짐은 거의 항상 양쪽의 인코딩 합의 실패 문제다.