더블 버퍼링 깜빡임 없는 부드러운 화면 -완-

더블 버퍼링 깜빡임 없는 화면 그리기

더블 버퍼링 화면에 바로 그리지 않고, 메모리 공간에 먼저 그린 뒤 한 번에 출력하는 구조를 사용하면 깜빡임을 거의 완전히 제거할 수 있습니다.
화면에 도형을 그리기 시작하면 바로 눈에 들어오는 문제가 있습니다. 바로 화면이 번쩍이며 깜빡이는 현상입니다. 특히 마우스로 계속 그리거나 자주 갱신할수록 더 두드러지게 나타납니다.

왜 화면이 깜빡이는가

깜빡임의 원인은 단순합니다. 화면을 지우고 다시 그리기 때문입니다.

기본 흐름은 다음과 같습니다.

  • 배경 지우기 (WM_ERASEBKGND)
  • WM_PAINT에서 다시 그림

이 과정이 반복되면서 사용자에게는 깜빡임으로 보이게 됩니다.

흔한 오해: 코드를 줄이면 해결될까

코드를 줄이거나 그리기 횟수를 줄이는 방법은 근본적인 해결이 되지 않습니다.

  • 일부 상황에서만 완화됨
  • 구조가 그대로라면 깜빡임 유지

문제는 “얼마나 많이 그리느냐”가 아니라 “어떻게 그리느냐”입니다.

해결 방법: 더블 버퍼링 구조 이해하기

더블 버퍼링은 보이지 않는 메모리 공간에 먼저 그림을 완성한 뒤, 결과만 화면에 한 번에 출력하는 방식입니다.

핵심 흐름은 다음과 같습니다.

  1. 메모리 DC 생성
  2. 메모리 DC에 전체 화면 그리기
  3. BitBlt로 한 번에 출력

이 방식은 중간 과정이 화면에 보이지 않기 때문에 깜빡임이 사라집니다.

실제 구현: 더블 버퍼링 적용하기

case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);

    RECT rc;
    GetClientRect(hwnd, &rc);

    HDC memDC = CreateCompatibleDC(hdc);
    HBITMAP memBitmap = CreateCompatibleBitmap(hdc, rc.right, rc.bottom);
    HBITMAP oldBitmap = (HBITMAP)SelectObject(memDC, memBitmap);

    // 배경 채우기
    FillRect(memDC, &rc, (HBRUSH)(COLOR_WINDOW+1));

    // 실제 그리기
    Ellipse(memDC, g_x - 20, g_y - 20, g_x + 20, g_y + 20);

    // 화면에 출력
    BitBlt(hdc, 0, 0, rc.right, rc.bottom, memDC, 0, 0, SRCCOPY);

    // 리소스 복구
    SelectObject(memDC, oldBitmap);
    DeleteObject(memBitmap);
    DeleteDC(memDC);

    EndPaint(hwnd, &ps);
}
return 0;

추가로 깜빡임을 완전히 줄이려면 다음도 함께 적용하는 것이 좋습니다.

  • WM_ERASEBKGND에서 return 1 처리
  • 배경을 한 번만 그리도록 통제

이 두 가지를 적용하면 이중으로 지워지는 문제를 방지할 수 있습니다.

마무리

이 단계까지 오면 WinAPI의 핵심 흐름을 모두 경험한 상태입니다.

  • 창 생성
  • 메시지 처리
  • 입력 처리
  • GDI 그리기
  • UI 컨트롤
  • 구조 개선
  • 렌더링 최적화

이제 단순 예제가 아니라, 실제 프로그램을 만들 수 있는 기반이 완성되었습니다

구조 개선 하기: 코드 정리와 리팩토링

구조 개선 : 유지보수 가능한 코드로 리팩토링하기

