유니코드
게시판 글쓰기에 버그가 있나보군요..ㅡㅡ; 다 안올라가져서 두개로 나누어서 올립니다. http://tong.nate.com/junghunny80/ 이곳에서 날랐고요. 원본 출처는 안열리는군요..ㅡㅡ;
2005.12.03 정리 LnK로직코리아 프로그램팀 강성수
이 보고서는 유니코드에 대한 내용들이다. 유니코드란 무엇인지부터, Windows에서 어떻게 돌아가는지, Visual Studio C++과 Windows API에서어떻게 유니코드들 지원하고 사용해야 하는지, 유니코드로 작성이 유의할 점 등의 다양한 정보가 포함되어 있다. 유니코드로 개발시 많은 도움이 되었으면 하는 바램으로 적어본다. 단, 필자 역시 유니코드로 개발한 경험은 없기 때문에 잘못된 정보가 있을지도 모른다. 있다면 필자에게 과감히 따져서 고치도록 하자!!
아스키 코드와 유니코드 아스키 코드 ( ASCII ) 영문자, 숫자, 기호를 7비트로 표시하는 미국 표준 코드이다. 아스키 코드로 표현할 수 있는 문자의 수는 2^7=128가지이고, 오류 검사용 패리티 비트 하나를 추가하여 8비트로 2진 코드화함으로써 바이트단위의 전송이 가능하다.
유니코드 ( UniCode ) 유니코드는 세계 각국의 언어를 통일된 방법으로 표현할 수 있게 제안된 국제적인 코드 규약의 이름이다. 26자의 영문 알파벳과 몇 가지 특수 문자를 표현하기에는 1바이트로 충분하지만 한글, 한자, 일어 등과 같은 문자는 그 구조가 영어와 달라 1바이트로는 표현이 불가능하다. 이에 아스키 코드를 16비트로 확장하여 전 세계의 문자를 표현하는 표준코드를 만들었는데 그것이 바로 유니코드이다. 따라서 하나의 문자코드로 전 세계의 모든 문자를 표현할 수 있는 코드 체계인 것이다. 모든 문자는 16비트로 표현되므로 최대 65536개의 문자를 표현 할 수 있고 각 국가별로 코드 영역이 구분되어 있기 때문에 코드 페이지를 변경할 필요가 없다. 특정 문자는 세계의 어느 곳에서나 일정한 코드로 표현되며 따라서 유니코드로 작성된 프로그램은 별도의 재 컴파일 과정을 거칠 필요가 없다.
아스키 코드와 유니코드 비교 한 글 H a n g u l \0 확장형 ANSI인 DBCS는 위에서 보는 것처럼 메모리에서 한글은 2바이트, 영문과 숫자는 1바이트를 차지한다. 각 바이트가 2바이트짜리 문자인지 아닌지는 IsDBCSLeadByte함수로 조사할 수 있으며 DBCS 문자열의 앞 뒤 문자를 구할 때는 CharPrev, CharNext라는 함수를 사용할 수 있으나 CharPrev함수의 경우 속도가 굉장히 느리고 비효율적이라고 한다. 또한 DBCS는 각 코드 페이지에 따라 실제로 맵핑되어 있는 문자가 달라질 수 있어 국제적인 범용 프로그램 제작에는 부적당하다. 한글 Windows에서는 한글이 그대로 보이지만 중국, 일본 Windows에서는 한글코드 영역에 히라가나 또는 한자코드가 맵핑되어 있어 제대로 보이지 않는다.
한 글 H a n g u l \0 이에 비해 유니코드는 위에 보는 메모리처럼 한글이나 영문이나 모두 2바이트씩 차지하므로 문자 수는 정확하게 배열길이와 같으며 특정문자의 앞뒤 문자는 2바이트씩 앞뒤로 포인터만 이동해 주면 쉽게 구할 수 있다. 영문이나 숫자도 2바이트씩 차지하기 때문에 기억 장소가 낭비되는 면이 있기는 하지만 유니코드를 이용하면 프로그램을 하나만 만들면 모든 나라들의 글자를 처리할 수 있기 때문에 그만큼 큰 이점이 있다.
유니코드의 종류 및 역사 여기서 일일이 나열하기엔 무리다. 다음 사이트에 자세히 나와있으니 참고하도록 하자. http://jinsuk.pe.kr/Unicode/Unicode_intro-kr.html
용어 정리 위 사이트에서 나오지 않지만 자주 보이는 용어들을 정리해 보았다. SBCS ( Single Byte Character Set ) 아스키 문자 셋이나 ANSI 문자 셋처럼 1바이트로 문자를 표현하는 문자 코드로, 바이트 단위이기 때문에 최대 256개의 문자를 표현할 수 있다. DBCS ( Double Byte Character Set ) 1바이트로 표현할 수 없는 문자들을 표현하기 위해 2바이트를 사용하는 체계로, 우리가 사용하는 DBCS는 영문, 기호는 8비트로 표현하고 한글은 16비트로 표현하는 ANSI의 확장형 문자코드 이다. MBCS ( Multi Byte Character Set ) 하나의 글자를 표현할 때 여러 바이트가 조합될 수 잇는 문자 세트로, 이 표준은 두 개 혹은 그 이상의 바이트를 합칠 수 있도록 허용하고 있으나 대부분은 두 개의 바이트만을 조합하고 있다.
Windows에서의 유니코드 Windows는 유니코드와 MBCS의 변환 함수가 제공되며 유니코드(UCS-2)를 기준으로 화면에 출력할 수 있다고 한다. 그러면 Windows 버전별로 유니코드가 어떻게 동작하는지 알아보자. Windows 95/98/ME는 유니코드를 기본적으로 지원하지 않으며 NT/2000 이상의 버전에서만 유니코드와 DBCS를 동시에 지원한다. 또한 Windows 2000 이상의 버전들은 유니코드를 기본으로 제공하면서, ANSI 버전의 함수가 호출되면 유니코드 문자열로 변환한 후 다시 유니코드 버전의 함수를 호출하도록 되어 있다. 따라서 ANSI 버전의 함수를 사용하는 응용프로그램은 모든 Windows 버전에서 동작할 수 있지만 NT기반 시스템에서는 다소 비효율적으로 동작하는 단점이 있고, 유니코드 버전의 함수를 사용하는 응용프로그램은 Windows 95/98/ME에서는 동작하지 않는 한계를 가지게 된다. 또한 ANSI응용프로그램을 유니코드 응용프로그램으로 바꾸거나, 그 반대의 경우일 때 변수나 함수가 다르기 때문에 일일이 소스를 수정해야 하는 불편이 따른다. 이런 문제를 극복하기 위해 Windows 플랫폼 SDK는 소스코드의 호환성을 유지하면서 ANSI API를 사용하는 버전과 유니코드 API를 사용하는 버전을 동시에 개발할 수 있도록 배려하고 있다. 즉, 프로그램을 컴파일 할 때 UNICODE(_UNICODE )라는 매크로를 정의해 놓으면 유니코드 API를 호출하고 그렇지 않을 경우에는 ANSI API를 호출하도록 설계해 놓은 것이다. 이런 Windows SDK는 소스차원에서만 호환될 뿐 실행파일에서는 호환되지 않기 때문에 ANSI 버전의 실행파일과 유니코드 버전의 실행파일을 따로 배포 해야 하는 불편함이 있다. 만약 응용프로그램이 유니코드를 사용하면서도 Windows 95/98/ME에서도 실행되게 하려면 NT기반 Windows에서는 유니코드 API를 호출하고 하위 버전에서는 ANSI API를 호출하면 될 것이다. 자세한 내용은 아래에서 더 알아보도록 하자. 참고로 Windows CE와 COM은 유니코드만 지원한다.
MS Visual Studio C++ 에서 유니코드 사용하기 유니코드가 현재 32비트까지 확장됨에 따라 컴파일러들도 유니코드 지원여부가 다르다고 한다. gcc와 MS Visual Studio C++도 다르며 해석도다르다. 여기에서는 MS Visual Studio C++과 Windows 플랫폼으로 개발을 할 것이기에 그것에 대해서만 알아보도록 한다. MS Visual Studio C++은 유니코드를 char 대신 wchar_t를 사용하는데 다음과 같이 정의되어 있다. Windows가 유니코드(UCS-2)를 기준으로 하기 때문에 2바이트로 표현하는 것 같다.
typedef unsigned short wchar_t;
따라서 배열을 잡을 때 다음과 같이 한다. char strName[] = “한글Hangul”; // ANSI wchar_t strName[] = L “한글Hangul”; // 유니코드
위 유니코드에서 L을 붙였는데, 문자열 상수에 붙이는 L 접두어는 이 문자열을 16비트의 문자열 상수로 초기화하라는 일종의 구두점이며 컴파일러가 지원한다. L 접두어를 붙이지 않으면 컴파일러는 문자열 상수를 무조건 ANSI 포맷으로 초기화한다. 이럴 경우 각각의 경우에 대해 각각의 변수, 함수 등을 호출해줘야 하는 번거로움이 생긴다. 그래서 Windows API에서의 유니코드는 위에서 언급했듯이 ANSI와 유니코드 소스 호환성을 위해 UNICODE(_UNICODE) 정의 여부에 따라 Windows API는 매크로를 통해 유니코드와 ANSI를 구별해 준다. 아래에서 더 자세히 알아보자.
문자형 char형과 wchar_t형은 CHAR, WCHAR로 재정의 되어있고 이 두 타입은 다시 TCHAR로 정의되며 UNICODE(_UNICODE) 매크로의 정의 여부에 따라 타입이 달라진다. typedef wchar_t WCHAR; typedef char CHAR;
#ifdef UNICODE typedef WCHAR TCHAR; #else typedef char TCHAR; #endif
따라서 문자형이나 문자배열형은 char형이나 wchar_t형을 직접 사용하지 말고 TCHAR형의 일반형으로 선언하면 유니코드와 ANSI를 동시에 지원할 수 있다.
(예) TCHAR strName[100]; // UNICODE 정의 여부에 따라 char 또는 wchar_t로 된다.
문자형 변수뿐 아니라 상수형 문자열 포인터 변수에서도 유니코드를 고려해야 한다. 만약 코드내에서 LPCSTR(const char*)나 LPSTR(char*)와같은 문자열 포인터 변수를 쓴다면 이 코드는 오직 ANSI 체계만 지원할 수 있게 된다. 이때는 LPCSTR, LPSTR 대신에 각각 LPCTSTR, LPTSTR 형으로 쓰는 것이 유니코드를 대비한 코딩이 될 수 있다. 결론은, 문자형 및 문자열 포인터는 Generic 형으로 쓰도록 하자.
문자열 상수 변수의 타입뿐만 아니라 문자열 상수를 정의할 때도 L 접두어가 있을 수도 있고 없을 수도 있는데 이때는 TEXT 매크로를 사용한다. 다음과 같이 정의되어 있다.
#ifdef UNICODE #define __TEXT(quote) L## quote #else #define __TEXT(quote) quote #endif #define TEXT(quote) __TEXT(quote)
(예) TCHAR strName[] = TEXT( “한글Hangul” );
_T, __T, TEXT, _TEXT, __TEXT는 동일한 매크로이고 같은 기능을 한다. 단, _TEXT 매크로는 win32 API 환경에서 쓰는 매크로이며 _T는 MFC에서 쓰는 매크로라고 한다. UNICODE, _UNICODE 역시 동일한 매크로이다. ( 테스트 결과 UNICODE는 Wondows 함수를 쓸 때 영향을 받고, _UNICODE는 C런타임 함수를 쓸 때 영향을 받는 것 같다. 따라서 UNICODE, _UNICODE 전부 선언해주는 것이 에러를 막는 방법이다. )
함수 C 런타임 표준 C 런타임 문자열 함수들은 오직 ANSI만을 지원한다. 그래서 유니코드를 지원하기 위해서 유니코드 문자열 함수들을 따로 정의해 놓았다. 또한 이들 역시 UNICODE(_UNICODE) 정의 여부에 따라 호출되도록 만들어져 있다. char *strcat(char*, const char*); // ANSI wchar_t *wcscat(wchar_t*, const wchar_t*); // 유니코드
char* strchar(const char*, int); // ANSI wchar_t* wcschar(const wchar_t*, wchar_t); // 유니코드 char* strcpy(char*, const char*); // ANSI wchar_t wcscpy(wchar_t*, const wchar_t*); // 유니코드 size_t strlen(const char*); // ANSI size_t wcslen(const wchar_t*); // 유니코드
유니코드 문자열 함수는 ANSI C 문자열 함수에 앞에 str을 빼고 대신 wcs라는 접두어를 붙인 것이다. ( wcs는 wide character string의 약자) 이들은 변수와 문자열과 마찬가지로 ANSI와 유니코드 모두를 한 코드에서 컴파일이 가능하도록 하려면 str이나 wcs가 들어가는 자리에 _tcs를 붙이면 된다.
// UNICODE 정의 여부에 따라 ANSI 또는 유니코드 함수를 호출 해 준다. TCHAR* _tcscat(TCHAR*, const TCHAR*); TCHAR* _tcschar(const TCHAR*, TCHAR); TCHAR* _tcscpy(TCHAR*, const TCHAR*); size_t _tcslen(const TCHAR*);
단, TCHAR.H를 include 해주어야 한다.
Windows API 함수 문자열을 인수로 취하는 모든 함수들은 유니코드 버전과 ANSI 버전이 이중으로 정의되어 있다. 그러나 문자열과 무관한 함수들은 하나씩만 정의되어 있다.
~W 함수 : W는 Wide로 유니코드 문자열을 지원 ~A 함수 : ANSI로 전달된 문자열을 유니코드로 변환 후 ~W함수를 호출해주는 단순한 래퍼 함수
(예) #ifdef UNICODE #define MessageBox MessageBoxW // 유니코드 문자열 지원하는 함수 #else #define MessageBox MessageBoxA // ANSI 문자열 지원하는 함수 #endif // !UNICODE
UNICODE(_UNICODE) 매크로 상수에 따라 정의가 달라지는 것을 알 수 있다. 단, Windows 95/98/ME 일 때 ~W함수의 경우 단순히 에러만 리턴 한다.
C 런타임 문자열 함수들을 Windows API 함수에서도 제공해주는데 lstrlen, lstrcpy, lstrcat, lstrcmp, lstrcmpi 가 그에 해당하며, 이 함수들도 W함수와 A함수 버전이 있고 UNICODE(_UNICODE ) 매크로에 따라 각각 호출된다.
printf와 sprintf 함수 역시 다음과 같다.
ANSI, 유니코드 모두 Generic하게 사용하기 위해선 _tprintf , _stprintf 함수를 쓰면 된다. Windows API인 wsprintf는 유저가 유니코드나 ANSI를 고려할 필요가 없다.
(예) char szA[100]; // ANSI 스트링 버퍼 char szW[100]; // 유니코드 스트링 버퍼 TCHAR strName[100]; // ANSI / 유니코드 스트링 버퍼
sprintf(szA, "%s", "ANSI string"); // 일반적인 ANSI 스트링을 표현할 때 sprintf(szA, "%s", L"Unicode string"); // 유니코드 문자열을 ANSI로 변환 swprintf(szW, L"%s", L"Unicode string"); // 유니코드 스트링을 표현할 때 swprintf(szW, L"%s", "ANSI string"); // ANIS 스트링을 유니코드 스트링으로 변환 _stprintf(strName, TEXT(“%s”), TEXT( “한글” )); // UNICODE 정의 여부에 따라 변환 |