Skip to content

개발 함정 #2: 나는 32비트 BMP에 알파 채널이 있다고 늘 생각했다

2026-06-23

Tags: Windows · 개발 함정


나는 수년간 믿어온 상식이 하나 있다: 32비트 BMP, 픽셀당 4바이트, BGRA, 4번째 바이트가 알파다. PNG는 RGBA, BMP는 단지 바이트 순서가 반대일 뿐——명확하고, 깔끔하고, 당연한 이야기.

내가 ICO 도구를 만들면서 투명도가 완전히 말이 안 되는 방식으로 망가졌을 때 비로소 BMP 사양을 펼쳐봤다——그리고 내가 믿었던 것이 거짓말이었음을 깨달았다.

모두가 「알고 있는」 사실

이야기의 시작은 아주 평범했다. ICO를 풀어서 각 프레임을 미리 보고, 투명도를 유지하는 도구가 필요했다. 논리는 더 간단할 수 없었다:

  1. ICO 안의 특정 프레임 BMP 데이터를 읽는다
  2. 비트맵으로 디코딩한다
  3. 표시한다

첫 번째 프레임이 로드되었다: 색상 맞음, 형태 맞음, 투명한 부분이 전부 검은색 솔리드 블록으로 변해 있었다.

나의 첫 반응은——이 ICO 파일이 고장 났다는 것이었다. 다른 걸로 바꿔도 검은색. 또 바꿔도 검은색.

이상했다. 이 아이콘들은 Windows 탐색기에서는 분명 투명하게 잘 보이고 있었다.

사양은 뭐라고 말하는가

BMP 사양을 뒤졌다. 클래식 BITMAPINFOHEADER는 40바이트, biBitCount = 32는 픽셀당 32비트를 의미한다. 그리고 나서 그 결정적인 한 줄을 봤다——

biCompressionBI_RGB일 때, 각 픽셀 DWORD의 상위 바이트는 **unused(미사용)**다.

미사용. 알파도, 투명도도 아닌, 「이 바이트에 뭘 넣어야 할지 모르겠으니 일단 비워두자」라는 뜻이었다.

다시 말해: 가장 기본적이고 흔한 32비트 BMP 포맷에서 사양은 알파 채널을 정의하지 않는다. 각 픽셀의 4번째 바이트는 규칙대로라면 그냥 자리를 채우는 padding이다.

이것은 내가 수년간 「알고 있었던」 것과 정반대였다.

그럼 알파는 어떻게 「있게」 되었나

BMP는 나중에 보강이 이루어졌다. BITMAPV4HEADER(108바이트)가 BI_BITFIELDS를 도입하여, 빨강, 초록, 파랑, 그리고 알파에 대한 비트 마스크를 명시적으로 선언할 수 있게 했다. 이 버전에 이르러서야 알파는 BMP에서 비로소 공식적인 자격을 얻었다.

문제는: 대부분의 BMP 파일, ICO 안에 들어 있는 프레임들도 마찬가지로, 여전히 옛날 40바이트 헤더에 BI_RGB, 마스크 없이 사용하고 있다는 것이다. 사양대로라면 그 4번째 바이트는 padding이다.

그런데 왜 Windows에서 아이콘은 투명하게 보이는가?

XP 시대에 Windows 그래픽 스택 전체가 암묵적인 합의에 도달했기 때문이다: 「됐다, 다들 그 padding 바이트를 알파인 척하자.」 GDI의 블렌딩 함수가 이를 암시했고, 각 렌더러들은 암묵적으로 따랐다. 그리하여 「32비트 BMP에 알파가 있다」는 것은 모두가 믿으면서도 사양에는 없는 「사실」이 되었다.

ICO는 사실 투명도 시스템이 두 개다

더 흥미로운 점은, ICO는 BMP의 그 한 바이트에 기대지 않는다는 것이다. ICO에는 자체적인 공식 투명도 메커니즘이 있다: AND 마스크.