구조 개선 과정은 선택이 아니라 필수입니다. 메시지 기반 구조 특성상 모든 로직을 WndProc에 넣으면 코드가 빠르게 복잡해지고 유지보수가 어려워집니다. 핵심은 이벤트 분기, 상태 관리, 기능 분리를 명확히 나누는 것입니다.
버튼과 입력창까지 구현했다면 이제 코드가 길어지고 수정이 점점 어려워졌을 것입니다. 이 단계에서는 기능을 확장하기 전에 구조를 정리해야 이후 작업이 훨씬 수월해집니다.

왜 구조 개선 이 필수 단계가 되는가

처음에는 WndProc 하나에 모든 코드를 넣어도 문제가 없어 보입니다. 하지만 기능이 늘어나면 다음 문제가 바로 나타납니다.

  • 메시지 처리 코드가 계속 증가
  • 전역 변수 관리가 어려워짐
  • UI와 로직이 뒤섞임

WinAPI는 메시지 기반 구조이기 때문에 한 곳에 로직이 몰리면 수정 시 영향 범위를 파악하기 어려워집니다.

1 – WndProc은 이벤트 분기만 담당하기

WndProc은 “무슨 일이 발생했는지 판단하는 역할”까지만 담당해야 합니다. 실제 동작은 별도의 함수로 분리하는 것이 핵심입니다.

case WM_LBUTTONDOWN:
    OnClick(hwnd, lParam);
    return 0;
void OnClick(HWND hwnd, LPARAM lParam)
{
    int x = LOWORD(lParam);
    int y = HIWORD(lParam);

    // 실제 처리 로직
}

이렇게 하면 WndProc은 이벤트 라우터 역할만 하게 되어 코드가 훨씬 읽기 쉬워집니다.

2 – 전역 변수 대신 상태 구조체 사용하기

전역 변수는 초기에는 편하지만 규모가 커질수록 문제를 일으킵니다. 상태를 하나의 구조로 묶는 것이 훨씬 안전합니다.

struct AppState
{
    int x;
    int y;
    std::wstring text;
};
방식 특징
전역 변수 간단하지만 확장성 낮음
구조체 상태 관리 명확, 유지보수 용이

상태를 구조체로 묶으면 코드 흐름을 이해하기 쉬워지고, 기능 추가 시 충돌을 줄일 수 있습니다.

3 – 기능별 함수 분리하기

WinAPI 프로그램은 다음 세 역할로 나눌 수 있습니다.

  • 입력 처리
  • 화면 그리기
  • UI 이벤트 처리
void Render(AppState& state, HDC hdc)
{
    TextOut(hdc, 100, 100, state.text.c_str(), state.text.length());
}
void HandleInput(AppState& state, LPARAM lParam)
{
    state.x = LOWORD(lParam);
    state.y = HIWORD(lParam);
}

상태를 참조로 전달해야 변경 내용이 유지됩니다. 값으로 전달하면 수정이 반영되지 않는 문제가 발생합니다.

4 – 클래스 기반 구조로 확장하기

규모가 커지면 구조체만으로는 한계가 생깁니다. 이때는 클래스 기반으로 확장하는 것이 좋습니다.

class App
{
public:
    void OnClick(LPARAM lParam);
    void Render(HDC hdc);

private:
    int x;
    int y;
    std::wstring text;
};
구조 특징
구조체 상태 관리 중심
클래스 상태 + 기능 통합

이 방식은 상태와 로직을 함께 관리하기 때문에 프로젝트 규모가 커질수록 효과가 커집니다.
구조를 개선하지 않은 상태에서 기능을 계속 추가하면, 코드 수정 하나가 전체 동작에 영향을 주는 상황이 발생합니다. 반대로 이 구조를 적용하면 기능을 독립적으로 확장할 수 있습니다.
이 단계까지 오면 WinAPI를 단순 학습이 아니라 실제 프로젝트 수준으로 확장할 수 있는 기반을 갖추게 됩니다.

버튼 추가하기: 컨트롤의 시작

