같은 JPEG이 브라우저마다 다른 이유 — ‘픽셀 퍼펙트’라는 신화

똑같은 JPEG 파일 하나가 크롬에서는 얇게, 파이어폭스에서는 두껍게 그려진다. 이것은 어느 브라우저의 버그인가, 아니면 우리가 웹에 대해 품어 온 “같은 코드는 같은 픽셀을 낳는다”는 믿음이 처음부터 착각이었다는 증거인가?

도입

한 개발자가 동료의 화면에서 이상한 것을 발견했다. 자신이 만든 15픽셀짜리 작은 로고가, 같은 웹페이지인데도 크롬에서는 파이어폭스보다 눈에 띄게 얇아 보였던 것이다. 확대해 나란히 놓고 보니 차이는 분명했다. 크롬 쪽 글자 획은 뭉개져 두툼했고, 파이어폭스 쪽은 날카로웠다. 같은 URL, 같은 이미지 파일, 같은 CSS. 다른 것은 브라우저뿐이었다. 개발자 guillaumetech는 이 사소한 어긋남을 그냥 넘기지 않고 파고들어, JPEG 디코딩의 밑바닥에서 그 원인을 캐낸 글을 공개했다. 발단은 아이콘 하나였지만, 그가 들춰낸 것은 훨씬 큰 질문이다. 우리는 흔히 “픽셀 퍼펙트”를 목표로 삼고, 디자인 시안과 브라우저 렌더링이 1:1로 일치하기를 기대한다. 그러나 같은 이미지조차 브라우저마다 다르게 그려진다면, 그 목표는 애초에 성립하는가. 이 글은 좁은 현상 하나에서 출발해 “브라우저 렌더링은 왜 서로 다른가”라는 넓은 질문으로 들어간다.

15픽셀 로고가 남긴 단서: 크롬은 이미지를 “덜” 디코딩한다

먼저 사실관계부터 분리하자. guillaumetech의 추적에 따르면, 크롬이 작은 JPEG을 다르게 그리는 직접적 원인은 “부분 IDCT 스케일링(partial IDCT scaling)“이라는 디코딩 최적화다. IDCT는 역이산코사인변환(Inverse Discrete Cosine Transform)의 약자로, JPEG을 픽셀로 되돌리는 핵심 연산이다. 이 이름을 이해하려면 JPEG이 이미지를 어떻게 담는지를 알아야 한다.

JPEG은 이미지를 통째로 저장하지 않는다. 그림을 8×8 픽셀 블록으로 잘게 나눈 뒤, 각 블록을 픽셀 값의 나열이 아니라 “주파수 성분”의 조합으로 바꿔 저장한다. 이것이 DCT(이산코사인변환)다. 한 블록 안에서 완만하게 변하는 밝기의 평균값 같은 저주파 성분과, 가장자리·획·잔무늬처럼 급격히 변하는 고주파 성분이 각각 계수로 분리되어 들어간다. 사람 눈이 저주파에 민감하고 고주파의 미세한 손실에는 둔감하다는 성질을 이용해, JPEG은 고주파를 과감히 버리며 용량을 줄인다. 사진이 잘 압축되는 이유가 여기 있다.

부분 IDCT 스케일링은 이 구조를 역이용한 속도 최적화다. 이미지를 원래 크기로 완전히 복원한 뒤 줄이는 대신, 처음부터 축소된 크기로 디코딩해 버린다. guillaumetech의 설명으로는, 크롬은 목표 크기에 맞춰 “분모가 8인 가장 가까운 분수”를 골라 그 배율로만 블록을 복원한다. 크게 줄일수록 고주파 계수를 계산에서 아예 빼버리고, 극단적으로 1/8까지 줄이면 블록당 저주파의 맨 밑, 즉 8×8 블록 전체의 평균값 하나(DC 성분)만 남긴다. 8×8 블록이 픽셀 한 점으로 뭉개지는 것이다. 이 과정에서 획의 날카로움을 만들던 고주파 정보가 통째로 증발하고, 그래서 크롬의 로고가 두툼하고 흐릿하게 보였다. 크롬은 이미지 디코딩을 그래픽 라이브러리 Skia에 맡기고, Skia는 다시 libjpeg-turbo를 쓰는데, 바로 이 libjpeg-turbo가 부분 IDCT 스케일링을 구현하고 있다.

