동일한 이미지 버그를 여섯 번이나 '수정'했습니다. 원인은 다크 모드가 아니었습니다.
요약
투명 PNG 이미지가 다크 모드 테마에서 검게 뭉쳐 보이는 버그를 해결하는 과정을 다룹니다. 가설에 기반한 반복적인 수정 대신 Playwright를 이용한 측정과 검증의 중요성을 강조합니다.
핵심 포인트
- 추측 대신 Playwright와 같은 도구로 직접 측정하여 가설을 검증해야 함
- Chrome의 강제 다크 모드가 이미지 렌더링에 미치는 영향을 확인
- AI 에이전트 사용 시 모호한 지시가 잘못된 결과로 이어질 수 있음
- 이미지 색상 문제 해결을 위해 RGB 값과 알파 채널의 일치 여부 확인 필요
저는 대한민국 제주도에 사는 대학교 1학년 학생입니다. 저는 혼자서 학습 앱을 만들고 있으며, 전문 개발자는 아닙니다.
일본 출시를 준비하던 중, 마스코트 이미지가 페이지에서는 거의 순수한 검은색으로 렌더링되지만, 브라우저 탭에서 직접 열었을 때는 완전히 정상적으로 보이는 버그를 발견했습니다. 저는 이것이 Chrome의 강제 다크 모드(forced dark mode) 때문이라고 가정하고, 그 가설을 바탕으로 여섯 번이나 '수정'을 시도했습니다.
마침내 Playwright로 측정해 보았을 때, 다크 모드는 이 문제와 아무런 관련이 없었습니다. 플래그(flag)를 켜거나 꺼도 단 하나의 픽셀도 변하지 않았습니다.
대상 독자
- 다크 테마 UI에서 투명한 PNG를 사용하는 모든 분
- 현재 Chrome의 강제 다크 모드를 의심하고 있는 모든 분
- AI 코딩 에이전트와 페어 프로그래밍(pair-programming)을 하고 있는 모든 분
얻을 수 있는 것
- 이미지가 검게 뭉쳐버리는 명확하지 않은 이유
- 추측 대신 측정을 통해 가설을 종결시키는 방법
- 추측을 멈춰야 할 시점에 대한 규칙
발생한 상황
저는 어두운 테마의 랜딩 페이지에 크림색 토끼 마스코트(투명 PNG)를 배치했습니다.
페이지 상에서 토끼의 몸체는 배경과 합쳐져 버렸습니다. 윤곽선과 귀의 분홍색 부분만 보였습니다. 직접 열었을 때는 괜찮았지만, 페이지에 삽입했을 때는 검게 뭉쳐버렸습니다.
여섯 번의 잘못된 수정
1. Chrome의 강제 다크 모드 탓하기
Chrome의 "웹 콘텐츠를 다크 모드로 표시" 기능은 렌더링된 크기에 따라 <img> 요소를 분류합니다. 작은 아이콘은 그대로 두지만, 큰 이미지는 사진으로 취급하여 밝은 영역을 어둡게 만듭니다.
제 헤더 로고(32px)는 괜찮았습니다. 큰 마스코트(160px)는 검은색이었습니다. 가설이 완벽하게 들어맞았습니다.
<meta name="color-scheme" content="dark">를 추가했습니다. 변화가 없었습니다.
2. background-image로 교체하기
동일한 기능이 CSS 배경 이미지(background images)는 다른 경로로 처리하기 때문에 반전되지 않습니다. <img>를 background-image로 교체했습니다. 변화가 없었습니다.
3. SVG로 감싸기
<img>로 인식되지 않도록 <svg><image href="..."/></svg> 형식을 사용했습니다. 변화가 없었습니다.
또한, AI 에이전트(AI agent)에게 내린 지시가 너무 모호해서, 에이전트는 "이미지를 SVG로 감싸라"는 명령을 "원본을 참조하여 이미지를 SVG로 다시 그려라"로 해석했습니다. 결과적으로 완전히 다른 결과물을 얻었습니다. 에이전트에게는 구체적으로 지시해야 합니다.
4. 파일 탓하기
파일을 직접 열어보니 색상이 정말로 물이 빠진 듯했습니다. 배경 제거(background removal) 단계에서 RGB 값이 변경되었던 것입니다.
원본으로부터 다시 빌드하면서, Pillow를 사용하여 알파 채널(alpha channel)만 조작했습니다. 교체하기 전에 RGB가 픽셀 단위로 일치하는지 확인했습니다. 여전히 변화는 없었습니다.
(그 확인 과정에 허점이 있었습니다. 이에 대해서는 아래에서 더 자세히 다루겠습니다.)
5. 색 공간(colour space)과 부모 CSS 탓하기
<html까지 모든 조상 요소를 거슬러 올라가며 filter, mix-blend-mode, backdrop-filter, 그리고 opacity를 확인했습니다. 모두 none이었습니다.
6. 마침내 측정하기
이 시점에서 저는 추측을 멈췄습니다.
# 강제 다크 모드(forced dark mode)를 ON 및 OFF로 설정하여 전체 페이지를 스크린샷 찍은 후 차이(diff) 비교
browser = playwright.chromium.launch(args=[
"--force-dark-mode",
...
결과:
max pixel diff across whole page: 0
mean pixel diff: 0.0
differing pixels: 0 / 1,152,000
강제 다크 모드를 토글(toggling)해도 아무것도 변하지 않았습니다. 단 하나의 픽셀도요.
정상 작동 여부를 확인하기 위해, color-scheme 선언이 없는 빈 테스트 페이지에 동일한 플래그(flag)를 실행했습니다. 크림색 박스가 (245,240,230)에서 (41,37,30)으로 변했습니다 — 제대로 반전되었습니다. 즉, 플래그는 작동하고 있었지만, 제 페이지에는 영향을 미치지 않고 있었던 것입니다.
저는 현장에 있지도 않은 용의자를 쫓고 있었던 셈입니다.
실제로 일어나고 있었던 일
원본 이미지의 크기는 1408×768이었습니다. 저는 이를 헤더의 32px 슬롯과 히어로(hero) 섹션의 128px 슬롯에 렌더링하고 있었습니다. 이는 11배에서 44배에 달하는 다운스케일(downscale)입니다.
투명 영역 또한 넓었기 때문에, object-contain을 사용하면 토끼 자체는 32px보다 더 작게 렌더링되었습니다.
측정 결과:
| 원본 좌표 및 RGB | 다운스케일 후 렌더링된 값 |
|---|---|
(254,165,135) 불투명 영역 | (231,151,123) → 약 9% 더 어두워짐 |
| ... |
가장자리만 무너지고 있었습니다. 이미지가 반전된 것이 아니라 — 다운스케일 과정에서 어두운 무언가가 스며들고 있었습니다.
여전히 추측 중인 부분
어둠이 어디서 왔는지에 대한 두 가지 가설:
가설 1: 브라우저의 리샘플링 (resampling) 과정에서 배경이 섞임. 다운스케일 (downscale) 과정에서 반투명한 가장자리 픽셀들이 그 뒤에 있는 어두운 배경과 혼합됩니다. 하지만 브라우저는 보통 프리멀티플라이드 알파 (premultiplied alpha) 방식으로 리샘플링을 수행하므로, 이것만으로는 이렇게 심각한 붕괴 현상을 설명하기에 확신이 서지 않습니다.
가설 2: 투명한 픽셀들이 이미 검은색이었음. 배경 제거 도구를 거친 이미지들은 종종 "완전히 투명하지만, RGB 값은 검은색인" 넓은 영역을 포함하곤 합니다. 그 자체로는 보이지 않지만, 다운스케일을 하면 그 검은색이 가장자리로 번져 나갑니다.
4단계에서 제가 재구축한 이미지가 원본과 픽셀 단위로 일치하는지 확인했습니다. 하지만 비교 대상이 바로 "원본"이었기 때문에, 만약 원본의 투명 영역 아래에 이미 검은색이 깔려 있었다면, 문제는 검사 과정을 거치면서도 수정되지 않은 채 그대로 남아 있었을 것입니다. 저는 무언가를 검증했다고 생각했지만, 그렇지 않았습니다.
저는 가설 2가 더 가능성이 높다고 생각합니다. 이를 확인하는 방법은 간단합니다. 알파 (alpha) 값이 0인 픽셀의 RGB 값을 확인하면 됩니다:
from PIL import Image
import numpy as np
...
만약 가설 2가 맞다면, 해결책은 투명한 영역의 RGB를 인접한 불투명 픽셀의 색상으로 채우는 것입니다. 이를 보통 컬러 블리딩 (colour bleed) 또는 엣지 패딩 (edge padding)이라고 부릅니다. 대부분의 게임 텍스처 도구에는 이 기능이 내장되어 있습니다.
확인이 끝나면 이 포스트를 업데이트하겠습니다. 만약 여러분도 이전에 이런 문제를 겪었다면, 어떤 경우였는지 듣고 싶습니다.
대신 내가 한 일
올바른 해결책은 실제로 렌더링할 크기에 맞춰 이미지를 준비하는 것입니다. 프리멀티플라이드 알파 (premultiplied alpha)로 리샘플링한 다음, 언프리멀티플라이 (unpremultiply)를 수행하십시오. 이 과정을 건너뛰면 가장자리가 어두워집니다.
저는 출시 마감 기한이 있었기 때문에, 마스코트 뒤에 흰색 원을 배치했습니다:
<div className="rounded-full bg-white p-2">
<Logomark />
</div>
원인이 무엇이든, 이 방법은 어두운 색상이 스며들 여지를 없애줍니다. 또한 다크 테마에서 마스코트의 가시성도 개선되었으므로 디자인 측면에서도 잘 풀린 셈입니다.
하지만 이것은 증상을 제거했을 뿐입니다. 원인을 증명한 것은 아닙니다.
내가 배운 점
가장 그럴듯한 가설이 가장 위험하다
나의 첫 번째 가설은 증상을 완벽하게 설명했습니다. 작은 로고는 괜찮고, 큰 이미지는 검게 변하는 것 — 이는 강제 다크 모드 (forced dark mode)의 크기 기반 분류와 정확히 일치했습니다. 바로 그 점 때문에 다섯 번째 시도 전까지는 의심조차 하지 않았던 것입니다.
실제 메커니즘은 "렌더링 슬롯이 더 크면 다운스케일 비율 (downscale ratio)이 더 커지므로, 가장자리 번짐 (edge bleeding) 현상이 더 강하게 나타난다"는 것이었습니다. 원인은 완전히 달랐지만, 증상은 동일했습니다.
"내가 확인했다"는 말이 실제로 무엇을 의미하는지 심문하라
4단계에서 나는 RGB 값이 픽셀 단위로 일치하는 것을 확인했다고 적었습니다. 수치는 정확했습니다. 하지만 나는 잘못된 대상을 비교하고 있었기에, 아무것도 배우지 못했습니다.
검증 코드 (verification code)가 통과되면 안전하다고 느껴집니다. 하지만 그 검증이 무엇을 배제할 수 있고, 무엇을 배제할 수 없는지는 코드 바깥의 영역에 존재합니다.
측정은 생각보다 저렴하다
6단계에서 수행한 Playwright 측정은 30분도 걸리지 않았습니다. 처음부터 이 작업을 수행했더라면 다섯 번의 수정 과정을 아낄 수 있었을 것입니다.
추측하고 수정하는 방식 (Guess-and-fix)은 잘 작동할 때는 괜찮습니다. 하지만 두 번 틀리면 측정하라. 그것이 올바른 임계값입니다.
AI 에이전트 (AI agents)에도 동일한 규칙이 적용된다
나는 이 작업의 대부분을 AI 코딩 에이전트 (AI coding agent)에게 위임했습니다. 에이전트는 당신의 가설에 의문을 제기하지 않습니다. 그들은 가설을 충실하고 빠르게 구현할 뿐입니다. 따라서 잘못된 가설은 잘못된 수정 사항을 매우 빠른 속도로 누적시킵니다.
어느 시점에는 세 겹으로 쌓인 임시방편 (workarounds)들이 그 자체로 새로운 버그의 원인이 되고 있었습니다.
에이전트에게 "이것을 수정해줘"라고 말하는 대신 "원인을 측정해서 보고해줘"라고 요청하는 것이 결국에는 더 빠릅니다. 그것이 단 한 번의 시도로 문제를 해결한 방법이었습니다.
내가 애초에 이것을 만들었던 이유
모든 시험 전에는 어딘가에서 기출문제 (past papers)가 나타납니다. 동아리 선배로부터, 연구실 동료로부터, 혹은 당신이 속해 있는 단톡방으로부터 말이죠.
나는 그 어디에도 속해 있지 않았습니다. 같은 강의를 듣고, 같은 시간 동안 공부하지만, 출발선이 달랐습니다.
이것은 비단 한국만의 일이 아닙니다. 일본에서도 똑같은 이야기를 계속 접했습니다. 어떤 집단(circle)에 속해 있지 않다면, 기출문제는 당신에게 도달하지 않습니다. 국가는 다르지만 구조는 동일합니다.
그래서 저는 소셜 네트워크(social network) 없이도 작동하는 것을 만들었습니다. 사용자가 자신의 강의 자료를 업로드하면, 시스템이 강사별로 이를 분석하여 예상 시험 문제와 요약된 요약본(condensed summary)을 생성합니다. 파일은 온전히 사용자의 소유로 남습니다. 다른 사용자와 공유되지 않으며, 별도의 문서 라이브러리(document library)도 존재하지 않습니다.
carrotly.app — 무료이며, 회원가입이 필요하지 않습니다.
유럽이나 미국의 커리큘럼(syllabus)에서 얼마나 잘 작동할지는 정말 모르겠습니다. 만약 여러분의 커리큘럼에서 제대로 작동하지 않는다면, 그것이 바로 제가 가장 원하는 피드백입니다.
제 로고에 있는 토끼는 여섯 차례의 수정 과정을 견뎌낸 마스코트입니다.
읽어주셔서 감사합니다. 만약 여러분도 동일한 증상을 겪고 있었다면, 이 글이 몇 시간의 시간을 아껴주었기를 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기