[시리즈 1편] 실무로 배우는 메시지 큐 - Windows Message Loop

[시리즈 1편] 실무로 배우는 메시지 큐 - Windows Message Loop

들어가며

이 글은 "실무로 배우는 메시지 큐" 시리즈의 첫 번째 글입니다.

실무에서 발견한 문제를 해결하는 과정에서, IME 입력 문제와 해결 과정을 공유합니다.

메시지 큐는 RabbitMQ, Kafka 같은 네트워크 레벨만 있는 게 아닙니다. 우리가 매일 쓰는 Windows 애플리케이션도 메시지 큐 기반으로 동작합니다.

  • 시리즈1 (이 글): 프로세스 내부의 메시지 큐 - Windows Message Loop의 이해와 활용
  • 시리즈2: 네트워크 레벨의 메시지 큐 - RabbitMQ

문제 상황

MES 시스템 운영 중 고객으로부터 이런 문의가 반복적으로 들어왔습니다.

"설비코드 검색이 안 돼요. 분명히 SS0002 입력했는데..."

확인해보니 실제 입력된 값은 SS0002 (전각 문자)였습니다. 작업자들이 메모장, 엑셀, MES를 오가며 작업하다 보니 IME 상태가 전각 모드로 고정되어 발생한 문제였습니다.

반각: SS0002, ABC123  → 검색 가능
전각: SS0002, ABC123  → 검색 불가

처음엔 특정 메뉴에만 적용하려 했으나, 여러 화면에서 동일한 문제가 발생해 전체 시스템 레벨에서 해결하기로 했습니다.


시도한 방법들

| 방법                      | 결과 | 문제점                                     |
| ------------------------- | ---- | ------------------------------------------ |
| TextChanged 이벤트        | ❌   | 무한 루프 (Text 변경 → 이벤트 발생 → 반복) |
| KeyPress 이벤트           | ❌   | IME 조합 중 문자 캐치 못함                 |
| IMessageFilter (WM_CHAR)  | ❌   | WM_IME_CHAR는 조합 완료 후에만 도착        |
| Form.Activated + IME 리셋 | △    | 메인폼만 동작, 팝업은 안 됨                |
| **Application.Idle**      | ✅   | **모든 폼에서 동작, 입력 직후 변환**       |

해결책: Application.Idle 이벤트

Windows 메시지 큐 아키텍처

Windows Forms는 메시지 큐 기반으로 동작합니다. 모든 사용자 입력(키보드, 마우스)은 메시지로 변환되어 큐에 쌓이고, Application.Run이 순차적으로 처리합니다.

┌─────────────────────────────────────────────────────┐
│              Windows Message Queue                  │
│  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐            │
│  │ WM_  │→ │ WM_  │→ │ WM_  │→ │ WM_  │  ...       │
│  │ KEY  │  │ IME  │  │MOUSE │  │PAINT │            │
│  │ DOWN │  │ CHAR │  │ MOVE │  │      │            │
│  └──────┘  └──────┘  └──────┘  └──────┘            │
└─────────────────────────────────────────────────────┘
         │
         ▼
    ┌─────────────────┐
    │Application.Run  │  ← 메시지 루프 (Message Pump)
    │  (Main Thread)  │
    └─────────────────┘
         │
         ├─→ 큐에 메시지 있음 → 메시지 처리
         │                      (WM_IME_CHAR → TextBox에 'A' 입력)
         │
         └─→ 큐가 비어있음 → Application.Idle 발생 ⚡
                              (우리 코드 실행: 'A' → 'A' 변환)

핵심 아이디어

Application.Idle은 메시지 큐가 비어있을 때, 즉 모든 입력 처리가 완료된 직후 발생합니다.

// Application.Run 내부 동작 (의사 코드)
while (프로그램 실행 중)
{
    if (메시지큐에 메시지 있음)
        메시지 처리();  // WM_KEYDOWN → TextBox 입력
    else
        Application.Idle 발생();  // ← 여기서 변환!
}

타이밍 비교: 왜 Idle이 완벽한가?

타임라인: 전각 'A' 입력 시 각 이벤트 발생 시점
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[입력 시작]    [IME 조합]    [문자 확정]    [화면 업데이트]
     │             │             │                │
     ▼             ▼             ▼                ▼
─────●─────────────●─────────────●────────────────●──────→ 시간

     ❌ KeyPress      (너무 빠름 - IME 조합 전이라 캐치 못함)

                  ❌ IMessageFilter (이미 조합 완료 후)

                                ❌ TextChanged (변환 시 다시 발생 → 무한루프)

                                               ✅ Application.Idle
                                               (큐 비었음 = 입력 완료)

Application.Idle이 완벽한 이유:

  • ✅ 입력이 완료된 후 호출 (TextBox에 이미 표시된 상태)
  • ✅ 한 번만 실행 (무한루프 없음)
  • ✅ 모든 폼에서 자동 동작

구현 코드

Program.cs

static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    // 전각→반각 자동 변환 등록
    Application.Idle += FullWidthConverter.Application_Idle;

    Application.Run(new MainForm());
}

FullWidthConverter.cs