이 최적화는 공짜 성능이 아니다. HN 토론에서 한 참여자는 자신이 겪은 실제 버그를 공유했는데, 일부 인코더는 JPEG의 마지막 블록(트레일링 MCU)에서 실제로 쓰이지 않는 행·열에 0이나 쓰레기 값을 남긴다는 것이다. 원래 크기로 디코딩하면 그 여분이 잘려 나가 문제가 없지만, IDCT 스케일링은 그 쓰레기 값을 이웃 출력 픽셀에 섞어버려 눈에 보이는 아티팩트를 만든다고 그는 지적했다(크롬 버그 트래커에 관련 이슈가 올라와 있다). 왜 이런 위험을 감수하느냐는 질문에 대해서는, 메모리 절약이 아니라 속도 때문이라는 답이 이어졌다 — 메모리는 CPU 캐시에 비하면 느리므로, 큰 이미지를 통째로 펼쳤다 줄이는 것보다 처음부터 작게 디코딩하는 편이 훨씬 빠르다는 것이다.

그렇다면 파이어폭스는 왜 다른가. HN에서 파이어폭스 내부를 아는 참여자들이 설명한 바를 옮기면, 파이어폭스도 이미지를 원본 해상도로 다 펼쳤다가 줄이지는 않는다. 대신 “디코딩하며 축소(downscale-during-decode)“하는 방식으로, 스트리밍 디코딩 도중에 축소를 함께 적용한다. 다만 크롬처럼 8×8 블록 단위로 계수를 잘라내는 부분 디코딩까지 하지는 않는다는 지적이 함께 나왔다. 두 브라우저 모두 “작게 그릴 때는 크게 만들 필요가 없다”는 같은 목표를 향하지만, 그 목표에 이르는 경로가 다르고, 그 경로의 차이가 곧 화면의 차이로 남는다.

왜 하나의 원인이 아닌가: 스케일링·감마·크로마·색 관리의 겹

여기서부터가 이 현상의 진짜 깊이다. guillaumetech 자신도 글 말미에 정정 노트를 달았다. 부분 IDCT만으로 모든 차이를 설명할 수는 없고, 축소에 쓰는 스케일링 알고리즘 자체도 결과에 크게 영향을 준다는 것이다. HN 토론은 이 지점을 여러 겹으로 벗겨냈다. 요점은 하나다 — 브라우저 렌더링의 차이에는 단일한 범인이 없다.

첫째 겹은 리샘플링 알고리즘이다. 이미지를 줄일 때 여러 원본 픽셀을 하나의 출력 픽셀로 어떻게 합칠지는 정답이 하나가 아니다. 어떤 참여자는 크롬과 파이어폭스가 애초에 서로 다른 축소 알고리즘을 쓰며, 이것이 IDCT보다 오히려 더 큰 차이를 만든다고 봤다. 그의 관찰로는 크롬 쪽이 대체로 더 흐릿하고, 파이어폭스 쪽이 더 날카롭지만 가장자리에 링잉(ringing) 아티팩트가 살짝 더 낀다. 이는 이미지 처리에서 고전적인 트레이드오프다. 바이리니어(bilinear)처럼 부드러운 알고리즘은 뭉개지는 대신 잔물결이 없고, 란초스(Lanczos)처럼 날카로운 알고리즘은 선명한 대신 대비가 큰 경계에서 오버슈트가 생긴다. 흥미로운 것은 사람의 선호다 — 한 참여자는 PSNR 같은 객관적 화질 지표는 대체로 링잉보다 흐림을 더 좋게 평가하지만, 사람의 주관적 평가는 반대로 기우는 경우가 많다고 지적했다. 어느 쪽이 “옳은” 축소인지에 대한 보편 정답이 없다는 뜻이다.