버튼 입력창으로 프로그램 형태 갖추기

버튼 입력창을 추가하면, 단순한 그래픽 프로그램에서 실제로 사용자와 상호작용하는 애플리케이션으로 확장됩니다. 핵심은 컨트롤을 생성하고, WM_COMMAND 메시지로 이벤트를 처리하는 구조입니다.
이전 단계에서 GDI로 도형을 그렸다면, 이제는 사용자가 직접 입력하고 결과를 확인하는 흐름까지 이어지게 됩니다.

GDI 다음에 컨트롤을 배우는 이유

WinAPI에서 화면을 구성하는 방식은 두 가지로 나뉩니다.

구분 특징
GDI 직접 그리기, 자유도 높음
컨트롤 미리 만들어진 UI, 사용 간편

그래픽 출력 중심에서 사용자 인터페이스 구성 중심으로 넘어가는 단계입니다. 컨트롤을 활용하면 빠르게 프로그램 형태를 갖출 수 있습니다.

버튼 추가하기: BUTTON 클래스로 클릭 가능한 요소 만들기

버튼은 “BUTTON” 클래스를 사용하여 생성합니다.

#define ID_BUTTON 1

HWND hButton = CreateWindow(
    L"BUTTON",
    L"확인",
    WS_VISIBLE | WS_CHILD,
    50, 50, 100, 30,
    hwnd,
    (HMENU)ID_BUTTON,
    hInstance,
    NULL
);

컨트롤 ID는 매크로로 정의해두는 것이 좋습니다. 버튼에서 발생한 이벤트는 부모 윈도우로 전달되기 때문에, WndProc에서 처리해야 합니다.

입력창 추가하기: EDIT 클래스로 텍스트 입력 받기

입력창은 “EDIT” 클래스를 사용합니다.

#define ID_EDIT 2

HWND hEdit = CreateWindow(
    L"EDIT",
    L"",
    WS_VISIBLE | WS_CHILD | WS_BORDER,
    50, 100, 200, 25,
    hwnd,
    (HMENU)ID_EDIT,
    hInstance,
    NULL
);

입력창은 반드시 핸들을 저장해두어야 합니다. 이후 입력값을 읽을 때 이 핸들을 사용합니다.

버튼 클릭으로 입력값 읽기

컨트롤 이벤트는 모두 WM_COMMAND 메시지를 통해 부모 윈도우로 전달됩니다.

case WM_COMMAND:
{
    if (LOWORD(wParam) == ID_BUTTON && HIWORD(wParam) == BN_CLICKED)
    {
        wchar_t buffer[100];
        GetWindowText(hEdit, buffer, 100);

        MessageBox(hwnd, buffer, L"입력값", MB_OK);
    }
}
return 0;

핵심 구조는 다음과 같습니다.

  • LOWORD(wParam): 어떤 컨트롤인지 구분
  • HIWORD(wParam): 어떤 이벤트인지 구분

이 흐름을 통해 버튼 클릭 → 입력값 읽기 → 결과 출력까지 연결됩니다.
입력값이 비어 있는 경우는 대부분 입력창 핸들이 올바르게 전달되지 않았거나, 잘못된 변수를 사용한 경우입니다.
이 단계까지 오면 WinAPI에서 기본적인 사용자 인터페이스를 갖춘 프로그램 구조를 완성하게 됩니다.

GDI 기초: 도형과 텍스트 그리기

GDI 기초 / 그리기 확장: 선, 사각형, 원 그려보기

GDI 기초 과정을 통해 직접 그리는 구조를 이해해야 합니다. 입력으로 좌표를 받고, 그 좌표를 기준으로 WM_PAINT에서 다시 그리는 방식이 핵심입니다.
이전 단계에서 입력 처리까지 구현했다면, 이제 단순 텍스트를 넘어 도형을 직접 그려보는 단계로 넘어가게 됩니다. 이 과정에서 “출력”이 아니라 “렌더링”이라는 개념이 자연스럽게 연결됩니다.

