포트폴리오/[IoT 기반 피지컬 AI 교육일지]

[ C언어 IoT 프로젝트 ] #4 인코딩 에러수정, WinAPI 비동기 UI 개발 (28일차 기록)

개발자혜콩 2026. 3. 24. 10:25

1. 오늘 한 것

① 한글 인코딩(UTF-8 ↔ CP949 ↔ UTF-16) 구조 통일

  • Linux 서버(UTF-8)에서 보낸 한글 데이터가 Windows 클라이언트(CP949)의 로그창에서 깨지는 현상을 해결하기 위해 전 구간 인코딩 파이프라인 구축.
  • Windows API의 W 계열 함수(SetWindowTextW 등)가 요구하는 UTF-16(wchar_t) 형식에 맞춰, MultiByteToWideChar / WideCharToMultiByte 함수를 활용한 형변환 유틸리티 구현.
std::wstring Utf8ToWide(const std::string& s) {
    if (s.empty()) return L"";
    int n = MultiByteToWideChar(CP_UTF8, 0,
                s.c_str(), -1, nullptr, 0);
    std::wstring w(n, 0);
    MultiByteToWideChar(CP_UTF8, 0,
                s.c_str(), -1, &w[0], n);
    return w;
}

std::string WideToUtf8(const std::wstring& w) {
    if (w.empty()) return "";
    int n = WideCharToMultiByte(CP_UTF8, 0,
                w.c_str(), -1, nullptr, 0, nullptr, nullptr);
    std::string s(n, 0);
    WideCharToMultiByte(CP_UTF8, 0,
                w.c_str(), -1, &s[0], n, nullptr, nullptr);
    return s;
}
  • C언어 소스파일의 한글 리터럴 깨짐 방지를 위해 gcc -finput-charset=UTF-8 옵션 적용 및 #pragma execution_character_set("utf-8") 선언.
#pragma execution_character_set("utf-8")

 

  • 흐름 최종정리
서버 UTF-8 수신
    → Utf8ToWide() → wstring
    → SetWindowTextW() / AppendLogW()
    → 화면에 정상 출력

입력창에서 wstring 읽기
    → WideToUtf8()
    → send() 로 UTF-8 전송

 


② WinAPI 기반 비동기 네트워크 UI 스레드 분리

  • 문제: recv() 함수는 데이터가 올 때까지 대기(Blocking)하므로, 메인 UI 스레드에서 호출 시 프로그램이 완전히 응답 없음 상태가 됨.
  • 해결: 별도의 RecvThread를 생성하여 네트워크 수신을 전담하게 하고, 수신된 데이터를 PostMessage()를 통해 UI 스레드 메시지 큐(WM_APPENDLOG)로 안전하게 전달하는 비동기 아키텍처 구현.
DWORD WINAPI RecvThread(LPVOID) {
    NetPacket pkt;
    while (true) {
        int n = recv(g_sock, (char*)&pkt,
                     sizeof(pkt), MSG_WAITALL);
        if (n <= 0) break;

        std::string msg(pkt.payload.chat.message);

        // UI 스레드에 메시지 전달 (PostMessage는 스레드 안전)
        std::wstring* p = new std::wstring(Utf8ToWide(msg));
        PostMessage(g_hWnd, WM_APPENDLOG, 0, (LPARAM)p);
    }
    return 0;
}

③ 커스텀 UI 컨트롤 드로잉 (WM_PAINT, WM_DRAWITEM)

  • 센서 위젯: 상단 온도/습도/움직임 카드는 STATIC 컨트롤을 서브클래싱(CardProc)하여 배경색과 폰트를 상태별로 직접 렌더링.
LRESULT CALLBACK CardProc(HWND hWnd, UINT msg,
                           WPARAM wParam, LPARAM lParam) {
    if (msg == WM_PAINT) {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);
        RECT rc; GetClientRect(hWnd, &rc);

        // 배경색 (카드별로 다름)
        FillRect(hdc, &rc, hBrush);

        // 라벨 (작게)
        SelectObject(hdc, g_hCardFont);
        DrawTextW(hdc, label, -1, &labelRect, DT_CENTER);

        // 값 (굵게, 크게)
        SelectObject(hdc, g_hCardValFont);
        DrawTextW(hdc, value, -1, &valRect,
                  DT_CENTER | DT_VCENTER | DT_SINGLELINE);

        EndPaint(hWnd, &ps);
        return 0;
    }
    return CallWindowProc(g_origCard, hWnd, msg, wParam, lParam);
}
  • 긴급 점멸 배경: 낙상/SOS 발생 시 WM_ERASEBKGND를 오버라이딩하여 배경을 붉게 점멸시킴. 이때 자식 컨트롤(버튼, 로그창)이 같이 깜빡이는 현상을 막기 위해 RedrawWindow에 RDW_NOCHILDREN 플래그를 적용해 부드러운 UI 갱신 구현.
  • SOS 버튼: BS_OWNERDRAW 속성과 WM_DRAWITEM 메시지를 활용하여 클릭 시 색상이 어두워지는 직관적인 피드백 버튼 직접 구현.