둘째 겹은 감마와 색 공간이다. 한 참여자는 감마 보정 문제를 제기했다. JPEG은 픽셀을 밝기에 선형인 값이 아니라 비선형으로 인코딩된 YCbCr 색 공간에 담는데, 축소를 위한 픽셀 평균을 이 비선형 값 위에서 곧바로 계산하면 물리적으로 틀린 평균이 나온다. 두 픽셀의 밝기를 섞으려면 원칙적으로 선형 광량으로 되돌린 뒤 평균해 다시 비선형으로 감마 인코딩해야 하는데, 이 단계를 건너뛰면 특히 밝기 대비가 큰 경계에서 미묘하게 어두워지거나 획이 얇아진다. 부분 디코딩 경로가 이 감마 보정을 건너뛸 가능성이 있다는 것이다. 여기에 더해 세 번째 겹으로, JPEG은 밝기(Y)보다 색(Cb, Cr) 정보를 절반 해상도로 깎아 저장하는 크로마 서브샘플링을 흔히 쓴다. 색 채널의 원래 해상도가 이미 낮으니, 이것을 어떻게 보간해 되살리며 축소하느냐에서 또 한 번 브라우저별 차이가 갈린다. 마지막 겹은 색 관리(color management)다. 이미지에 박힌 ICC 프로파일을 존중하는지, 디스플레이의 색역으로 어떻게 변환하는지도 브라우저와 OS·GPU 조합마다 다르다.

이 겹들이 왜 표준으로 봉합되지 않았는지가 핵심이다. JPEG·PNG 같은 포맷 표준은 “비트스트림을 어떻게 픽셀로 되돌리는가”까지는 규정한다. 그러나 그 픽셀을 화면 크기에 맞게 “어떻게 다시 축소하고, 어느 색 공간에서 섞고, 어떤 감마로 보정하는가”는 대체로 구현의 재량으로 남겨두었다. 표준이 침묵한 자리마다 브라우저는 각자의 공학적 판단 — 대개 속도와 화질의 저울질 — 으로 빈칸을 채웠고, 그 판단이 서로 달랐던 것이다. HN에서 한 참여자가 냉정하게 요약했듯, 화질이 정말 중요하다면 축소를 브라우저에 맡겨서는 안 된다. 브라우저는 최신 최고 화질 알고리즘(그는 란초스 3-lobe를 예로 들었다)을 일관되게 쓰지 않고 성능을 우선하며, 심지어 어떤 알고리즘이 쓰이는지가 탑재된 GPU에 따라 달라질 수도 있다는 것이다. 이 마지막 문장이 무겁다. 렌더링 결과가 사용자의 하드웨어에 의존한다면, “픽셀 퍼펙트”는 개발자가 통제할 수 있는 변수가 아니다.

‘픽셀 퍼펙트’라는 신화와 그 실무적 청구서

그러니 이 사건이 주는 첫 교훈은 인식의 전환이다. 웹에서 픽셀 퍼펙트는 달성 가능한 기본값이 아니라, 특정 조건에서만 근사되는 예외다. 같은 마크업과 같은 애셋이 같은 픽셀을 낳는다는 가정은, 폰트 힌팅·서브픽셀 렌더링·이미지 축소·색 관리처럼 브라우저·OS·GPU가 제각기 결정하는 수많은 하위 단계 앞에서 무너진다. 디자이너와 프런트엔드 개발자에게 이것은 추상적 철학이 아니라 구체적인 청구서로 돌아온다.

가장 실용적인 대응은 애초에 브라우저에게 축소를 시키지 않는 것이다. HN 토론이 거의 만장일치로 도달한 결론이 여기였다. 20×20으로 표시할 아이콘에 2000×2000 원본을 넣고 브라우저더러 줄이라고 하는 것은 대역폭 낭비이자 화질 도박이다. 표시할 크기에 맞는 해상도의 이미지를 내려주는 것이 먼저다. 아이콘·로고·라인아트처럼 경계가 또렷한 그래픽은 아예 래스터가 아니라 SVG로 가는 편이 낫다. 원문 저자가 결국 문제의 로고를 SVG로 교체해 해결했듯이 말이다. SVG는 어떤 배율에서도 선명하고 용량도 작으며, 다크 모드에 맞춰 색을 바꿀 수도 있다. 한 참여자는 크롬의 이 최적화가 일렉트론(Electron) 릴리스에까지 흘러들어 제품 곳곳의 아이콘이 망가졌고, SVG로 갈아탈 때까지 업그레이드를 미뤄야 했던 경험을 전했다. 같은 계열의 문제가 JPEG만이 아니라 축소된 PNG에서도 나타났다는 것이다. 어쩔 수 없이 래스터를 축소해야 한다면, 픽셀 아트처럼 보간을 원치 않는 경우엔 image-rendering: pixelated 같은 CSS로 브라우저의 매끄러운 축소를 끌 수도 있다. 다만 이것은 만능이 아니라 특정 상황의 도구다.

