Windows 유니코드

2025. 2. 21. 18:10개발/Windows

이 글은 윤성우님의 '뇌를 자극하는 윈도우즈 시스템 프로그래밍'을 참고하여 공부한 내용입니다.



개발 언어를 공부할 때에는 유니코드와 아스키코드를 크게 구분짓지 않고 코드를 작성하였습니다. 왜냐하면 char 타입을 사용하여  1바이트씩 출력을 해도  UTF 인코딩을 사용하는 출력장치가 알아서 정상적으로 출력해주었기 때문입니다. 하지만 유니코드를 사용한다는 것은 입출력을 전달하기만 하는 것은 아닙니다. 그래서 우리는 유니코드의 문자열을 가공하고 해석하기 위해서 Windows에서 제공하는 유니코드 기반 라이브러리를 알아야 합니다.

 

Char Set

문자셋이라고 하며 문자 표현방법이라고 해석할 수 있습니다. 그리고 문자셋은 크게 2가지로 나누어 구분됩니다.

MBCS, Mulit Byte Character Set

이 문자셋은 여러가지 바이트 크기를 이용해 문자를 표현하는 방식을 사용합니다. 아스키코드가 이에 해당되며 아스키코드와 같이 1바이트만으로 표현하는 방식을 SBCS, Single Byte Charater Set이라고 부르기도 합니다. 그래서 이 문자셋에서는 영문은 1바이트 한글은 2바이트로 표현하며, 메모리를 절약하여 실용적인 인상이 있습니다. 하지만 개발하는 과정에서 문자열 처리에 대한 세심한 주의가 필요합니다.

 

WBCS, Wide Byte Character Set

유니코드가 이 문자셋에 속합니다. 이 문자셋에서는 모든 문자를 2바이트로 표현하기에 한글과 영어 구분할 것 없이 하나의 문자로 관리하게 됩니다.

 

MBCS 문자셋의 이해를 돕기 위한 예시를 작성해보겠습니다.

#include <iostream>

int main()
{
    char str[] = "ABC한글";
    int size = sizeof(str);
    int len = strlen(str);

    printf("배열의 크기: %d \n", size);
    printf("문자열 길이: %d \n", len);
    
    return 0;
}

 

배열의 크기: 8
문자열 길이: 7

 

MBCS에서는 알파벳은 1바이트 한글은 2바이트로 취급합니다. 그래서 배열의 크기는 ABC(3 Byte) + 한글(4 Byte) + \0(1 Byte) 총 8바이트 크기가 정상적으로 출력됩니다. 하지만 길이는 다릅니다. 한글이 2바이트여도 글자 수는 1개로 취급해서 길이가 5가 나와야하는데 7이 출력되었습니다. 즉 1바이트 단위로 길이를 측정한 것이라 추측할 수 있습니다. 

 

WBCS 기반 프로그래밍

위 예시의 문자열 길이를 정상적으로 출력하기 위해서는 유니코드 기반의 형식을 사용해야 합니다.

 

wchar_t

typedef unsigned short wchar_t;

 

이 자료형은 char와 다르게 unsigned short 자료형을 사용해 2 Byte의 메모리 공간을 할당합니다.  그래서 이 자료형을 사용해 유니코드 문자형식을 관리하는 것이 가능합니다.

 

L"String"

이 표현식은 해당 문자열이 유니코드 기반으로 표현된다는 의미를 가지게 합니다.

wchar_t str[] = "ABC";

 

위 예시의 코드를 작성하면 아래와 같은 오류가 발생합니다.

E0144 "const char [4]" 형식의 값을 사용하여 "wchar_t []" 형식의 엔터티를 초기화할 수 없습니다.

 

위 예시는 wchar_t 자료형을 사용한 문자열 선언 중 생기는 오류를 보여줍니다. 대입하려는 문자열이 WBCS가 아닌 MBCS 기반이기 때문입니다. 그럼 위와 같은 경우 L표현식을 사용해야 합니다.

wchar_t str[] = L"ABC";

 

문자열 앞에 L을 붙혀, 유니코드 기반을 명시하는 것으로 자료형 오류를 회피한 것 입니다.

 

유니코드 기반 함수

자료형과 표현식을 변경해도 사용하는 함수가 MBCS면 예상치 못한 결과를 반환할지 모릅니다. 위 예시에서 사용했던 strlen은 SBCS 기반의 함수입니다. 그래서 문자열의 길이가 예상하는 5가 아닌 7이 출력된 것이죠. 정상적인 출력을 얻기위해 코드를 수정해보겠습니다.

#include <iostream>

int main()
{
    wchar_t str[] = L"ABC한글";
    int size = sizeof(str);
    int len = wcslen(str);

    _wsetlocale(LC_ALL, L"ko_KR.utf8");

    wprintf(L"배열의 크기: %d \n", size);
    wprintf(L"문자열 길이: %d \n", len);

    return 0;
}

 

배열의 크기: 12
문자열 길이: 5

 