case WM_DRAWITEM: {
    DRAWITEMSTRUCT* dis = (DRAWITEMSTRUCT*)lParam;
    if (dis->CtlID == ID_SOS) {
        HBRUSH hbr = CreateSolidBrush(
            (dis->itemState & ODS_SELECTED)
            ? RGB(140,10,10) : RGB(210,30,30));
        FillRect(dis->hDC, &dis->rcItem, hbr);
        DeleteObject(hbr);
        SetTextColor(dis->hDC, RGB(255,255,255));
        SetBkMode(dis->hDC, TRANSPARENT);
        DrawTextW(dis->hDC, L"SOS", -1, &dis->rcItem,
                  DT_CENTER | DT_VCENTER | DT_SINGLELINE);
        return TRUE;
    }
}

2. 문제 / 헷갈린 점

① 인코딩 지옥의 원인 파악 지연

  • 상황: 서버에서 정상적으로 보낸 한글이 UI에서 "거주자 : 안녕하세요" 처럼 박살 나서 출력됨.
  • 해결: 세 가지 환경의 기본 인코딩이 전부 다르다는 것을 인지하지 못했음. Linux는 UTF-8, Windows 시스템은 CP949, WinAPI 내부 함수는 UTF-16을 쓴다. **"통신 구간은 무조건 UTF-8로 통일하고, 클라이언트의 화면 출력/입력 직전에만 UTF-16(Wide Char)으로 변환한다"**는 원칙을 세워 해결함.

② 네트워크 스레드와 UI 갱신 충돌

  • 상황: RecvThread에서 직접 UI 핸들(SetWindowText)을 건드리려 하니 동작이 불안정해짐.
  • 해결: 스레드 간 자원 접근은 위험하다.
// 수신 스레드: new로 메모리를 할당해 포인터를 메시지 큐에 던짐 (PostMessage는 스레드 안전)
std::wstring* p = new std::wstring(Utf8ToWide(msg));
PostMessage(g_hWnd, WM_APPENDLOG, 0, (LPARAM)p);

// UI 스레드 (WndProc): 큐에서 꺼내어 UI를 갱신하고 메모리 해제
case WM_APPENDLOG: {
    std::wstring* p = (std::wstring*)lParam;
    if (p) {
        // ... (로그창 갱신 로직)
        delete p;
    }
}

 

③ WM_ERASEBKGND 점멸 시 자식 컨트롤 증발

  • 상황: 알람 발생 시 화면을 빨갛게 칠하자, 위에 올려둔 버튼과 로그창이 덮여서 사라지거나 심하게 깜빡임.
  • 해결: 화면 무효화 시 자식 영역을 제외하는 플래그 적용.
// 점멸 중: 자식 컨트롤(버튼, 텍스트박스)은 건드리지 않고 배경만 갱신
RedrawWindow(hWnd, &alertRect, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_NOCHILDREN);

3. 오늘 배운 핵심

  • 네트워크 통신의 한글 인코딩은 무조건 UTF-8이 표준이다. 플랫폼에 종속된 인코딩(CP949)은 통신 구간에 태우지 말고 엔드포인트에서 형변환(Utf8ToWide)으로 처리해야 한다.
  • UI 스레드와 작업 스레드는 철저히 분리해야 한다. 네트워크 수신 같은 블로킹 작업은 별도 스레드로 빼고, PostMessage를 통한 비동기 메시지 전달 방식으로 UI를 갱신해야 창이 멈추지 않는다.
  • WinAPI는 날것의 제어권을 제공한다. 버튼 클릭 피드백부터 배경 깜빡임 방지까지 모든 핸들을 직접 제어해야 하지만, 그만큼 OS의 메시지 루프와 화면 렌더링(WM_PAINT) 원리를 투명하게 이해할 수 있다.

4. 다음 목표

  • 그동안 작성한 아두이노(센서), 리눅스(서버/DB), 윈도우(클라이언트 UI) 3계층 코드를 모두 통합하여 End-to-End 전체 시스템 테스트 진행.
  • 실시간 낙상 감지(초음파) 발생부터 DB alert_logs 인서트, 그리고 클라이언트 화면의 붉은 점멸 및 경고음 발생까지의 딜레이(Latency) 측정 및 최적화.