메뉴 닫기

MFC에서 사용자 정의 메시지를 등록하고 처리하는 방법


MFC에서 사용자 정의 메시지를 등록하고 처리하는 방법

📌 메시지 기반 구조를 확장하는 가장 확실한 방법, 직접 정의하는 메시지 처리 기법

윈도우 프로그래밍에서는 메시지 처리가 핵심입니다.
그 중에서도 사용자 정의 메시지는 프로그램 구조를 보다 유연하고 확장성 있게 만드는 데 꼭 필요한 기능이죠.
MFC에서는 #define WM_USER + 100과 같은 방식으로 메시지를 정의하고, 메시지 맵에 등록해 커스텀 이벤트를 처리할 수 있습니다.
이 글에서는 사용자 정의 메시지를 만드는 방법부터 메시지 맵 등록, 메시지 전송 및 처리까지 전 과정을 단계별로 소개해 드릴게요.

MFC의 기본 메시지 시스템만으로는 해결하기 어려운 커뮤니케이션 구조를 설계할 때, 사용자 정의 메시지를 활용하면 모듈 간 결합도를 낮추고 이벤트 중심의 흐름 제어가 훨씬 쉬워집니다.
초보자도 쉽게 따라할 수 있도록 예제와 함께 설명드릴 테니, 윈도우 메시지 처리 구조에 대해 제대로 이해하고 싶은 분이라면 꼭 끝까지 읽어보세요.







🔗 사용자 정의 메시지란?

Windows 운영체제는 메시지 기반 구조로 동작하며, 모든 UI 상호작용과 이벤트는 메시지를 통해 처리됩니다.
이러한 시스템 메시지 외에 개발자가 직접 정의해서 사용할 수 있는 메시지를 사용자 정의 메시지라고 합니다.

기본적으로 Windows에서는 수많은 시스템 예약 메시지가 존재합니다.
대표적으로 WM_COMMAND, WM_PAINT, WM_DESTROY 등이 있으며, 이들은 OS나 컨트롤에서 자동으로 발생시켜줍니다.

하지만 개발자가 직접 메시지를 만들어 윈도우 간 통신이나 비동기 이벤트 처리에 활용하고 싶을 때는 사용자 정의 메시지를 사용합니다.
예를 들어, 백그라운드 작업 완료 통보나 특정 명령 수행 요청을 별도로 처리하고 싶을 때 매우 유용하죠.

💎 핵심 포인트:
사용자 정의 메시지를 활용하면 모듈 간 강한 결합 없이 이벤트 기반 처리를 구현할 수 있어 코드 유지 보수성과 확장성이 향상됩니다.

📌 사용자 정의 메시지를 사용하는 이유

  • 🧩UI 이벤트 외의 커스텀 상황에 대한 처리 가능
  • 🔄다이얼로그 간 통신 또는 모듈 간 제어에 활용
  • ⚙️비동기 이벤트의 안전한 처리 구조 구현

다음 STEP에서는 이러한 사용자 정의 메시지를 어떻게 선언하고 사용할 수 있는지, 구체적인 WM_USER 기반 메시지 정의 방법에 대해 알아보겠습니다.


🛠️ WM_USER 기반 메시지 정의하기

Windows에서는 사용자 정의 메시지를 만들 때 WM_USER 이후의 메시지 번호를 사용합니다.
WM_USER는 시스템이 예약한 메시지 범위를 피해, 개발자가 안전하게 사용할 수 있도록 정의된 상수입니다.

보통 메시지를 정의할 때는 다음과 같은 방식으로 헤더 파일 또는 클래스 상단에 선언합니다.

CODE BLOCK
// 사용자 정의 메시지 정의 예시
#define WM_MY_CUSTOM_MSG    (WM_USER + 100)
#define WM_MY_DATA_READY    (WM_USER + 101)

이처럼 번호를 순차적으로 증가시키면서 정의하면, 여러 개의 사용자 정의 메시지를 충돌 없이 사용할 수 있습니다.
또한 각 메시지의 의미가 명확하도록 이름을 직관적으로 붙이는 것이 중요합니다.

📌 WM_USER 이후 범위에 정의해야 하는 이유

Windows는 내부적으로 0 ~ WM_USER-1까지의 범위를 시스템 메시지로 예약하고 있습니다.
따라서 개발자가 메시지를 직접 만들 때는 반드시 WM_USER 이상의 값을 사용해야 다른 시스템 메시지와 충돌하지 않습니다.

⚠️ 주의: WM_USER 이후의 번호라도 너무 낮은 값은 MFC 내부 메시지와 겹칠 수 있으므로, 일반적으로 WM_USER + 100 이상부터 사용하는 것이 좋습니다.

