C++/MFC에서 char를 wchar_t로 안전하게 변환하는 법 (MultiByteToWideChar 완전 정리)

Prompt Architect 편집팀 · 2026-06-18 · 8분

TL;DR — MFC 환경에서 멀티바이트 char 문자열을 유니코드 wchar_t로 변환하는 표준 방법을 MultiByteToWideChar API 중심으로 단계별 정리하고, 메모리 누수·인코딩 함정·현대적 대안까지 다룹니다.

들어가며: 왜 갑자기 글자가 깨질까

MFC나 Win32 환경에서 C++로 개발하다 보면 한 번쯤 이런 상황을 만난다. 분명히 멀쩡한 문자열을 CString이나 MessageBoxW, 혹은 와이드 문자열을 요구하는 API에 넘겼는데, 화면에는 한 글자만 찍히거나 알 수 없는 기호가 출력된다. 컴파일 에러로 cannot convert 'char *' to 'LPCWSTR' 같은 메시지가 뜨기도 한다.

원인은 거의 항상 문자 인코딩의 불일치다. 우리가 흔히 쓰는 char 기반 문자열(멀티바이트, MBCS)과 Windows가 내부적으로 사용하는 wchar_t 기반 유니코드 문자열(UTF-16)은 메모리에서 완전히 다른 모양을 가진다. 이 둘 사이를 정확히 변환하지 않으면 데이터가 깨진다.

이 글에서는 char에서 wchar_t로 변환하는 표준 방법인 MultiByteToWideChar API를 단계별로 해부하고, 실무에서 자주 터지는 메모리 누수와 인코딩 함정, 그리고 더 안전한 현대적 대안까지 한 번에 정리한다.

1. char와 wchar_t는 무엇이 다른가

먼저 둘의 정체를 짚고 가자.

  • char: 1바이트 단위 문자. Windows의 멀티바이트(MBCS) 환경에서 한글 같은 문자는 2바이트로, 영문은 1바이트로 표현된다(CP949/EUC-KR 등 코드 페이지에 의존).
  • wchar_t: Windows에서는 2바이트(16비트). 내부적으로 UTF-16 인코딩을 사용한다. L"문자열" 형태의 리터럴이 이 타입이다.

핵심은 바이트 수가 문자 수와 같지 않다는 점이다. char 문자열에서 "안녕"은 4바이트지만, wchar_t에서는 2개의 wchar_t(4바이트)다. 단순히 포인터를 캐스팅하는 것으로는 절대 변환되지 않는다. 코드 페이지 규칙에 따라 실제 비트 패턴을 다시 계산해야 하고, 그 역할을 Windows API MultiByteToWideChar가 담당한다.

2. MultiByteToWideChar의 동작 원리

이 함수의 시그니처는 다음과 같다.

int MultiByteToWideChar(
    UINT     CodePage,        // 원본 문자열의 코드 페이지 (예: CP_ACP)
    DWORD    dwFlags,         // 변환 플래그
    LPCCH    lpMultiByteStr,  // 변환할 char 문자열
    int      cbMultiByte,     // 입력 길이(바이트). -1이면 null 종료까지 자동
    LPWSTR   lpWideCharStr,   // 결과를 담을 wchar_t 버퍼
    int      cchWideChar      // 출력 버퍼 크기(문자 수). 0이면 필요한 크기만 반환
);

여기서 가장 중요한 트릭은 이 함수를 두 번 호출하는 패턴이다.

  1. 첫 번째 호출: 출력 버퍼 크기를 0으로(또는 버퍼를 NULL로) 넘기면, 변환에 필요한 wchar_t 개수를 반환값으로 알려준다. 이때 메모리를 쓰지 않는다.
  2. 두 번째 호출: 첫 호출에서 받은 크기만큼 버퍼를 할당한 뒤, 실제 변환을 수행한다.

이렇게 두 번 부르는 이유는 변환 후 필요한 길이를 미리 알 수 없기 때문이다. 멀티바이트 문자열은 문자마다 바이트 수가 달라서, 미리 정확한 wchar_t 개수를 계산하려면 함수에게 한 번 물어보는 것이 가장 안전하다.

3. 단계별 변환 함수 구현

이제 직접 변환 함수를 만들어 보자. 원리를 이해하기 쉽도록 변수명을 명확하게 붙였다.

#include <windows.h>
#include <string>

// char(멀티바이트) 문자열을 wchar_t(유니코드)로 변환한다.
// 반환된 포인터는 호출 측에서 반드시 delete[] 로 해제해야 한다.
wchar_t* ConvertToWide(const char* source)
{
    if (source == nullptr) return nullptr;

    // 1단계: 변환 후 필요한 wchar_t 개수를 먼저 조회한다.
    //        마지막 인자를 0으로 주면 버퍼를 채우지 않고 크기만 돌려준다.
    int wideLength = MultiByteToWideChar(
        CP_ACP,   // 시스템 기본 코드 페이지(한국어 환경이면 CP949)
        0,        // 기본 플래그
        source,
        -1,       // -1: null 종료 문자까지 자동 계산
        nullptr,
        0
    );

    if (wideLength == 0) return nullptr; // 변환 실패 방어

    // 2단계: 계산된 크기만큼 버퍼를 동적 할당한다.
    //        wideLength에는 null 종료 문자 자리도 포함되어 있다.
    wchar_t* result = new wchar_t[wideLength];

    // 3단계: 실제 변환을 수행한다.
    MultiByteToWideChar(
        CP_ACP,
        0,
        source,
        -1,
        result,
        wideLength
    );

    return result;
}

사용하는 쪽 코드는 다음과 같다.

const char* original = "프롬프트 아키텍트 Hello";
wchar_t* wide = ConvertToWide(original);

if (wide != nullptr)
{
    MessageBoxW(nullptr, wide, L"변환 결과", MB_OK);
    delete[] wide;  // ★ 반드시 해제 — 빼먹으면 메모리 누수
    wide = nullptr;
}

원본 글에서도 강조했듯, 함수가 new로 할당한 메모리는 호출한 쪽에서 delete[]로 해제해야 한다. 이 책임이 함수 밖으로 넘어가는 구조는 실수를 부르기 쉽다(뒤에서 더 안전한 방법을 다룬다).

4. 코드 페이지 선택: CP_ACP vs CP_UTF8

변환 품질을 좌우하는 가장 중요한 인자는 첫 번째 CodePage다. 자주 쓰는 값은 두 가지다.

  • CP_ACP: 시스템 기본 ANSI 코드 페이지. 한국어 Windows라면 CP949(EUC-KR 호환)다. 레거시 MBCS 데이터를 다룰 때 적합하다.
  • CP_UTF8: 입력 char 문자열이 UTF-8로 인코딩되어 있을 때 사용한다.

여기서 흔한 사고가 발생한다. 요즘 소스 파일이나 외부 데이터는 UTF-8인 경우가 많은데, 무심코 CP_ACP로 변환하면 글자가 깨진다. 입력 데이터가 어떤 인코딩인지 먼저 확인하고 그에 맞는 코드 페이지를 지정해야 한다.

// 입력이 UTF-8 문자열일 때
int len = MultiByteToWideChar(CP_UTF8, 0, utf8Source, -1, nullptr, 0);

5. 흔한 실수와 엣지 케이스

실무에서 반복적으로 터지는 함정들을 모았다.

  • 버퍼 크기 단위 혼동: cchWideChar바이트가 아니라 문자(wchar_t) 개수다. new wchar_t[wideLength]로 할당하는 것이 맞다. 바이트 기준으로 착각해 wideLength * 2로 할당할 필요는 없다.
  • null 종료 처리: cbMultiByte-1을 주면 null 종료 문자까지 포함해 길이를 계산한다. 반대로 길이를 직접 지정(strlen(s))하면 null이 빠져 결과 문자열이 종료되지 않을 수 있다. 특별한 이유가 없으면 -1을 권장한다.
  • 메모리 누수: 가장 흔한 버그. delete[]를 빼먹거나, 예외가 중간에 발생해 해제 코드에 도달하지 못하는 경우다.
  • 반환값 0 무시: 변환 실패 시 함수는 0을 반환한다. GetLastError()로 원인(잘못된 코드 페이지, 변환 불가 문자 등)을 확인할 수 있는데, 이 체크를 생략하면 깨진 데이터가 조용히 흘러간다.
  • char str = "Hello" 경고*: 최신 C++에서 문자열 리터럴은 const char*다. char*로 받으면 경고가 뜨므로 const를 붙이는 것이 안전하다.

6. 더 안전한 현대적 대안

수동 new/delete는 위험하다. 메모리 관리를 자동화하는 방법을 권한다.

#include <string>
#include <windows.h>

// std::wstring으로 반환하면 메모리 해제를 신경 쓸 필요가 없다.
std::wstring ToWString(const std::string& source, UINT codePage = CP_ACP)
{
    if (source.empty()) return std::wstring();

    int len = MultiByteToWideChar(
        codePage, 0,
        source.c_str(), (int)source.size(),
        nullptr, 0
    );

    std::wstring result(len, L'\0');           // 미리 버퍼 확보
    MultiByteToWideChar(
        codePage, 0,
        source.c_str(), (int)source.size(),
        &result[0], len
    );
    return result;  // RAII가 메모리를 알아서 정리한다
}

std::wstring을 쓰면 스코프를 벗어날 때 자동으로 메모리가 회수되어 누수 걱정이 사라진다. MFC 프로젝트라면 CStringWCA2W(ATL 변환 매크로)를 활용하는 것도 좋은 선택이다. 가능하다면 처음부터 프로젝트를 유니코드 빌드로 설정해 TCHAR/CString 일관성을 유지하면 변환 자체를 줄일 수 있다.

요약

  • char(MBCS)와 wchar_t(UTF-16)는 메모리 구조가 다르므로 캐스팅이 아닌 변환이 필요하다.
  • MultiByteToWideChar두 번 호출(크기 조회 → 실제 변환)하는 것이 표준 패턴이다.
  • 코드 페이지(CP_ACP vs CP_UTF8)를 입력 인코딩에 맞춰 정확히 지정해야 글자가 깨지지 않는다.
  • 수동 new 대신 std::wstring이나 ATL 변환 매크로로 메모리 누수를 원천 차단하라.

AI에게 물어볼 때 (프롬프트 팁)

이 주제를 ChatGPT나 Claude에게 물어볼 때, 막연히 "char를 wchar_t로 바꾸는 법 알려줘"라고 하면 일반론만 돌아온다. 다음처럼 환경·인코딩·제약을 명시하면 바로 쓸 수 있는 답을 얻는다.

  • 예시 1 — 맥락 고정: "Visual Studio MFC(유니코드 빌드, 한국어 Windows) 환경이다. UTF-8로 들어온 std::stringCStringW로 변환하는 함수를 작성해줘. 메모리 누수가 없도록 RAII 기반으로 하고, 변환 실패 시 처리도 포함해줘."

  • 예시 2 — 디버깅 의뢰: "다음 코드에서 한글만 깨지고 영문은 정상이다. 코드 페이지 문제로 의심되는데 원인과 수정안을 단계별로 설명해줘. [코드 첨부]"

  • 예시 3 — 비교 검토: "char→wchar_t 변환에서 MultiByteToWideChar 직접 호출, ATL의 CA2W, std::wstring_convert 세 방법의 장단점을 표로 비교하고, 신규 프로젝트라면 어느 것을 권하는지 근거와 함께 알려줘."

좋은 코드는 좋은 질문에서 나온다. 환경·제약·기대 출력 형식을 구체적으로 적을수록 AI의 답변 품질이 올라간다. 프롬프트 설계가 막막하다면 Prompt Architect의 분석기로 질문을 점검해 보는 것도 방법이다.