Win32 API가 한때 독특한 모양의 Windows 앱을 어떻게 가능하게 했는지, 그리고 그런 창들이 왜 점점 사라졌는지를 살펴본다.
요즘 앱들은 너무 다 비슷하게 생겨서 질렸다. 오늘날 Windows 데스크톱 앱은 전부 똑같아 보이는데, 실제로도 다 똑같기 때문이다. 전부 형편없는 React, Electron, electronbun, Tauri 같은 브라우저 래퍼 위에 만들어져서 진짜 데스크톱 앱을 흉내 낸다. 느리고, 메모리를 엄청 먹고, 사실상 블로트웨어다. Notepad는 기본적인 메모 앱이지 Word 대체제가 아니고, 계산기는 계산기여야지 NASA 달 탐사 미션 플래너가 되어서는 안 된다. 어느 순간부터 Microsoft는 완전히 방향을 잃었다. 최적화 개념조차 없는 웹 개발자들에게 아예 운전대를 넘겨준 것처럼 보인다.
고작 Notepad 앱 하나가 메모리를 거의 50mb나 먹는데, 순수 Win32 C로 만든 동등한 NOTEPAD는 메모리 1.8mb만 쓴다. 오늘날 기준으로 50mb가 엄청 큰 수치는 아닌 것처럼 보일 수 있지만, 바로 그게 문제다. mb 하나하나가 쌓이고 또 쌓인다. 나는 최근 Intel Ultra 9 285와 RAM 32 GB가 달린 새 컴퓨터를 샀는데, Windows 11이 막 부팅됐을 때 메모리가 무려 77%나 차 있었다.
이제 Win32 API로 프로그래밍하는 건 잊힌 기술이 되었고, 나는 Windows 앱이 한때 어떻게 만들어졌는지를 아쉬운 마음으로 돌아보게 된다. 지저분하긴 했지만, 대신 완전한 제어권이 있었다.
현대의 UI 프로그래밍은 대부분 운영체제를 당신 눈앞에서 숨기려 한다. 당신은 사각형 상자에 갇혀 있지만, Windows XP 시절에는 비표준 애플리케이션 창을 갖는 것이 멋지게 여겨지던 때가 있었다. Windows Media Player조차 그랬다.
모든 것이 밋밋한 둥근 사각형에 사이드바 하나, 설정 톱니바퀴 하나, 그리고 그 아래에 웹 스택이 눌러앉아 있는 형태일 필요는 없었다. Windows 앱이 이상하게 생겨도 괜찮았던 시절이 있었다. 미디어 플레이어는 하드웨어처럼 보였고, 데스크톱 마스코트는 화면 위를 돌아다녔다. 유틸리티 패널은 대시보드 같기도 했고, 장난감 같기도 했고, 라디오나 작은 외계인 조종 콘솔처럼 보이기도 했다. 운영체제가 처음 사각형 창을 준다고 해서, 창이 꼭 사각형이어야만 하는 것은 아니었다.
대개 핵심은 사용성이 아니었다. 정체성이었다.
현대 데스크톱 UI가 대부분 잃어버린 부분이 바로 그것이다. Windows가 더는 그런 것을 못 해서가 아니라, 이제는 대부분의 사람들이 창 자체를 자신이 제어하는 대상으로 여기며 프로그래밍하지 않기 때문이다.
이 글의 GitHub 저장소는 Win32가 여전히 정확히 그런 일을 할 수 있게 해 준다는 사실을 상기시키는 작은 예시다. 한 예제는 창을 타원형으로 만든다. 다른 예제는 비트맵으로 창 모양을 만든다. 세 번째는 전체를 애니메이션 데스크톱 마스코트로 바꾼다. 거대한 프레임워크는 전혀 필요 없다. 그냥 Windows를 직접 다루기만 하면 된다.
게임, 브라우저, 혹은 현대 UI 프레임워크에서 온 사람들이 가장 먼저 헷갈려 하는 점은 Win32가 당신이 소유한 업데이트 루프를 중심으로 돌아가지 않는다는 것이다. Win32는 메시지를 중심으로 돌아간다. 앱은 그 자리에 있고, Windows가 계속 이벤트를 건네준다.
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
그 루프가 바로 계약이다. Windows는 무언가가 일어났다고 말하고, 당신의 window procedure는 그것이 무엇을 의미하는지 결정한다. “WM_CREATE”는 창이 생성되고 있다는 뜻이다. “WM_PAINT”는 다시 그려야 한다는 뜻이다. “WM_SIZE”는 클라이언트 영역이 바뀌었다는 뜻이다. “WM_DESTROY”는 끝났다는 뜻이다. 그것이 Win32 프로그래밍의 진짜 형태다. 메시지를 하나씩 처리하면서 동작을 쌓아 올리는 것이다.
그리고 이것을 이해하고 나면, 이상한 모양의 창도 더 이상 신비롭게 느껴지지 않는다.
보통 최상위 창은 사각형이지만, Windows에는 region object, 즉 “HRGN”이라는 개념도 있다. “SetWindowRgn”으로 창에 region을 할당하면, 그 부분만 실제 창으로 취급된다. 나머지는 시각적으로도, 상호작용 측면에서도 사라진다. 그게 바로 핵심 트릭이다.
가장 단순한 버전은 “basic/main.c”에 있다. 이것은 테두리가 없는 창을 만들고, 그런 다음 이렇게 한다.
region = CreateEllipticRgn(0, 0, rc.right, rc.bottom);
SetWindowRgn(hwnd, region, TRUE);
이것만으로도 창은 타원형이 된다. 사각형 프레임 안에 가짜 타원을 그리는 것이 아니라, 실제 타원형 HWND가 된다. 이 샘플은 사람들이 자주 잊는 실용적인 부분도 처리한다. 제목 표시줄을 없애면 시스템이 더는 창을 대신 끌어 주지 않는다. 그래서 “WM_LBUTTONDOWN”에서 “HTCAPTION”과 함께 “WM_NCLBUTTONDOWN”을 보내 제목 표시줄 드래그를 흉내 낸다.
그 작은 디테일이야말로 사용자 정의 창의 모든 이야기를 축약한 것이다. 모양을 만드는 것은 쉽다. 원래 일반 프레임이 공짜로 해 주던 일을 대체하는 것이 진짜 작업이다.
다음 샘플부터가 더 흥미로워진다. “drivenbyimage/main.c”는 모양을 수학적으로 정의하는 대신 비트맵 데이터에서 그것을 끌어낸다. “shape.bmp”를 불러오고, “GetDIBits”로 픽셀을 읽은 다음, 이미지를 한 줄씩 스캔한다. 투명하지 않은 픽셀들의 각 가로 구간은 작은 사각형 region이 되고, 그 구간들을 합쳐 하나의 최종 창 region으로 만든다.
#define TRANSPARENT_COLOR RGB(255, 0, 255)
마젠타는 빈 공간을 의미한다. 나머지는 모두 창의 일부가 된다.
이 말은 비트맵이 한 번에 두 가지 일을 한다는 뜻이다. 첫째, 그것은 “WM_PAINT”에서 그리는 대상이다. 둘째, 그것은 실제 창의 모양이다. 예전의 많은 스킨형 앱이 이런 방식으로 동작했다. 원, 둥근 모서리, 혹은 깔끔한 벡터 기하 형태에만 제한되지 않았다. 이미지가 개, 우주선, 오디오 기기, 혹은 만화 얼굴처럼 생겼다면, 창도 그 모양이 될 수 있었다.
바로 이 부분이 옛날 데스크톱 소프트웨어를 재미있게 만들었다. 창 프레임은 더 이상 자연법칙이 아니었고, 그저 또 하나의 에셋이 되었다.
하지만 비트맵 기반 region에는 여전히 딱딱한 경계가 있다. 픽셀은 들어가 있거나 빠져 있거나 둘 중 하나다. 이것은 실루엣이나 오려낸 듯한 UI에는 완벽하지만, 부드러운 가장자리나 반투명 픽셀, 혹은 애니메이션이 필요하다면 layered window로 넘어가야 한다.
그것이 “Animated/” 예제가 하는 일이다. 나는 itch에서 작가 inmenus,가 만든 멋진 애니메이션 8bit 개 스프라이트 시트를 찾았고, 그 작가는 그 아트를 Creative Commons로 제공했다. 그러니 그 점에 감사드린다.
region으로 창을 도려내는 대신, 이 예제는 “WS_EX_LAYERED” 팝업을 만들고 “UpdateLayeredWindow”로 32-bit 알파 이미지를 그 안에 업로드한다. 샘플은 스프라이트 시트를 사용하고, 타이머에 맞춰 프레임을 진행시키고, GDI+로 메모리 비트맵에 그린 다음, 결과를 데스크톱에 밀어 넣는다. 이 시점이 되면 더 이상 “내 창은 타원이다” 혹은 “내 창은 이 마스크와 일치한다”라고 말하는 것이 아니다. “내 창은 지금 이 픽셀들이 보여 주는 그대로다”라고 말하는 것이다.
애니메이션 마스코트는 layered window 쪽이 더 잘 맞는다. 픽셀 단위 알파를 얻을 수 있고, 가장자리가 더 깔끔하며, 투명성을 제대로 처리할 수 있고, 매 프레임마다 보이는 모양을 자유롭게 바꿀 수 있기 때문이다.
하지만 Win32 API 프로그래밍에는 문제가 있다. 그리고 진실은, 사용자 정의 창은 모든 것을 직접 해야 한다는 점이다. 모든 Windows 메시지를 직접 제어해야 하고, 그건 취약하다.
불편한 부분은 일반 프레임을 버리기로 결정하는 순간 시작된다. 그러면 창 끌기, 크기 조절, 닫기 동작, hit testing, 키보드 처리, 다시 그리기의 정확성, DPI 처리, 그리고 기본 창이 이미 수십 년 전에 해결해 둔 온갖 짜증 나는 예외 상황을 전부 당신이 맡게 된다. 그래서 이상한 모양의 창은 프로토타입을 만들기는 쉽지만, 완성도 있게 다듬는 데 비용이 많이 든다.
결과가 정말로 특별하지 않은 한, 사용자들은 대개 그 노력에 보답하지 않는다.
데스크톱 UI 문화는 “이 미친 스킨 좀 봐”에서 “믿을 수 있게 동작하고 내 방해만 하지 마”로 옮겨 갔다. 이상한 창은 진지한 소프트웨어보다도 장난, 애드웨어, 툴바, 비대해진 유틸리티와 더 자주 연결되게 되었다. 아쉬운 일이다.
그래도 나는 이런 것들이 존재한다는 사실이 좋다. Windows가 한때 소프트웨어가 물리적인 존재감을 가질 수 있는 플랫폼이었다는 것을 떠올리게 해 주기 때문이다. 단지 브라우저 탭을 위장한 것 안의 페이지 레이아웃이 아니었다. 대부분의 경우 평범한 사각형 창이 올바른 답이다. 하지만 그것이 법칙이 아니라 선택이라는 점을 기억하는 것은 좋다.
Win32의 좋은 점은 이런 일들을 하지 말라고 설득하려 들지 않는다는 것이다. 그저 메시지와 핸들, 드로잉 API, 그리고 뭔가 흥미로운 것을 만들 수 있을 만큼의 자유를 줄 뿐이다.
코드는 이 글의 GitHub 저장소에서 찾을 수 있다.