캐릭터 인코딩

언리얼 엔진 4 에 사용되는 캐릭터 인코딩에 대한 개요입니다.

Windows
MacOS
Linux

언리얼에서 사용되는 캐릭터 인코딩에 대한 개요입니다.

사전 지식: The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)

텍스트 포맷

텍스트와 스트링을 표현하는 데 사용할 수 있는 포맷은 여럿 있습니다. 이러한 포맷을 이해하고 그에 따른 장단점을 이해하는 것은 프로젝트에 어떤 포맷을 사용할 것인지 결정하는 데 도움이 될 수 있습니다.

이 내용은 해당 포맷에 대한 기술적인 정의는 아니며, 단지 이 문서에 걸맞도록 단순화된 버전입니다.

ASCII

0, 9, 10, 13, 32 - 126 를 포함한 캐릭터입니다. (P4 타입 텍스트) (이는 체크인시 P4 트리거를 통해 검증됩니다.)

ANSI

ASCII 와 (서유럽 하이 ASCII 등) 현재 코드 페이지는 P4 서버에 바이너리로 저장해야 합니다.

UTF-8

(ASCII 상위집합인) 특수 캐릭터 시퀀스를 사용하여 비-ANSI 캐릭터를 표현할 수 있는 단일 바이트로 구성된 스트링 입니다. (P4 타입 Unicode)

UTF-16

바이너리의 경우

장점

단점

내부 포맷이 정의되지 않습니다. 각 파일을 포맷에 관계없이 로드할 수 있습니다.

머지할 수 없습니다. 이런 종류의 파일은 전부 배타적으로 체크아웃해야 합니다.

내부 포맷은 정의되지 않습니다. 각 파일의 포맷이 다를 수 있습니다.

P4 는 각 버전 전체를 저장하므로, 디포 크기가 불필요하게 불어날 수 있습니다.

텍스트의 경우

장점

단점

머지할 수 있습니다. 독점 체크아웃할 필요가 없습니다.

매우 제한적입니다. ASCII 캐릭터만 허용됩니다.

UTF-8 의 경우

장점

단점

필요한 모든 문자에 간단히 접근할 수 있습니다.

아시아 언어에는 메모리 프로파일이 다릅니다.

메모리를 적게 사용합니다.

P4 타입 유니코드는 저희 퍼포스 서버에 활성화되어 있지 않습니다.

ASCII 상위집합입니다. 일반 ASCII 스트링 그대로가 완벽히 UTF-8 스트링이 됩니다.

스트링 연산이 더 복잡합니다. 길이 계산같은 간단한 연산을 할래도 스트링을 파싱해야 합니다.

스트링이 ASCII 이고 그대로 출력하는 것으로 감지되어도 작동합니다.

MSDev 는 아시아 지역에서 ASCII 이외의 것을 처리하지 못합니다. 그래서 체크인 도중 텍스트가 ASCII 인지 검사합니다.

유니코드가 활성화된 서버가 있다면 파일을 머지할 수 있어 독점 체크아웃할 필요가 없습니다.

파싱을 통해 스트링이 (BOM 있든 없든) UTF-8 인지 알아낼 수 있습니다.

UTF-16 의 경우

장점

단점

필요한 모든 글자에 간단히 접근할 수 있습니다.

메모리를 더 사용합니다.

단순합니다. 메모리 사용량은 글자 수의 두 배입니다. (사용하는 글자는 모두 Basic Multilingual Plane) 에 있습니다.

BOM 이 없는 포맷인지 알기 어렵습니다.

단순합니다. 파싱할 필요 없이 스트링을 나누고/합치는 연산이 가능합니다.

스트링이 ASCII 이고 그대로 출력하는 것으로 감지되면 작동하지 않습니다. (이제 UTF-16 검증과 함께 체크인시 검사됩니다.)

게임에서 사용된 포맷과 같아, 변환이나 파싱이나 메모리 연산 등이 필요치 않습니다.

MSDev 는 아시아 지역에서 ASCII 이외의 것을 잘 처리하지 못합니다. 그래서 체크인 도중 텍스트가 ASCII 인지 검증하는 것입니다.

머지할 수 있습니다. 독점 체크아웃할 필요가 없습니다.

C# 은 내부적으로 UTF-16 을 사용합니다.

UE4 내부 스트링 표현

