[시리즈 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

[시리즈 2편] 실무로 배우는 메시지 큐 - RabbitMQ

[시리즈 2편] 실무로 배우는 메시지 큐 - RabbitMQ

들어가며 [시리즈1]에서는 프로세스 내부 메시지 큐를 다뤘습니다. 이번엔 네트워크 메시지 큐인 RabbitMQ를 다룹니다. RabbitMQ 공식 문서나 기술 블로그는 많지만, 실무에서 어떻게 사용하는지에 대한 글은 의외로 적습니다. "Producer가 뭐고 Consumer가 뭔지는 알겠는데, 그래서 실제로는 어떻게 쓰는데?" 이번 글에서는 우리 MES 시스템에서 RabbitMQ를 어떻게 활용하고 있는지 실제 코드와 함께 공유합니다. 우리

By Jeonggil
[시리즈 2편] 그림으로 풀어낸 SaaS 알림 시스템

[시리즈 2편] 그림으로 풀어낸 SaaS 알림 시스템

이 글은 1편 - 그림으로 풀어낸 SaaS 알림 시스템의 후속편입니다. 들어가며 1편에서는 설비 연속 OFF 알림 기능의 핵심 로직과 어떤식으로 해결했는지 그림으로 알아봤습니다. 이번 글에서는 실무에서 마주한 진짜 고민들을 공유합니다: * 왜 3개의 새로운 테이블이 필요했나? * 어떻게 확장 가능한 구조를 만들었나? * SMS 14원짜리 알림이 왜 무서운가? * 운영 레벨로 나가기까지 무엇을 준비했나?

By Jeonggil
[시리즈 1편] 그림으로 풀어낸 SaaS 알림 시스템

[시리즈 1편] 그림으로 풀어낸 SaaS 알림 시스템

들어가며 제조업 IoT 플랫폼에서 N대 이상의 설비를 실시간으로 모니터링하고, 설비가 연속으로 꺼졌을 때 담당자에게 즉시 알림을 보내는 기능을 개발하게 되었습니다. 데이터는 실시간으로 쌓이지만, 설비이상을 체크하는 스케줄러 주기는 1분으로 설정하였습니다. 시스템 아키텍처 기존 인프라와 Push 기능은 이미 구축되어 있었습니다. 저는 중간에 들어가는 Alert Scheduler만 구현하면 되는 상황이었습니다. ┌──────────────────────────────────────────────────────────┐ │ 설비 IoT 센서 (실시간)

By Jeonggil
2025, ISACA Korea, Conference

2025, ISACA Korea, Conference

ISACA_2025_공동학술대회_발표자료ISACA_2025_공동학술대회_발표자료.pdf244 MBdownload-circle 이한수, InfoBank Partner * 스타트업 * 자유로움, 행복, 만족하는 삶 * TIPS 정부 지원 * 집중 * Always 수익 개선 * Like Sports (배려, 매너, 열정과 노력, 승복과 존중) 공성배, Megazone Vice P * Ai Native * 실행속도 * 비용 통제 * MIT 최근 통계 * 95% Projects failed * AI * C Level의

By Hyonsok