Hello World와 입력 처리 다음에 GDI 를 배우는 이유

WinAPI의 흐름은 다음처럼 확장됩니다.

  • Hello World → 화면 출력
  • 입력 처리 → 사용자 반응
  • GDI → 화면 구성

이 단계부터는 프로그램이 단순히 결과를 보여주는 것이 아니라, 화면 자체를 직접 만들어가는 구조로 바뀝니다.

STEP 1 – GDI 와 HDC 개념 이해하기

GDI의 핵심은 HDC입니다. 이는 화면에 그리기 위한 디바이스 컨텍스트 핸들로, 모든 그리기 작업의 기준이 됩니다.

PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);

// 그리기 작업 수행

EndPaint(hwnd, &ps);

BeginPaint와 EndPaint 사이에서만 안정적으로 그리기가 이루어집니다. 이 구조를 벗어나면 화면이 정상적으로 갱신되지 않을 수 있습니다.

STEP 2 – 선 그리기: MoveToEx와 LineTo 사용하기

선은 가장 기본적인 도형이며, 시작점과 끝점을 이용해 그립니다.

  1. 시작 위치 설정 (MoveToEx)
  2. 끝 위치까지 선 그리기 (LineTo)
MoveToEx(hdc, 50, 50, NULL);
LineTo(hdc, 200, 200);

MoveToEx로 시작 위치를 지정하지 않으면 예상과 다른 위치에서 선이 그려질 수 있습니다.

STEP 3 – 사각형과 원 그리기

사각형과 원은 경계 좌표를 이용해 간단하게 그릴 수 있습니다.

Rectangle(hdc, 50, 50, 200, 150);
Ellipse(hdc, 220, 50, 350, 150);

이 함수들은 왼쪽 위와 오른쪽 아래 좌표를 기준으로 도형을 생성합니다. 기본 색상이 적용되는 이유는 GDI의 기본 펜과 브러시가 사용되기 때문입니다.

STEP 4 – 입력 처리와 그리기 연결하기

GDI를 실제 프로그램처럼 사용하려면 입력 처리와 반드시 연결해야 합니다.

기본 흐름은 다음과 같습니다.

  • 입력 발생
  • 좌표 저장
  • InvalidateRect 호출
  • WM_PAINT에서 도형 그리기
case WM_LBUTTONDOWN:
{
    g_x = LOWORD(lParam);
    g_y = HIWORD(lParam);

    InvalidateRect(hwnd, NULL, TRUE);
}
return 0;
case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);

    Ellipse(hdc, g_x - 20, g_y - 20, g_x + 20, g_y + 20);

    EndPaint(hwnd, &ps);
}
return 0;

이 구조를 통해 클릭 위치에 원을 그릴 수 있습니다.
여기서 중요한 점은 WM_PAINT가 화면을 매번 새로 그린다는 것입니다. 그래서 이전 도형이 사라지는 현상이 발생할 수 있습니다. 여러 개의 도형을 유지하려면 좌표를 하나가 아니라 리스트 형태로 저장해야 합니다.
이 개념까지 이해하면 WinAPI에서 단순 출력이 아닌, 실제 그래픽 프로그램의 기초를 갖추게 됩니다.

입력 처리하기: 마우스와 키보드 이벤트

입력 처리 클릭과 키보드로 창 반응시키기

입력 처리는 “메시지를 받아 상태를 바꾸고, 다시 그린다”는 흐름으로 동작합니다. 이 구조를 이해하면 클릭이나 키 입력에 따라 화면이 변하는 프로그램을 만들 수 있습니다.
Hello World까지 출력했다면 이런 생각이 들 수 있습니다. “이 창은 왜 아무 반응이 없을까?” 실제 프로그램이라면 클릭하거나 키를 눌렀을 때 화면이 바뀌어야 자연스럽게 느껴집니다. 이번 단계에서는 WinAPI 창이 사용자 입력에 반응하고, 그 결과를 화면에 반영하는 흐름까지 이어서 만들어보겠습니다.