언리얼 엔진 4 의 모든 스트링은 FStrings 혹은 TCHAR 정렬 상태로 UTF-16 포맷 메모리에 저장됩니다. 대부분의 코드에서 2 바이트가 하나의 코드포인트라 가정하므로, 언리얼의 내부 인코딩이 UCS-2 로 보다 정확히 설명될 수 있도록 Basic Multilingual Plane(BMP) 만 지원됩니다. 스트링은 현 플랫폼에 적합한 엔디안에 저장됩니다.

디스크에/서 또는 네트워킹 도중 패키지를 Serialize 할 때, 0xff 보다 작은 모든 TCHAR 캐릭터 스트링은 8 비트 바이트 시리즈로 저장되고, 그 외에는 2 바이트의 UTF-16 스트링으로 저장됩니다. Serialize 코드는 필요에 따라 어떤 엔디안 변환도 다룰 수 있습니다.

UE4 에 로드되는 텍스트 파일

언리얼이 외부 텍스트 파일을 로드할 때(, 예로 실행시간에 .INT 파일을 읽을 때)는, 거의 항상 UnMisc.cpp 에서 찾을 수 있는 appLoadFileToString() 함수로 해결합니다. 작업은 주로 appBufferToString() 함수에서 일어납니다.

이 함수는 UTF-16 파일에서 유니코드 바이트-오더-마크(byte-order-mark, BOM)를 인식하고, 있으면 어느 엔디안으로든 UTF-16 으로 파일을 로드합니다.

BOM 마크가 없을 때 발생하는 일은 플랫폼에 따라 달라집니다.

Windows에서는 디폴트 Windows MBCS 인코딩을 사용하여 텍스트를 UTF-16로 변환하려고 시도할 것이고 (예로 미국 영어, 서유럽은 Windows-1252, 한국어는 CP949, 일본어는 CP932) MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS...) 를 사용합니다. 이 부분은 2009년 7월 QA 빌드 쯤에 추가되었습니다.

만약 이 변환이 실패하거나 혹은 Windows 외의 플랫폼에서 실패한다면, 그것은 그저 각각의 바이트를 읽고 16비트로 패딩을 붙여 TCHAR 배열을 만듭니다.

appLoadFileToString() 으로 로드하는 경우 UTF-8 인코딩 텍스트 파일 감지나 디코딩을 위한 코드가 없으니 유의하시기 바랍니다.

언리얼에 의해 저장되는 텍스트 파일

엔진에 의해 생성되는 대부분의 텍스트 파일은 appSaveStringToFile() 을 사용하여 저장될 것입니다.

단일한 바이트로 표현가능한 TCHAR 캐릭터로 된 스트링은 일련의 8 비트 바이트로서 저장될 것이고, 그렇지 않으면 bAlwaysSaveAsAnsi 플래그가 true로 패스되는 경우, 즉 디폴트 Windows 인코딩으로 먼저 변환될 수 있는 경우가 아닌 이상 UTF-16 으로 저장될 것입니다. 이것은 현재 셰이더 파일에서만 행해지고 있으며 셰이더 컴파일러가 UTF-16 파일과 관련하여 가지고 있는 문제를 해결하기 위함입니다.

언리얼에서 사용되는 텍스트 파일용 추천 인코딩

INT 와 INI 파일

어느 엔디안이든 UTF-16 입니다. 아시아 언어에 대한 디폴트 MBCS 인코딩이 Windows 상에서 작동할 동안, 이 파일들은 PS3와 Xbox360 상에서 로드 될 필요가 있고 변환 코드는 Windows 상에서만 작동합니다.

소스 코드

일반적으로 우리는 C++ 소스 코드 안에 스트링을 그대로 쓰는 것은 추천하지 않으며, 그런 데이터는 INT 파일로 보낼 것을 추천합니다.

C++ 소스 코드

UTF-8 혹은 디폴트 Windows 인코딩 입니다. MSVC, Xbox360 컴파일러 와 gcc 모두 UTF-8 인코딩된 소스 파일이면 됩니다. 하이 비트 세트 캐릭터가 포함된 Latin-1 인코딩된 파일, 이를테면 저작권, 상호, 온도 표시같은 심볼은 가급적 소스 코드에 피해 주는 것이 좋은데, 이는 로케일이 다른 시스템에서는 인코딩이 깨지기 때문입니다. 써드파티 소프트웨어에서는 (저작권 공지 등) 불가피한 경우가 몇몇 있어서, MSVC 의 경우 아시아지역 Windows에서 발생할 수 있는 4819 경고를 끕니다.

