삽질하는플머

코드 페이지 삽질기.

탐구생활/Delphi

게임을 일본에 내보냈던 몇년 전 경험에 대해 조금 이야기 해 볼까 합니다.
제가 만든 개허접 GUI엔진에서는 순전히 글립 캐싱할 때 인덱싱이 쉽다는 이유로 유니코드를 사용하고 있습니다.
뭘 모르고 선택했던 것인데... 두고 두고 생각할 때 가장 잘 한 일이었다고 생각해요.

그러나 델파이의 string과 widestring은 "현재 시스템의 코드페이지를 기준으로 자동변환된다" 는, 어쩌면 지극히 상식적인 문제 때문에 일본 서비스를 시작하면서 웃지 못할 일이 발생하고 맙니다.

이 델파이의 신묘함 때문에 초창기 게임 클라이언트는 의도하지 않은 기능을 하나 갖게 되는데, 바로 코드페이지가 같은 시스템 끼리는 같은 나라 언어로 대화할 수 있다는 것이었죠.

예를 들어 일본 서버에 접속한 한국 유저들 끼리는 한글로 채팅을 할 수 있었습니다.
물론 주위의 일본사람들이 볼 때는 의미없이 깨져버린 문자열로 보이겠지만요. (한글로 보인다는 의미가 아닙니다.)
한국 유저가 게임에서 채팅을 시작하면, 입력은 유니코드로 됩니다. 엔터를 누르는 순간 이 유니코드 문자열은 채팅내용을 서버로 전달하는 RequestChat() 함수의 인자로 전달되는데, 이 인자값은 string으로 정의되어 있었고 따라서 현재 시스템의 로케일을 기준으로 자동으로 KSC-5601로 바뀌어 서버로 날아갑니다. 서버야 뭐 이 내용을 그대로 다시 전달하겠죠?? 받아들이는 입장에서 만약 한글 윈도라면... 날아온 코드는 KSC-5601 이니 그대로 다시 GUI엔진으로 들어가면서 자동변환되어 제대로 된 한글이 표시됩니다. 그러나 일본어 윈도에서는 이 문자열을 SHIFT-JIS라고 판단해 전혀 엉뚱한 말로 바꾸어 표시하게 되죠.

'十分' 이라는 한자어를 예로 들어 봅시다. 두 글자 모두 한국과 일본에서 동일하게 표현되는 글자지만 KSC-5601에서는 #223, #168, #221, #194 라는 아스키값으로, SHIFT-JIS에서는 #143, #92, #149, #170 이라는 값으로 처리됩니다. 한마디로 한글윈도에서 일본어로 채팅을 해도 일본사람이 알 수 없는 문자열로 바뀌어버린다는 이야기죠.
한글로 채팅하는 어이없는 기능 대신, 일본어를 보기 위해서는 로케일 설정을 일본어로 하거나 일본어 윈도를 깔아야 하는 상황이 된 겁니다.

사실, 일본의 X게임을 돌리기 위해 VDos 설치까지 감수했던 우리로서는 뭐 어차피 일본서비스, 이정도는 애교로 봐 주자 했었죠. 굳이 일어윈도를 안 깔아도 AppLocale, 또는 Oh!TextHooker 같은 걸출한 물건들이 있었으니까요. 

그러나 문제는 생각지도 않았던 곳에서 발생합니다.

일본사람들이 바글거리는 곳에서 한국 유저가 특정한 단어를 채팅창에 입력하면 일본 유저들의 클라이언트는 모두 종료되어 버리더군요. 어찌나 황당하던지... 반대로 일본사람이 어떤 단어를 입력하면 이번에는 한글윈도에서 띄운 클라이언트들이 휘리릭~ 증발되어 버렸죠. 의도하지 않게 변환된 문자열이 Format문을 만났고, 랜더링 틱 마다 중첩된 에러로 스택오버플로가 발생하면서 클라이언트가 죽은 것이었습니다.

그래서... 일본 서버에서 날아온 문자열은 그냥 SHIFT-JIS라고 판단하는 쪽으로 생각을 바꾸게 됩니다.
문자열이 widestring -> string, 또는 string -> widestring 으로 변환되는 모든 곳에 WideCharToMultiByte(), MultiByteToWideChar() API로 둑을 쌓아야 했습죠.
델파이가 이걸 자동으로 바꿔줘 버리니 에러가 표시되는 것도 아니고... 정말 미칠 것 같더군요. ㅠㅠ;;;