wchar_t를 사용하여 "ABC한글\0" 널 문자까지 포함한 크기 12 Byte가 출력되었습니다. 그리고 strlen을 대신하여 wcslen 함수를 사용하여 유니코드 문자열의 정확한 길이를 출력하였습니다. 출력함수인 printf도 유니코드 기반 함수인 wprintf를 대신하여 출력합니다. 중간에 있는 _wsetlocale은 지역/인코딩 설정을 통해 한글 출력이 가능하게 해줍니다. wprintf 대신 std::wcout도 사용 가능하며, 동일하게 _wsetlocate 설정을 해주어야 합니다.

 

wmain

main 함수는 프로그램의 진입점으로 사용됩니다. 그리고 main 함수의 매개변수에는 argcargv가 있습니다.

int	main(int argc, char *argv[])

 

argc, argument count의 약자로 프로그램 실행 시 쉘에서 전달받은 매개변수의 갯수가 전달 됩니다. 이때 실행하기 위해 호출된 프로그램 이름도 갯수에 포함됩니다. argv, argument values 전달받은 매개변수 문자열들의 배열 입니다. 이 매개변수를 통해 프로그램은 각기다른 동작이 정의 될 수 있습니다.

 

하지만 여기서 argv는 char 자료형을 사용합니다. 그래서 본문을 유니코드 기반 코드로 작성하여도 완벽한 유니코드 기반 코드는 될 수 없습니다. 그래서 wchar_t 형을 사용하는 main 함수가 있습니다.

int	wmain(int argc, wchar_t *argv[])

 

wmain은 유니코드 기반의 매개변수를 받기 위한 프로그램 진입점 전역함수입니다. 이 함수를 통해 매개변수를 wchar_t 형식을 사용해 받을 수 있으며, argc는 동일합니다. 아래 코드는 전달 받은 매개변수를 출력하는 예시입니다.

#include <iostream>

int wmain(int argc, wchar_t *argv[])
{
    for (int i = 1; i < argc; i++)
    {
        fputws(argv[i], stdout);
        fputws(L"\n", stdout);
    }

    return 0;
}

 

fputws는 유니코드 기반 문자열, wchar_t *형식을 두 번째의 스트림에 쓰는 함수입니다. 즉 표준출력에 전달 받은 매개변수와 줄바꿈을 출력합니다.

> .\x64\Debug\250222.exe ABC SCD

 

ABC
SCD

 

만약 매개변수에 한글을 쓸 경우, _wsetlocate을 설정해주어야 합니다.

 

tchar.h

MBCSWBCS을 동시에 지원하는 프로그램을 만들기 위해서는 이 헤더파일이 필요합니다. 여기서 동시에 지원한다는 뜻은 프로그램이 각각 시스템 환경에 따라 유동적으로 MBCS 혹은 WBCS를 지원한다는 것 입니다.

#ifdef _UNICODE
    typedef wchar_t	TCHAR;
    typedef wchar_t*	PTCHAR
#else
    typedef char	TCHAR;
    typedef char*	PTCHAR
#endif

 

tchar.h 헤더파일에는 위와 비슷한 방식으로 조건부 컴파일로 작성되어 있습니다. _UNICODE 혹은 _MBCS 매크로가 선언되어 있는지에 따라 TCHAR 자료형이 바뀌는 것 입니다. 

tchar.h MBCS WBCS
TCHAR arr[10]; char arr[10]; wchar_t arr[10];

 

위와 같은 방식으로 리터럴 문자 표현식도 적용할 수 있습니다.

#ifdef _UNICODE
    #define __T(x)	L ## x
#else
    #define __T(x)	x
#endif

#define _T(x)		__T(x)
#define _TEXT(x)	__T(x)

 

앞서 말했든 WBCS 기반인 wchar_t에는 유니코드 표현식으로 문자열을 넣어야 합니다. 이 때 매크로를 사용하면 리터럴 문자열을 유동적으로 표현할 수 있습니다. 

tchar.h MBCS WBCS
_T("ABC"); "ABC"; L"ABC";

 

위 매크로 중 ##은 두 개의 값을 붙여서 사용한다는 뜻입니다.

 

이제 함수를 알아보겠습니다. wmain을 포함한 유니코드 기반 함수들도 조건부 컴파일을 통해 선택적으로 컴파일 됩니다.

#ifdef _UNICODE
    #define	_tmain		wmain
    #define	_tprintf	wprintf
    ...
#else
    #define	_tmain		main
    #define	_tprintf	printf
    ...
#endif

 

나머지 함수들은 tchar.h 헤더 파일을 통해 확인할 수 있습니다.

tchar.h MBCS WBCS
int _tmain(int argc, PTCHAR argv[]) int main(int argc, char* argv[]) int wmain(int argc, wchar_t* argv[])

 

이제 위 매크로를 사용한 코드를 작성해보겠습니다.

#include <iostream>
#include <tchar.h>

int _tmain(int argc, PTCHAR argv[])
{
    TCHAR str[] = _T("MBCS or WBCS");

    _tprintf(_T("%s\n"), str);
    _tprintf(_T("string size: %d\nstring length: %d\n"), sizeof(str), _tcslen(str));
    
    return 0;
}

 

MBCS or WBCS
string size: 26
string length: 12

 

위 예시는 tchar.h 헤더파일의 매크로를 사용하여 작성한 코드입니다. 하지만 명시적이지 않기 때문에 코드 가독성이 떨어집니다. 그리고 C++에서는 std::wstring, std::wcout 등 유니코드를 지원하는 기능이 더 권장됩니다.