정의한 메시지는 이후 메시지 맵에 등록해 핸들링을 할 수 있으며, 메시지 송신자 쪽에서는 SendMessage 또는 PostMessage를 통해 메시지를 보낼 수 있습니다.







⚙️ 메시지 맵에 등록하는 방법

MFC에서는 정의한 사용자 메시지를 처리하기 위해 반드시 해당 메시지를 메시지 맵(message map)에 등록해야 합니다.
메시지 맵은 클래스 내부에서 어떤 메시지를 어떤 함수로 처리할지를 연결해주는 역할을 하죠.

예를 들어, WM_MY_CUSTOM_MSG라는 메시지를 처리하기 위한 코드는 다음과 같이 작성할 수 있습니다.

CODE BLOCK
// 헤더 파일 (예: MyDialog.h)
afx_msg LRESULT OnMyCustomMessage(WPARAM wParam, LPARAM lParam);

// 메시지 맵 등록 (CPP 파일 내부)
BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
    ON_MESSAGE(WM_MY_CUSTOM_MSG, OnMyCustomMessage)
END_MESSAGE_MAP()

// 메시지 핸들러 구현
LRESULT CMyDialog::OnMyCustomMessage(WPARAM wParam, LPARAM lParam)
{
    AfxMessageBox(_T("사용자 메시지 수신 완료!"));
    return 0;
}

핸들러 함수는 반드시 LRESULT 반환형WPARAM, LPARAM 인자를 가져야 하며, ON_MESSAGE 매크로를 통해 메시지 맵에 등록해야 합니다.

📌 메시지 맵 등록 없이 처리할 수 있을까?

메시지 맵에 등록하지 않으면 해당 메시지를 수신해도 아무 동작이 일어나지 않습니다.
즉, 등록과 핸들러 구현은 반드시 함께 이루어져야 하며, 특히 클래스가 CWnd 또는 CDialog 기반일 때 메시지 맵이 활성화됩니다.

💎 핵심 포인트:
사용자 정의 메시지를 선언했다면, 반드시 ON_MESSAGE 매크로로 메시지 맵에 등록하고 핸들러 함수는 WPARAM/LPARAM 인자 구조를 따라야 합니다.


🔌 메시지 전송 및 처리 실전 예제

지금까지 사용자 정의 메시지를 선언하고 메시지 맵에 등록하는 방법을 알아봤습니다.
이번에는 실제로 메시지를 전송하고 처리하는 예제를 통해 전체 흐름을 이해해볼 차례입니다.

아래 예시는 두 개의 다이얼로그 간 통신을 가정하며, 메인 다이얼로그가 서브 다이얼로그로 메시지를 보내 텍스트를 업데이트하는 구조입니다.

📌 사용자 정의 메시지 정의

CODE BLOCK
#define WM_UPDATE_TEXT (WM_USER + 200)

📌 서브 다이얼로그 메시지 맵 등록

CODE BLOCK
BEGIN_MESSAGE_MAP(CSubDialog, CDialogEx)
    ON_MESSAGE(WM_UPDATE_TEXT, OnUpdateText)
END_MESSAGE_MAP()

LRESULT CSubDialog::OnUpdateText(WPARAM wParam, LPARAM lParam)
{
    SetDlgItemText(IDC_STATIC_TEXT, _T("업데이트 완료!"));
    return 0;
}

📌 메인 다이얼로그에서 메시지 전송

CODE BLOCK
// 포인터 또는 HWND를 통해 메시지 전송
subDlg->SendMessage(WM_UPDATE_TEXT, 0, 0);
// 또는 비동기 방식으로
PostMessage(subDlg->GetSafeHwnd(), WM_UPDATE_TEXT, 0, 0);

이 구조를 활용하면 모듈 간 직접 호출 없이 느슨한 연결(loose coupling)을 유지할 수 있어 유지보수에 매우 유리합니다.
또한 메시지 기반 설계를 통해 다양한 이벤트 흐름을 유연하게 처리할 수 있죠.

💡 TIP: 메시지를 보낼 때 대상 윈도우의 유효성을 항상 확인해야 하며, GetSafeHwnd()를 사용하는 것이 안전한 방법입니다.







💡 사용자 정의 메시지 활용 팁

사용자 정의 메시지는 프로그램의 유연성과 확장성을 높이기 위한 중요한 도구입니다.
단순히 메시지를 보낸다고 끝나는 것이 아니라, 어떻게 설계하고 언제 사용하는가가 더 중요합니다.

📌 메시지 번호 충돌 방지