Hello World 다음에는 왜 입력 처리를 배워야 할까?

WinAPI 프로그램은 입력을 처리해야 비로소 “동작하는 프로그램”처럼 보입니다.
이전 단계에서는 WM_PAINT 메시지를 통해 텍스트를 화면에 출력했습니다. 하지만 이 상태는 단순히 보여주기만 하는 정적인 화면입니다. 사용자의 행동에 따라 변화가 있어야 실제 프로그램처럼 느껴집니다.

핵심 흐름은 다음과 같습니다.

  • 입력 → 상태 변경 → 화면 다시 그리기

이 세 단계가 연결되면서 프로그램이 실제로 반응하기 시작합니다.

마우스 클릭 처리하기: WM_LBUTTONDOWN 이해하기

마우스를 클릭하면 WM_LBUTTONDOWN 메시지가 발생합니다. 이 메시지를 통해 클릭 위치와 이벤트를 처리할 수 있습니다.

case WM_LBUTTONDOWN:
{
    int x = LOWORD(lParam);
    int y = HIWORD(lParam);

    g_text = L"마우스를 클릭했습니다";
    InvalidateRect(hwnd, NULL, TRUE);
}
return 0;

lParam에는 클릭 좌표가 들어 있습니다. LOWORD와 HIWORD를 이용하면 X, Y 값을 분리할 수 있습니다.
중요한 점은 단순히 메시지를 처리하는 것이 아니라, 상태값을 바꾸고 화면 갱신까지 연결해야 한다는 것입니다.

키보드 입력 처리하기: WM_KEYDOWN 이해하기

키보드 입력은 WM_KEYDOWN 메시지로 처리합니다. 단, 창이 포커스를 가지고 있어야 메시지가 들어옵니다.

case WM_KEYDOWN:
{
    if (wParam == 'A')
    {
        g_text = L"A 키를 눌렀습니다";
        InvalidateRect(hwnd, NULL, TRUE);
    }
}
return 0;

wParam에는 눌린 키 코드가 들어 있습니다. 이를 통해 특정 키 입력에 따라 동작을 다르게 만들 수 있습니다.
여기서 자주 발생하는 실수는 창에 포커스가 없는 상태에서 키 입력을 테스트하는 것입니다. 반드시 창을 한 번 클릭한 후 입력을 확인해야 합니다.

입력 결과를 화면에 반영하는 흐름 만들기

입력 처리의 핵심은 “값 변경과 화면 갱신은 별개”라는 점입니다.

다음 흐름을 반드시 기억해야 합니다.

  • 입력 메시지 수신
  • 상태값 변경 (예: g_text)
  • InvalidateRect 호출
  • WM_PAINT에서 다시 그림
case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);

    TextOut(hdc, 100, 100, g_text.c_str(), g_text.length());

    EndPaint(hwnd, &ps);
}
return 0;

이 구조를 통해 클릭이나 키 입력에 따라 화면 텍스트가 바뀌는 프로그램을 만들 수 있습니다.
많은 초보자가 “값은 바뀌었는데 화면이 그대로인” 문제를 겪습니다. 대부분 InvalidateRect 호출이 빠진 경우입니다.
이 흐름을 이해하면 WinAPI에서 입력과 화면 출력이 어떻게 연결되는지 명확해집니다. 이제 단순 출력 프로그램을 넘어, 사용자와 상호작용하는 기본 형태를 갖춘 상태입니다.

WinAPI 입문기 하나하나 배워보자

WinAPI 첫 걸음: Hello World 윈도우 만들기