퍼포스에 UTF-16 텍스트 파일 저장하기

  • 'Text' 를 사용하지 마십시오

    • Text 로 체크인 및 저장된 UTF-x 파일은 동기화 후 깨지게 됩니다.

  • 만약 'Binary' 를 사용하는 경우, 파일을 독점 체크아웃(exclusive checkout) 마킹하시기 바랍니다.

    • ASCII, UTF-8, UTF-16 로는 체크인해도 엔진에서 작동합니다.

    • 그러나 binary 파일은 머지가 불가능하기 때문에, 독점 체크아웃 하지 않으면 change 가 이상해질 것입니다.

  • 'UTF-16' 를 사용하는 경우, 다른 사람이 UTF-16 이외의 파일을 체크인 하지는 않는지 확인하십시오.

  • 'Unicode' 타입은 UTF-8 이고, 여기서는 쓸 일이 없습니다.

변환 루틴

스트링을 다양한 인코딩으로 상호 변환할 수 있는 매크로가 많이 있습니다. 이 매크로는 로컬 영역에서 선언된 클래스 인스턴스를 사용하여 스택에 공간을 할당하므로, 그쪽에 포인터를 유지하지 않도록 하는 것이 매우 중요합니다! 함수 호출에 스트링을 전달하는 용도로만 사용해야 합니다.

  • TCHAR_TO_ANSI(str)

  • TCHAR_TO_OEM(str)

  • ANSI_TO_TCHAR(str)

  • TCHAR_TO_UTF8(str)

  • UTF8_TO_TCHAR(str)

이들은 다음과 같은 UnStringConv.h 의 헬퍼 클래스를 사용합니다:

* typedef TStringConversion<TCHAR,ANSICHAR,FANSIToTCHAR_Convert> FANSIToTCHAR;

  • typedef TStringConversion<ANSICHAR,TCHAR,FTCHARToANSI_Convert> FTCHARToANSI;

  • typedef TStringConversion<ANSICHAR,TCHAR,FTCHARToOEM_Convert> FTCHARToOEM;

  • typedef TStringConversion<ANSICHAR,TCHAR,FTCHARToUTF8_Convert> FTCHARToUTF8;

  • typedef TStringConversion<TCHAR,ANSICHAR,FUTF8ToTCHAR_Convert> FUTF8ToTCHAR;

또한 엄청나게 중요한 점은, TCHAR_TO_ANSI 를 사용할 때는 바이트 수와 TCHAR 스트링 길이가 같을 것이라고 가정해서는 안됩니다. 복수의 바이트 캐릭터 세트에서는 TCHAR 캐릭터 당 복수의 바이트가 필요할 수 있습니다. 만약 결과 스트링의 길이가 몇 바이트인지 알고자 한다면, 매크로 보다는 헬퍼 클래스를 사용해 주시기 바랍니다. 예를 들어:

FString String;
...
FTCHARToANSI Convert(*String);
Ar->Serialize((ANSICHAR*)Convert, Convert.Length());  // FTCHARToANSI::Length() 는 null terminator 를 제외한 인코딩된 스트링의 바이트 수를 반환합니다.

유니코드의 Non-Trivial ToUpper() 와 ToLower()

UE4 는 현재 ASCII 만 처리합니다. (ASCII | 코드 페이지 1252 | | 서유럽)

모든 언어에 대해서 이 작업을 하기에 그나마 가장 나은 방법은 이 정도입니다: en.wikipedia.org/wiki/ISO/IEC_8859

  • ISO/IEC 8859-1 : 영어, 프랑스어, 독일어, 이탈리아어, 포르투갈어와 양 스페인어

  • ISO/IEC 8859-2 : 폴란드어, 체코어, 헝가리어

  • ISO/IEC 8859-5 : 러시아어

ftp.unicode.org/Public/MAPPINGS/ISO8859/ 의 매핑에는 위에 나열한 언어의 변환 규칙이 포함되어 있습니다. '대문자' 와 '소문자' 정보는 바라는 결과를 내기 위해 적합한 유니코드 캐릭터로 교차 참조될 것입니다.

동아시아 인코딩 고유의 C++ 소스 코드에 대해서

UTF-8 와 디폴트 Windows 인코딩 모두 C++ 컴파일러와 관련해서 다음과 같은 문제를 야기할 수 있습니다:

기본 Windows 인코딩

(CP437 미국과 같은) 1 바이트 캐릭터 코드 페이지로 실행되는 Windows에서 C++ 소스 코드를 컴파일 할 때는, 소스 코드에 CP932 (일본어), CP936 (중국어 간체) 혹은 CP950 (정통 중국어) 와 같은 동아시아 2 바이트 캐릭터 인코딩이 들어있는지 주의를 기울이시기 바랍니다.

이 동아시아 캐릭터 인코딩 시스템에서는 첫째 바이트에 0x81-0xFE 를, 그리고 둘째 바이트에 0x40-0xFE 를 사용합니다. 둘째 바이트에서 0x5C 의 값은 ASCII/latin-1 에서 백슬래시로 해석되는데, 이게 C++ 에 있어 특별한 의미를 지닙니다 (스트링을 그대로 쓰는 것에서의 이스케이프 시퀀스이고, 줄 끝에 사용되면 줄 연속입니다).
그러한 소스 코드를 1 바이트 코드 페이지 Windows에서 컴파일 할 때, 컴파일러는 동아시아 2 바이트 캐릭터 인코딩에 대해선 신경을 쓰지 않는 바, 이것은 컴파일 오류, 혹은 EXE 에 버그가 생기는 등 더 심각한 문제를 야기할 수 있습니다.

한 줄 코멘트: 동아시아어 코멘트의 마지막에 0x5c 가 있는 경우, 줄이 없어져 찾기 어려운 버그 또는 오류가 유발될 수 있습니다.

    // EastAsianCharacterCommentThatContains0x5cInTheEndOfComment0x5c'\'
    important_function(); /* 이 줄은 윗줄과 연결되어 코멘트의 일부가 됩니다. */

스트링 리터럴 내부:
0x5c 이스케이프 시퀀스로 인식되면 스트링이 깨지거나 오류가 발생합니다.

    printf("EastAsianCharacterThatContains0x5c'\'AndIfContains0x5cInTheEndOfString0x5c'\'");
    function();
    printf("컴파일러는 이 줄에 있는 왼쪽 따옴표를 첫째 줄에서 이어진 스트링 리터럴의 끝으로 인식하고, 이 메시지는 C++ 코드일 것으로 기대합니다.");

0x5c 다음에 오는 캐릭터가 이스케이프 시퀀스를 지정하는 경우, 컴파일러는 이스케이프 시퀀스 캐릭터 세트를 하나의 지정된 캐릭터로 변환합니다.
(지정되지 않는 경우, 정의된 구현 결과가 나지만, MSVC 는 0x5c 를 제거하고, "인식되지 않은 캐릭터 이스케이프 시퀀스" 경고를 냅니다.)
위의 경우에서, 스트링의 끝에 0x5c 백슬래시가 있고, 다음 캐릭터는 따옴표이므로, 이스케이프 시퀀스 \" 는 스트링 데이터의 따옴표로 변환되고, 컴파일러는 다음 따옴표나 줄 끝에 다다를 때까지 계속해서 스트링 데이터를 만들기에 오류가 납니다.

위험한 캐릭터의 예:
CP932 (일본어 Shift-JIS) "表" 은 0x955C 이고, 많은 CP932 캐릭터가 0x5C 를 가지고 있습니다.
CP936 (중국어 간체 GBK) "乗" 은 0x815C 이고, 많은 CP936 캐릭터가 0x5C 를 가지고 있습니다.
CP950 (정통 중국어 Big5) "功" 은 0xA55C이고, 많은 CP950 캐릭터가 0x5C를 가지고 있습니다.
CP949 (한국어, EUC-KR) 은 괜찮은데, 이는 EUC-KR 이 두 번째 바이트에서 0x5C 를 사용하지 않기 때문입니다.

BOM 없는 UTF-8 (BOM 을 시그니쳐로 서술하는 텍스트 에디터도 있습니다.)

C++ 소스 코드를 동아시아어 코드 페이지 CP949 (한국어), CP932 (일본어), CP936 (중국어 간체), CP950 (정통 중국어) Windows에서 컴파일하는 경우, 그 소스 코드에 UTF-8 로 저장된 동아시아어 캐릭터가 있는지 주의를 기울이시기 바랍니다.

UTF-8 캐릭터 인코딩은 동아시아 캐릭터에 대해 3 바이트를 사용합니다: 첫 번째 바이트는 0xE0-0xEF, 두 번째 바이트는 0x80-0xBF, 세 번째 바이트는 0x80-0xBF 입니다. BOM 없는 동아시아 Windows의 기본 인코딩 UTF-8 인코딩된 3 바이트와 다음 바이트를 두 개의 2 바이트 동아시아어 인코딩 캐릭터로 인식합니다. 첫째 둘째 바이트를 첫 번째 동아시아어 캐릭터로, 셋째와 그 다음 바이트를 두 번째 동아시아어 바이트로 말이죠. UTF-8 인코딩된 3 바이트 이후에 오는 캐릭터가 스트링 리터럴이나 코멘트에서 특별한 의미를 지니는 경우 문제가 발생할 수 있습니다.

예를 들어 인라인 코멘트에서:
홀수개의 동아시아어 캐릭터 이후 다음 캐릭터가 코멘트의 끝을 나타내는 코멘트 텍스트의 경우, 코드가 없어져 찾기 힘든 버그나 오류가 유발됩니다.

    /*OddNumberOfEastAsianCharacterComment*/
    important_function();
    /*normal comment*/

동아시아 코드 페이지 Windows에서의 컴파일러는 UTF-8 디코딩된 동아시아어 캐릭터의 마지막 바이트와 별표 '*' 를 하나의 동아시아어 캐릭터로 인식하기에, 다음 캐릭터가 여전히 코멘트의 일부인 것으로 간주됩니다. 위의 경우 컴파일러는 important_function() 부분을 코멘트의 일부로 인식하여 제거해 버립니다. 이러한 동작은 위험성이 매우 높으며, 없어진 코드를 찾기도 힘듭니다.

한줄 코멘트에서: 동아시아어 코멘트 끝에 '\' 백슬래시를 사용하면 줄이 없어져서 찾기 힘든 버그 또는 오류가 유발됩니다.

    // OddNumberOfEastAsianCharacterComment\
    description(); /* 윗줄에 백슬래시를 사용했기에 이 줄은 코멘트로 간주됩니다. */

매우 드문 경우인데, 프로그래머가 코멘트 끝에 일부러 '\' 백슬래시를 쓰지는 않을 것이기 때문입니다.

스트링 리터럴 내부에서:
홀수의 UTF-8 인코딩된 동아시아어 캐릭터가 스트링 리터럴 내부에 있고 다음 캐릭터가 특수한 의미를 지니는 경우, 스트링이 깨지거나 오류 또는 경고가 유발됩니다.

    printf("OddNumberOfEastAsiaCharacterString");
    printf("OddNumberOfEastAsiaCharacterString%d",0);
    printf("OddNumberOfEastAsiaCharacterString\n");

동아시아 코드 페이지 Windows에서의 C++ 컴파일러는 UTF-8 디코딩된 동아시아 캐릭터 스트링의 마지막 바이트와 그 다음의 캐릭터를 하나의 동아시아 캐릭터로 해석합니다. 운이 좋으면 (꺼놓지 않은 경우) "C4819" 경고 또는 오류가 나서 문제를 알 수 있습니다. 운이 없으면 그냥 스트링이 깨지는 것입니다.

결론

C++ 소스 코드에 UTF-8 이나 기본 Windows 인코딩을 사용할 수는 있으나, 이런 문제가 있음을 알아 주시기 바랍니다. 다시 한 번 말씀드리지만, C++ 소스 내부에 스트링 리터럴을 쓰지는 말아 주시기 바랍니다. C++ 소스 코드에 동아시아어 캐릭터 인코딩을 사용해야만 한다면 동아시아어를 기본 코드 페이지로 사용해 주시기 바랍니다.
또 한가지 좋은 방법은 BOM 있는 UTF-8 방식을 사용하는 것입니다. (BOM 을 유니코드 시그너처로 인식하는 텍스트 에디터도 있습니다).

2010년 2월 18일, UTF-8 과 UTF-16 으로 약간의 컴파일러를 테스트했습니다.

PC 와 Xbox 360 용 MSVC, PS3 용 gcc 나 slc 는 (BOM 이 있든 없든) UTF-8 인코딩된 소스 코드를 컴파일할 수 있습니다.
하지만 (리틀 엔디안/빅 엔디안) UTF-16 은 MSVC 에만 지원됩니다.

퍼포스는 UTF-16 과 UTF-8 둘 다 작업 가능하나, p4 diff 에서는 UTF-8 파일의 BOM 이 일정한 글자로 보이는 문제가 있습니다.

외부 참고자료: Code Pages Supported by Windows

새로운 언리얼 엔진 4 문서 사이트에 오신 것을 환영합니다!

문서 사이트에 대한 의견을 모을 수 있는 피드백 시스템을 포함해서 여러가지 새로운 기능을 준비하고 있습니다. 아래 Documentation Feedback 포럼(영문) 또는 언리얼 엔진 네이버 공식 카페(한글) 중 편하신 곳에 의견이나 문제점을 알려 주세요.

새 시스템이 준비되면 알려 드리겠습니다.

네이버 카페
공식 포럼