WM_USER 기반 메시지는 서로 다른 클래스나 모듈 간에도 공유될 수 있으므로, 전역 메시지 번호 충돌을 피하는 것이 중요합니다.
이를 위해 프로젝트 단위로 명명 규칙번호 범위를 정해두면 좋습니다.

📌 쓰레드 간 통신 시 주의사항

다중 쓰레드 환경에서는 PostMessage를 주로 사용하지만, 메시지 큐가 없는 쓰레드에는 메시지가 전달되지 않습니다.
항상 메시지를 받을 수 있는 UI 쓰레드 또는 메시지 루프가 있는 쓰레드를 대상으로 보내야 합니다.

  • 🔢WM_USER + 100부터 시작하여 번호 체계를 모듈 단위로 분리
  • 🔄SendMessage는 동기, PostMessage는 비동기임을 명확히 구분
  • 📥받는 쪽은 반드시 ON_MESSAGE로 등록하고 핸들러 구현 필수
  • 🧵멀티쓰레드 환경에서는 메시지 루프 유무 확인 필요

💎 핵심 포인트:
사용자 정의 메시지는 단순한 트릭이 아니라, 아키텍처 구성의 핵심 도구입니다. 설계 단계에서부터 메시지의 흐름과 범위를 정리해 두는 습관을 들이세요.


자주 묻는 질문 (FAQ)

사용자 정의 메시지는 꼭 WM_USER 이후로만 써야 하나요?
네. 시스템 메시지와 충돌을 피하기 위해 반드시 WM_USER 이상의 값을 사용하는 것이 원칙입니다.
WM_USER + 1 같은 번호를 여러 곳에서 써도 되나요?
아니요. 메시지 번호가 중복되면 다른 메시지와 충돌하거나 예상하지 못한 동작이 발생할 수 있습니다. 번호는 고유하게 관리하세요.
SendMessage와 PostMessage는 사용자 메시지에도 사용 가능한가요?
물론입니다. 사용자 정의 메시지를 보내는 가장 일반적인 방식이 바로 SendMessage 또는 PostMessage입니다.
메시지를 받은 뒤 어떤 값을 반환해야 하나요?
일반적으로 0을 반환하면 되고, 특별한 의미가 있는 메시지라면 처리 상태나 결과 값을 반환해도 됩니다.
CWnd 클래스가 아니면 사용자 메시지를 받을 수 없나요?
CWnd 기반 클래스여야 메시지 맵을 사용할 수 있습니다. 그렇지 않으면 메시지 루프를 직접 구현해야 합니다.
WM_APP은 WM_USER와 어떻게 다른가요?
WM_APP은 WM_USER보다 더 안전한 사용자 메시지 영역으로, 애플리케이션 전용으로 충돌 가능성이 더 낮습니다.
PostMessage로 보낸 메시지가 유실될 수 있나요?
대상 윈도우가 이미 종료되었거나 메시지 큐가 비정상 상태일 경우 메시지가 처리되지 않고 유실될 수 있습니다.
사용자 메시지를 로그로 확인할 수 있나요?
메시지 핸들러 내부에서 TRACE, 로그 출력 함수 등을 활용해 수신 여부와 전달 값 등을 디버깅할 수 있습니다.


📌 확장 가능한 메시지 기반 구조 설계를 위한 실전 팁

MFC에서 사용자 정의 메시지를 활용하는 방법은 단순한 기능 확장을 넘어 소프트웨어 아키텍처의 구조를 개선하는 핵심 전략이 될 수 있습니다.
WM_USER 이후의 메시지를 안전하게 정의하고, 메시지 맵에 등록하여 모듈 간 결합도를 낮춘다면 복잡한 UI나 백그라운드 로직도 깔끔하게 관리할 수 있죠.

이번 글에서는 사용자 정의 메시지의 개념부터 시작해 정의, 등록, 전송, 처리까지 전 과정을 상세히 살펴보았습니다.
또한 메시지 번호 충돌 방지 방법, 쓰레드 간 통신 시 유의사항, 실전 예제까지 함께 다뤄보며 실무에서 바로 적용할 수 있는 내용 위주로 구성했습니다.

앞으로 MFC 기반 프로젝트에서 사용자 정의 메시지를 통해 더욱 명확하고 유연한 이벤트 흐름을 구축해보세요.
단일 책임 원칙을 지키고, 모듈 간 독립성을 높이는 데에도 큰 도움이 될 것입니다.


🏷️ 관련 태그 : MFC, 사용자정의메시지, 메시지맵, WM_USER, SendMessage, PostMessage, 메시지통신, CDialog, MFC프로그래밍, 메시지기반구조