WinAPI 프로그램에서 윈도우를 만들려면 “클래스 등록 → 윈도우 생성 → 메시지 처리”라는 흐름을 반드시 따라야 합니다. 이 구조만 이해하면 처음 보는 코드도 훨씬 쉽게 읽히게 됩니다.
처음 Visual Studio에서 WinAPI 프로젝트를 실행해 보면, 창이 보이지 않거나 잠깐 나타났다 바로 종료되는 경우를 자주 겪게 됩니다. 콘솔 프로그램과 달리 구조가 눈에 보이지 않기 때문입니다. 이 글에서는 실제로 “Hello World”가 화면에 출력되는 윈도우를 단계별로 만들어 보겠습니다.

WinAPI 프로그램의 기본 구조 이해하기

WinAPI 프로그램은 main이 아닌 WinMain에서 시작됩니다. 그리고 일반적인 함수 호출 흐름이 아니라, “메시지 기반 구조”로 동작합니다.

핵심 흐름은 다음 세 단계로 이어집니다.

  • 윈도우 클래스 등록 (설계도 정의)
  • 윈도우 생성 (실제 창 생성)
  • 메시지 처리 (동작 정의)

이 구조를 이해하면 코드가 왜 복잡해 보이는지도 자연스럽게 납득할 수 있습니다. 특히 클래스를 등록하지 않으면 윈도우를 생성할 수 없습니다.

STEP 1 – 윈도우 클래스 등록하기

윈도우를 만들기 위해서는 먼저 WNDCLASS 구조체를 통해 “창의 성격”을 정의해야 합니다.

핵심 요소는 다음과 같습니다.

  1. lpfnWndProc: 메시지를 처리할 함수
  2. hInstance: 프로그램 인스턴스
  3. lpszClassName: 클래스 이름
WNDCLASS wc = {0};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"HelloWindow";

RegisterClass(&wc);

여기서 L 접두어는 UNICODE 문자열을 의미합니다. WinAPI는 기본적으로 유니코드를 사용하기 때문에 반드시 붙여주는 것이 안전합니다.
자주 발생하는 실수는 RegisterClass 실패를 확인하지 않는 것입니다. 이 경우 이후 단계도 모두 실패하게 됩니다.

STEP 2 – 실제 윈도우 생성 및 표시

클래스를 등록했다면 이제 실제 창을 생성합니다.

HWND hwnd = CreateWindow(
    L"HelloWindow",
    L"Hello World Window",
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT,
    500, 300,
    NULL, NULL, hInstance, NULL
);

ShowWindow(hwnd, nCmdShow);

CreateWindow는 창의 크기, 위치, 스타일을 결정합니다. 그리고 ShowWindow를 호출해야 화면에 표시됩니다.
이 단계에서 가장 흔한 실수는 ShowWindow를 빠뜨리는 것입니다. 실행은 되지만 창이 보이지 않는 원인이 됩니다.

STEP 3 – 메시지 처리와 Hello World 출력

이제 창은 만들어졌지만, 아직 내용은 비어 있습니다. 화면에 무엇을 그릴지는 직접 정의해야 합니다.

WndProc 함수에서 WM_PAINT 메시지를 처리하면 됩니다.

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);

        TextOut(hdc, 100, 100, L"Hello World", 11);

        EndPaint(hwnd, &ps);
    }
    return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, msg, wParam, lParam);
}

WM_PAINT는 화면을 다시 그릴 때 호출되는 메시지입니다. 이 안에서 TextOut을 사용하면 문자열을 출력할 수 있습니다.

마지막으로 메시지 루프가 필요합니다.

  • 메시지를 받아야 창이 유지됩니다
  • 루프가 없으면 프로그램이 바로 종료됩니다
MSG msg = {};
while (GetMessage(&msg, NULL, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

이 구조까지 완성하면 WinAPI의 기본 흐름이 모두 연결됩니다.
윈도우 생성 → 메시지 처리 → 화면 출력까지 이어지며, 정상적인 GUI 프로그램이 동작하게 됩니다.