POCU C언어 정주행 19회차 - C11 유니코드, 베스트 프랙티스

2023. 1. 12. 00:53C언어 복습

1. C11 유니코드

이 글부터 C11에서 들어온 기능들을 소개할 예정이다. 이전 글에서 유니코드와 wchar_t에 대해서 알아봤는데 여기에 나오는 문자열 관련 내용들은 전부 C11에서 들어온 내용이다. 우선 이전 글과 이어지는 내용이라 이전 글에서 어떤 내용을 다뤘는지 간단하게 알아보자면 wchar_t에 대해서 다뤘고 단점이 뭐였는지도 알아보았다. 하지만 결론은 ICU를 사용하거나 wchar_t로 데이터를 주고 받는 데 있어서 두 기계가의 포팅이 가능하도록 변환을 할 수 있는 방법을 마련해야 한다는 것으로 끝났었다.

 

그래서 C11로 넘어오면서 wcahr_t을 대체할만한 것으로 유티코드가 떠오르게 된 것이다. UTF-32, UTF-16같은 것들은 크기가 고정되어 있기 때문에 멀티 바이트로 변환시킬 때 wchar_t와 같은 문제가 생기지 않기 때문이다. 단, C11 표준에서 지원하는 것은 아니고 컴파일러마다 지원을 할지 말지를 선택하게 했기 때문에 지원을 하지 않을 수 있다. 그럼에도 컴파일러가 지원하지 않을 이유는 없으니 어지간하면 지원을 할 것이라고 강의에서 이야기했다.

// 이 헤더파일을 인클루드해야 한다.
#include <uchar.h>

// 아래의 두 매크로가 선언되어 있다면 사용이 가능하다.
#define __STDC_UTF_16__
#define __STDC_UTF_32__

// 각각 char16_t, char32_t를 사용할 수 있게 해준다.

char16_t msg1[] = u"포큐";
char16_t msg2[] = u"\uD3EC\uD050";

char32_t msg3[] = U"포큐";
char32_t msg4[] = U"\U0000d3ec\U0000d050";

const char* utf16_str = u"포프";        // 컴파일 오류
const char16_t utf16_str[] = u"포프";   // 컴파일 됨

 

이제 위와 같이 코드를 짜고 printf와 같은 함수로 "%s"같은 서식 문자를 써서 출력을 하려고 하면 출력이 안된다. 당연한 것이 멀티바이트가 아니기 때문이다. 위의 UTF를 멀티바이트로 변환을 시켜서 출력을 시켜야 제대로 출력이 된다. 여기서 주의해야 할 것이 문자열을 변환하는 함수는 없고 문자만 변환시키는 함수만 존재하기 때문에 일일히 반복문을 돌려야 한다... 멀티바이트 문자와 UTF끼리 변환하는 함수는 아래의 4개가 있다.

size_t c16rtomb(char* restrict s, char16_t c16, mbstate_t* restrict ps);
size_t mbrtoc16(char16_t* restrict pc16, const char* restrict s, size_t n, mbstate_t* restrict ps);
size_t c32rtomb(char* restrict s, char32_t c32, mbstate_t* restrict ps);
size_t mbrtoc32(char32_t* restrict pc32, const char* restrict s, size_t n, mbstate_t* restrict ps);

 

아래는 c16rtomb함수의 사용 예제인데 utf16_str[i]에 해당하는 UTF-16으로 문자를 읽어서 p에 멀티바이트로 변환하도록 작동한다. 그리고 변환시키는 과정에서 mbstate_t가 필요하기 때문에 0으로 초기화시킨 후에 주소로 넘겨준 모습이다. 

const char16_t utf16_str[] = u"포프";
mbstate_t state = { 0 };
char buffer[64];
char* p = buffer;

for (size_t i = 0; i < ARRAY_LENGTH(utf16_str); i++)
{
    size_t num_bytes = c16rtomb(p, utf16_str[i], &state);
    
    if ((size_t)-1 == num_bytes)
    {
        break;
    }
    
    p += num_bytes;
}