ICO 프레임 하나의 BMP 데이터는 이런 구조다:

  • 픽셀 데이터(XOR 마스크)——BGR에 정체불명의 4번째 바이트가 붙은 것
  • 바로 뒤에 1비트 모노크롬 마스크(AND 마스크)——픽셀당 1비트, 1은 투명, 0은 불투명

이것이 사양이 공식적으로 인정하는 ICO 투명도다. 32비트 알파는 나중에 덧붙여진 「부가 기능」이다.

Windows가 렌더링할 때의 선택도 현실적이다:

  • 32비트 알파에 0이 아닌 값이 하나라도 있으면 → 알파를 사용하여 부드럽게 블렌딩
  • 알파가 전부 0 → 불투명으로 취급하고 AND 마스크로 폴백

두 시스템이 공존하며, 잘 작동하는 쪽을 사용한다. 꽤 배려심 있다——직접 코드를 짜서 파싱하기 전까지는.

내가 빠진 함정은 무엇이었나

다시 검은 배경 아이콘으로 돌아가자. 진실은 이랬다:

내 디코더(WinUI의 BitmapDecoder든, 브라우저의 BMP 디코더든)는 성실하게 BMP 사양을 준수하고 있었다——BI_RGB + 32비트를 보면 4번째 바이트를 padding으로 취급해 버린다. 투명 영역의 알파가 사라지고, 남은 색상에 기본 불투명도가 더해져 검은 블록으로 변한 것이다.

디코더가 고장 난 게 아니다. 이 포맷 자체가 이 문제에서는 신뢰할 수 없는 것이다.

그래서 수정 방법은 직관에 반한다: ICO를 「고치는」 것도, 디코더를 바꾸는 것도 아닌——디코더를 더 이상 믿지 않는 것이다. 그 4개의 원시 바이트를 직접 읽고, BGRA를 직접 캔버스에 채워 넣는다; 알파가 전부 0일 때만 돌아가서 AND 마스크를 정직하게 계산한다.

나는 이후 ICO Unpacker의 웹 버전과 데스크톱 버전 모두에서 이렇게 했다: 디코더를 우회하고 DIB의 바이트를 직접 파싱한다. 이렇게 하면 그 4번째 바이트가 알파든 padding이든, 투명도가 허공에 증발하지 않는다.

ICO Unpacker는 무료 Windows 데스크톱 앱으로도 사용할 수 있습니다——동일한 논리, 브라우저 불필요, 완전 오프라인.

그래서 결국 무엇을 기억해야 하나

  • 「32비트 BMP에 알파 채널이 있다」는 것은 업계 전체의 암묵적 합의이지, 사양이 정의한 사실이 아니다.
  • 클래식 BITMAPINFOHEADER + BI_RGB에서 4번째 바이트는 padding이다; 알파는 BITMAPV4HEADER + BI_BITFIELDS에서야 비로소 공식화된다.
  • 어떤 도구가 BMP 투명도를 「잃어버렸다」면, 반드시 버그는 아닐 수 있다——그냥 사양을 따랐을 뿐일 수도 있다.
  • ICO에는 공식 투명도(AND 마스크)가 있고, 32비트 알파는 후에 추가된 부가 기능이다; 두 체계가 공존하며 Windows는 상황에 따라 적절히 선택한다.
  • 투명도를 정말로 지키고 싶다면, 돌아가지 마라: 직접 바이트를 읽어라.

이전 글 개발 함정 #1에서는 「온라인 변환 도구가 은밀하게 BMP 프레임을 망가뜨린다」는 이야기를 했다——그것은 다른 사람이 알파를 잘못 쓰는 문제다. 이 글에서 다루는 것은 반대편이다: 알파를 올바르게 썼더라도, BMP 포맷 자체가 누군가 알아주기를 보장하지 않는다.


이 글은 개발 함정 시리즈의 일부입니다.