public static class FullWidthConverter
{
    public static void Application_Idle(object sender, EventArgs e)
    {
        try
        {
            Control focused = GetFocusedControl();
            if (focused == null) return;

            // 포커스된 컨트롤의 텍스트 변환
            if (focused is TextBox tb)
                ConvertText(tb, () => tb.Text, s => tb.Text = s,
                            () => tb.SelectionStart, v => tb.SelectionStart = v);
            // TextEdit, ComboBoxEdit 등 동일 패턴
        }
        catch { }
    }

    private static string ToHalfWidth(string s)
    {
        if (string.IsNullOrEmpty(s)) return s;
        char[] c = s.ToCharArray();

        for (int i = 0; i < c.Length; i++)
        {
            // 전각 공백 → 반각
            if (c[i] == '\u3000') c[i] = ' ';
            // 전각 ASCII → 반각 (0xFEE0 차이)
            else if (c[i] >= '\uFF01' && c[i] <= '\uFF5E')
                c[i] = (char)(c[i] - 0xFEE0);
        }

        return new string(c);
    }
}

전각↔반각 변환 원리

전각 'A' = U+FF21
반각 'A'  = U+0041
차이값    = 0xFEE0  ← 이 값만 빼면 변환 완료

실행 흐름 (시퀀스 다이어그램)

사용자가 전각 'A' 입력 시

┌─────────┐         ┌──────────┐         ┌─────────────┐         ┌──────────────┐
│  사용자 │         │ Windows  │         │ Message Queue│        │FullWidth     │ 
│         │         │   OS     │         │             │         │Converter     │
└────┬────┘         └────┬─────┘         └──────┬──────┘         └──────┬───────┘
     │                   │                      │                       │
     │ 'A' 키 입력      │                       │                       │
     │──────────────────>│                       │                       │
     │                   │                       │                       │
     │                   │ WM_IME_CHAR 생성      │                       │
     │                   │──────────────────────>│                       │
     │                   │                       │                       │
     │                   │    메시지 처리 시작    │                       │
     │                   │<──────────────────────│                       │
     │                   │                       │                       │
     │                   │  TextBox.Text = "A"  │                       │
     │                   │  (화면에 전각 표시)    │                       │
     │                   │                       │                       │
     │                   │  큐가 비어있음!        │                       │
     │                   │  Application.Idle 발생│                       │
     │                   │───────────────────────────────────────────────>│
     │                   │                       │                       │
     │                   │                       │    'A' → 'A' 변환     │
     │                   │                       │    TextBox.Text = "A" │
     │                   │                       │                       │
     │    화면에 'A' 표시 (너무 빨라서 'A'는 못 봄)                        │
     │<───────────────────────────────────────────────────────────────────│
     │                   │                       │                       │

소요 시간: 전체 과정이 약 10ms 이내에 완료되어 사용자는 전각 입력을 인지하지 못합니다.

붙여넣기(Ctrl+V)도 동일: WM_PASTE 메시지 처리 후 Idle 발생 → 변환


성능 영향

Q: Idle 이벤트가 너무 자주 호출되지 않나요?

실제 측정 결과 CPU 사용량 0.01% 미만입니다.


마치며

적용 결과

  • ✅ 전각 문자 입력 문의 100% 해소
  • ✅ 사용자 추가 조작 불필요 (자동 변환)
  • ✅ 기존 코드 수정 없이 Program.cs 한 줄 추가로 전체 적용

핵심 교훈

"타이밍이 중요하다"

  • TextChanged: 너무 빠름 (변경 중)
  • KeyPress: 너무 빠름 (조합 전)
  • Application.Idle: 딱 맞음 (입력 완료 직후)

Windows 메시지 루프를 이해하면 비슷한 UI 타이밍 문제를 해결할 때 유용합니다.


참고 자료


다음 글 예고
이번 글은 프로세스 내부 메시지 큐를 다뤘습니다.
다음 편에서는 네트워크 간 메시지 큐인 RabbitMQ를 다룹니다.

Read more

Lumen - AI Agent를 위한 지속 가능한 두뇌

Lumen - AI Agent를 위한 지속 가능한 두뇌

왜 만들었는가 AI 에이전트는 모든 대화를 기억상실증 상태에서 시작합니다. Claude Code, Cursor, Codex, Mastra 하네스, LangChain 파이프라인 — 이 도구들은 세상을 알지만 당신의 세상은 전혀 모릅니다. 당신이 읽은 200편의 논문, 당신이 출시하는 코드베이스, 지난 분기에 내린 아키텍처 결정, 새벽 2시에 그 버그를 잡아냈을 때 마침내 통했던 트래젝토리. 모든 세션이 같은 컨텍스트를

By Sardor Madaminov

200 OK, 텅 빈 body — Starlette Race Condition 장애 분석기

발생일: 2026-04-23 / 해결일: 2026-04-27 영향 범위: report-dev.machine365.ai 전체 API 들어가며 API가 200 OK를 반환하는데 body가 비어있다. 프론트엔드에는 아무것도 안 뜨고, Swagger UI(/docs)도 빈 화면. 그런데 로컬에서 돌리면 멀쩡하다. 배경은 이랬다. 미터링(Metering) 기능을 만들면서 API 호출 로그를 수집할 미들웨어를 작성했다. Spring Boot 백엔드에 먼저 적용하고, Python

By Jeonggil