사용법은 이 정도로만 알아도 사용하는 것 자체는 큰 문제가 없다. 여기서 -1과 비교 연산을 하는 부분이 나오는데 더 이상 변환할 문자가 없을 경우에 -1을 반환시키기 때문이다. 문자를 변환시켰다면 멀티바이트로 몇 바이트만큼 변환시켰는지 반환된다.

 

그럼 이제 UTF-16, UTF-32와 같은 유니코드만 사용하면 문자열 관련 문제는 모두 해결되는 것일까? 아쉽게도 그렇지는 않다. wchar_t를 사용해야 하는 경우가 존재하기 때문인데 운영체제에서 지원하는 몇몇 함수들은 매개 변수로 wchar_t 문자열을 요구하기 때문에 가끔씩 쓰긴 써야한다고 한다.

 

여담으로 강의에 따르면 요즘은 UTF-8이 많이 쓰이는 상태라고 하는데 아쉽게도 C11에는 UTF-8이 들어오지 않아서 C22에서 넣자고 제안된 상태라고 한다. 강의가 나올 당시가 약 2018~2019년도 쯤이니 이 때 당시에는 C에 UTF-8이 없었을 것이다. 물론 아래와 같이 코드를 짜는 것은 가능하지만 멀티바이트로 바꾸는 함수는 아직 없다.

const char* utf8_str = u8"포프";
const char* utf8_str2 = u8"\U0000d3ec\U0000d504";

// char8_t*가 없어서 char*로 저장되며 멀티바이트로 변환은 못함...

 

 

2. 베스트 프랙티스

지금까지 C에서 다국어 문자열을 처리하기 위한 방법들이 어떤 것이 존재하는지 알아봤다. 그럼 이제 단계별로 베스트 프랙티스를 알아볼 차례이다. 자신의 환경에 맞는 방법을 잘 골라보도록 하자.

 

0 - 기본 원칙

0순위로 지켜져야 하는 기본 원칙이다. 본인의 상황이 아래와 같다면 이 기본 원칙만 지켜도 굉장히 편하게 다국어 문자열을 처리할 수 있다.

  • 사용자에게 보여주지 않을 문자열은 전부 아스키로 저장한다.
  • 다국어 지원은 사용자에게 보여줄 문자열만 한다.
  • 파일로 저장할 때는 공통된 인코딩이 좋다. (요즘은 UTF-8)
  • 가능하다면 가장 편한 방법은 ICU 라이브러리를 쓰는 것이다.

 

1 - 최상의 시나리오 (C89 이상)

사용자 환경을 모두 UTF-8로 만드는 방법이다. 읽고 쓸 때, 멀티바이트와 UTF-8로 변환만 시키면 되기 때문에 아무런 문제가 없다. 단, 사실상 모든 사용자 환경을 똑같이 만든다는 것은 사실상 같은 회사 내에서 사용할 때나 가능한 방법이다.

 

2 - wchar_t가 UTF-32인 경우 (C89 이상)

이 경우도 꽤 괜찮은 경우다. 요즘은 거의 다 UTF-8을 사용하기 때문에 파일을 저장하고 읽고 쓰는 데 있어서 UTF-32를 UTF-8로 변환을 시킬 수 있다면 1에서 이야기했던 것처럼 그대로 UTF-8과 멀티바이트를 서로 변환시켜 가면서 사용하면 되는 것이다.

 

3 - char32_t가 UTF-32인 경우 (C11 이상)

이 경우부터는 C89에서 통하지 않는 방법이다. C89에는 char32_t같은 자료형이 없기 때문이다. 대신 C11부터는 char32_t같은 자료형을 지원하기 때문에 이것을 이용해서 UTF-8로 변환한 이후에 그것을 멀티바이트로 변환하는 방법을 사용하면 된다.