둘째 교훈은 테스트와 애셋 준비의 방식이다. 렌더링이 비결정적이라면, “내 크롬에서 잘 보인다”는 검증은 불완전하다. 시각적으로 중요한 요소는 여러 브라우저와 여러 픽셀 밀도(1x·2x·3x)에서 실제로 확인해야 하고, 화질이 사업적으로 중요하다면 축소는 빌드 파이프라인에서 통제된 알고리즘으로 미리 수행해 결과 이미지를 고정하는 편이 안전하다. 앞의 참여자 말처럼, 정말 화질에 목숨을 건다면 브라우저 대신 캔버스에 직접 렌더러를 짜거나 네이티브로 내려가는 선택지까지 있다. 물론 대부분의 서비스에는 과하다. 현실적 균형점은 “브라우저마다 픽셀이 조금씩 다를 수 있음을 받아들이되, 그 편차가 눈에 띄지 않도록 애셋을 적정 크기로 준비하는 것”이다.

셋째는 포맷 전환의 흐름이다. HN에서는 AVIF 지원율이 이미 95%에 이르렀으니 이제 JPEG보다 우선 고려할 때라는 목소리가 나왔다. 다만 새 포맷이 확산돼도 축소·감마·색 관리의 구현 차이라는 근본 문제가 사라지는 것은 아니다. 포맷은 “무엇을 저장하는가”를 정하지만, “화면에 어떻게 옮기는가”의 마지막 한 걸음은 여전히 구현의 몫으로 남기 때문이다. 낙관 시나리오는 브라우저들이 고품질 축소를 기본값으로 수렴시키는 것이고, 비관 시나리오는 GPU·OS 파편화가 오히려 심해지는 것이며, 현실은 그 사이 어딘가에서 개발자가 계속 애셋을 손수 다듬는 모습일 것이다.

결론

리드의 질문으로 돌아가자. 같은 JPEG이 브라우저마다 다르게 보이는 것은 버그인가, 아니면 웹의 본질인가. 답은 후자에 가깝다. 크롬의 부분 IDCT 스케일링은 명백한 오류라기보다 속도를 위해 화질을 내준 합리적 최적화였고, 파이어폭스의 다른 결과 역시 다른 최적화의 산물이었다. 두 브라우저 어느 쪽도 규칙을 어기지 않았다. 그들이 채운 것은 표준이 비워 둔 재량의 공간이었고, 그 공간이 존재하는 한 “픽셀 퍼펙트”는 보장이 아니라 희망이다.

그렇다고 통제를 포기하라는 말은 아니다. 오히려 반대다. 통제할 수 없는 것(브라우저 내부의 축소 파이프라인)과 통제할 수 있는 것(어떤 포맷을, 어떤 해상도로, 어떤 크기에 내려줄지)을 분명히 갈라, 후자에 집중하는 것이 프로의 태도다. 15픽셀 로고 하나가 알려준 것은 결국 이것이다 — 웹은 결정론적 캔버스가 아니라, 수많은 구현이 각자의 저울질로 협상하는 장(場)이다. 그 사실을 알고 애셋을 준비하는 개발자와, “내 화면에서는 잘 보이던데”라고 말하는 개발자 사이의 거리는 생각보다 멀다. 당신이 마지막으로 “픽셀까지 맞췄다”고 말한 그 화면은, 정말 모든 사용자의 화면에서 같은 픽셀이었는가?


출처: