KoreanFoodie's Study

[C++ 게임 서버] 5-3. Unicode 본문

Game Dev/Game Server

[C++ 게임 서버] 5-3. Unicode

GoldGiver 2023. 12. 15. 12:02

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 5-3. Unicode

핵심 :

1. 문자열을 핸들링에는 2가지 요소가 있다. 하나는 Character Set 이고, 다른 하나는 Encoding 이다.

2. Unicode 는 다양한 문자열에 대응할 수 있는 Set 으로, 인코딩 방식에 따라 UTF-8 이나 UTF-16 등으로 나뉘게 된다.

3. MBCS 는 문자의 특징마다 사용하는 바이트가 다른 경우를 의미하며, WBCS 은 모든 문자열이 동일한 바이트를 사용하는 방식이다.

이번 글에서는 누구나 익숙하지만 물어보면 항상 헷갈리는 내용, 유니코드에 대해 다시 짚고 넘어가도록 하겠다.

일단 Unicode 에 대해 제대로 이해하려면 문자열 인코딩의 두 요소, 'Character Set' 과 'Encoding' 을 구분해야 한다.

먼저, Character Set 은 그냥 ASCII Code 처럼, 각 문자열에 매칭된 숫자표 같은 것이라고 이해하면 된다. 라틴어 및 숫자 정도만 표시할 때는 ASCII Code 로도 표현이 충분하겠지만, 한글이나 한자를 사용하게 되면 더 큰 Character Set 이 필요하게 될 것이다.

대표적인 Character Set 인 아스키 코드

 

한글의 경우에는, KS X 1001 을 사용하기도 한다.

 

그런데 사실 세상에는 수많은 문자들이 있다. 이 녀석들을 전부 다 표현하려면 얼마나 많은 바이트가 필요할까? 🤔

Unicode 는 여러 변주가 있지만, 기본적으로는 위와 같은 방식으로 접근해 볼 수 있다. 1 바이트로 모든 문자를 표현하는 것은 어려우니, 각 언어별로 문자열 매칭에 쓸 범위를 나눠서 사용하는 것이다.

 

그리고 Encoding 은... 그냥 문자열을 압축하고 해제하는 방식이다. 예를 들어, 같은 txt 파일을 Windows 에서는 zip 파일로 압축하고, Linux 에서는 tar.gz 로 압축하는데, 이러한 방식의 차이를 바로 인코딩이라고 볼 수 있다.

그 중 대표적인 예시로 UTF-8 을 보면, Unicode 문자 집합을 사용하면서, 인코딩은 영문을 1바이트, 한글을 3바이트를 잡고 다룬다.

 

반면 UTF-16 은, 영문이든 한글이든 가리지 않고 2 바이트를 사용한다. 그리고 몇몇 예외적인 언어에 한해서만 4 바이트를 사용한다!

 

추가로, 마이크로소프가 쓰는 Code Page 949 라는 녀석도 있다 :

 

마지막으로, MBCS 와 WBCS 에 대해 간단히 살펴보자.

즉, UTF-8 의 경우에는 영문이 1 바이트고, 한글은 3 바이트를 사용해서 표시하니 MBCS 가 되는 것을 알 수 있다.

반면, UTF-16 을 사용하여 한글과 영문을 표현하게 되면 모든 문자열이 동일한 2 바이트를 사용하게 되므로, WBCS 형식이 됨을 알 수 있다! 😄

 

이제 예제 코드를 보면서 글을 마무리 짓도록 하자.

char sendData1[1000] = "가";	// CP949 = KS-X-1001 (한글 2바이트) + KS-X-1003 (로마 1바이트)
char sendData2[1000] = u8"가"; // UTF-8 = Unicode (한글 3바이트 + 로마 1바이트)
WCHAR sendData3[1000] = L"가"; // UTF-16 = Unicode (한글/로바 2바이트)
TCHAR sendData4[1000] = _T("가"); // 속성 > 고급 > 문자집합 > (유니코드 / 멀티바이트) 중 택 1 하여 프로젝트에 맞게 알아서 세팅

일단, 같은 "가" 라는 문자열도 위처럼 여러 가지로 표현이 가능하다. 일단 우리는 WCHAR 를 써서 간단하게 패킷에 문자열을 넣어 볼 것이다.

 

ServerPacketHandler 에서는 다음과 같이 쓰고 :

bw << (uint16)name.size();
bw.Write((void*)name.data(), name.size() * sizeof(WCHAR));

 

ClientPacketHandler 에서는 다음과 같이 읽으면 된다 :

wstring name;
uint16 nameLen;
br >> nameLen;
name.resize(nameLen);

br.Read((void*)name.data(), nameLen * sizeof(WCHAR));

로그는 아래처럼 찍어주면 된다!

// WCHAR 이라서, cout 대신 wcout 을 사용. 로케일도 설정해 줘야 깨지지 않는다
wcout.imbue(std::locale("kor"));
wcout << name << endl;
 
Comments