아무튼 몇 달에 걸친 삽질 끝에 십수만라인 곳곳에 뚝을 쌓았고, 데이터에도 코드페이지를 명시 했습니다.
덕분에 일본 클라이언트를 한글윈도에서 띄워도 일본어가 제대로 표시될 수 있게 되었고...
이 때의 경험은 두고두고 좋은 약이 되었네요.


  // 안시 문자열을 유니코드로...
  function AnsiToWide(const aAnsiBuf: PChar; aAnsiLen: Integer; aCodePage: UINT): WideString;
  var
    Str: String;
    WideStrLen: Integer;
  begin
    WideStrLen :=
      MultiByteToWideChar(
        aCodePage,
        MB_PRECOMPOSED,
        aAnsiBuf,
        aAnsiLen,
        nil,
        0
      );

    SetLength(Result, WideStrLen);

    MultiByteToWideChar(
      aCodePage,
      MB_PRECOMPOSED,
      aAnsiBuf,
      aAnsiLen,
      PWideChar(Result),
      WideStrLen
    );
  end;


  // 유니코드를 안시문자열로...
  function WideToAnsi(const aWideBuf: PWideChar; aWideLen: Integer; aCodePage: UINT): String;
  var
    AnsiStrLen: Integer;
  begin
    AnsiStrLen :=
      WideCharToMultiByte(
        aCodePage,
        0,
        aWideBuf,
        aWideLen,
        nil,
        0,
        nil,
        nil
      );

    SetLength(Result, AnsiStrLen);

    WideCharToMultiByte(
      aCodePage,
      0,
      aWideBuf,
      aWideLen,
      PChar(Result),
      AnsiStrLen,
      nil,
      nil
    );
  end;


이후 루아를 만나게 됩니다. 그런데 루아라는 물건은 유니코드와는 아무 관계없는 놈 입니다.
사실 컴파일러나 인터프리터라는 입장에는 유니코드 따위 고민할 필요가 전혀 없죠. 그저 로직의 처리만 신경쓰고 인자로 넘겨받은 문자열의 내용은 단지 그 문자열을 넘겨받는 함수가 고민해야 할 부분이니까요.

먼저 경험을 토대로 문자열이 들어간 모든 데이터는 자신의 '코드페이지'를 가지도록 만들었으므로,  게임 내 아이템 이름을 넘겨받거나 할 때도 위의 함수에 이 코드페이지를 넘겨 변환하는 방식으로 처리할 수 있었습니다.  

그렇게 몇 년을 이게 정답이다~ 하면서 사용하다가... 오늘 옆자리의 하모씨가 한마디 던집니다.

    "그래 우리는 그렇고... 그렇다면 와우의 루아는 어떻게 UTF-8 을 처리하지?"
    "응~ 그러게~~ 음... 응?? UTF-8은 안시문자열이잖아~~!!"

그쵸. UCS2, UCS4가 아니더라도 안시문자열에 다국어가 훌륭하게 담기는 규약이 이미 오래전 부터 존재하고 있었고 이곳 저곳에 아무 생각없이 써 오고 있었던 겁니다. 처음부터 내부처리는 UCS2, 외부저장은 UTF-8이라는 규칙만 떠 올렸다면 국가별 코드 페이지 관리는 개나 던져주고 많은 이들이 행복하게 살 수 있었는데 말이죠.

위의 함수를 약간 손봐서 aCodePage가 CP_UTF8 (65001) 인 경우는 Multi우쩌구 말고 UTF8Decode, UTF8Encode를 사용하도록 변경 했습니다. 그리고 다국어가 섞인 루아파일을 UTF-8로 저장해 클라이언트에서 불러 봤습니다.


 Lua.SetCodePage(65001)
 Gui.MessageBox(
   "일어 : パリス・ヒルトン 突然の破局\n" ..
   "한글 : 민성기 만세만세만세\n" ..
   "쭝국 : 时髦单品购买清单",
   "테스트 대화상자"
 )  
  


오오오~~ 감동의 눈물이~~ 주주륵~~~
'十分' 만에 해결될 일을 도대체 얼마나 돌아온건지~~~


언어별 데이터 관리로 고생하는 기획팀에 심심한 애도의 묵념을~~~ㅠㅠ;;




2009.6.24

http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/System_SetMultiByteConversionCodePage.html

System.SetMultiByteConversionCodePage Function

여기에 적절한 코드페이지를 설정하면, Wide <> Ansi 변환시 기준 코드페이지가 설정된다.