[실전 미디어 02] 왜 webp만 저장하기로 했나

지난 편에서 원본을 그대로 서빙하지 않고 업로드 순간에 표준화한다는 원칙을 세웠다. 그렇다면 표준이 될 포맷은 무엇이어야 하는가. 이미지에서 내가 내린 첫 번째 굵직한 결정은 들어오는 모든 이미지를 단 하나의 포맷으로 강제 변환한다는 것이었다. 그 포맷은 webp였다. 여러 포맷을 나란히 지원하는 대신 하나로 통일한 이 결정이 뒤따르는 거의 모든 이미지 처리를 단순하게 만들어줬다.


1. 포맷 파편화라는 실제 문제

통일하기 전에는 스토리지 안에 JPEG, PNG, GIF, 아이폰에서 온 HEIC까지 온갖 포맷이 뒤섞여 있었다. 각 포맷은 압축 특성도, 브라우저 호환성도, 투명도 지원 여부도 제각각이었다. 표시 코드는 매번 확장자를 보고 분기해야 했고, 어떤 포맷은 특정 브라우저에서 열리지 않아 깨진 이미지로 떴다.


파편화는 최적화도 가로막았다. 포맷마다 압축 파라미터가 다르니 일괄로 품질을 조정하거나 크기를 줄이는 작업을 짤 수가 없었다. 같은 사진이라도 JPEG로 온 것과 PNG로 온 것의 용량이 몇 배씩 차이 났다. 특히 스크린샷을 PNG로 올리는 사용자가 많았는데, PNG는 사진성 이미지에 대해 JPEG나 webp보다 훨씬 컸다.


한 포맷으로 강제하면 이 모든 게 사라진다. 저장 시점에 이미 webp라는 걸 알기 때문에 표시 코드는 분기가 필요 없고, 품질 정책도 한 곳에서 통제되며, 브라우저 호환성 걱정도 한 번만 하면 된다. 파편화를 입구에서 막는 것이 출구에서 매번 대응하는 것보다 압도적으로 쌌다. 입구는 한 곳이지만 출구는 목록, 본문, 공유 카드, 검색 노출까지 수없이 많았기 때문이다. 한 지점에서 통제하면 나머지 모든 지점이 공짜로 단순해졌다.


물론 통일에는 대가가 있었다. 원본 포맷의 고유 특성, 예를 들어 무손실이 필요한 도표나 투명 배경 같은 요소를 어떻게 보존할지 따로 고민해야 했다. 하지만 대다수 업로드가 사진과 스크린샷이라는 현실을 보면, 소수의 예외를 특별히 처리하는 편이 다수를 위해 파편화를 감수하는 것보다 나았다.


2. 왜 하필 webp였나

후보는 사실상 셋이었다. 오래된 JPEG, 요즘의 webp, 그리고 더 새로운 AVIF. JPEG는 범용성은 최고지만 투명도를 지원하지 않고 압축 효율이 낡았다. 스크린샷과 사진이 섞이는 커뮤니티 특성상 투명도를 못 쓰는 건 치명적이었다. 그래서 JPEG는 일찌감치 기본 포맷 후보에서 빠졌다.


webp는 사실상 모든 주요 브라우저가 지원한다. 최신 조사로도 전 세계 브라우저의 97퍼센트 안팎이 webp를 읽을 수 있었다. 같은 화질에서 JPEG보다 대략 25에서 35퍼센트 작았고, 투명도와 무손실 모드까지 갖췄다. 사진은 손실 모드로 작게, 도표나 아이콘은 무손실로 깔끔하게, 하나의 포맷 안에서 둘 다 처리할 수 있다는 점이 결정적이었다. 사진과 스크린샷이 한 글 안에 뒤섞이는 커뮤니티에서, 포맷을 나누지 않고도 각각에 맞는 압축을 줄 수 있다는 건 운영을 크게 단순하게 만들어줬다.


인코딩 속도도 중요한 근거였다. webp 인코딩은 대체로 빠르다. 2메가짜리 이미지를 1초 안에 webp로 인코딩할 수 있는데, 이건 뒤에 나올 브라우저 안에서의 실시간 변환에서 특히 중요했다. 사용자가 파일을 고르자마자 변환이 끝나야 업로드 체감이 빠른데, 느린 인코더로는 그 경험을 못 만든다.


정리하면 webp는 호환성과 압축률과 속도의 균형점이었다. 어느 하나에서 압도적이진 않지만 셋 다 합격점 이상이었고, 하나의 포맷으로 사진과 도표를 모두 커버한다는 유연성까지 있었다. 무난하다는 것이 오히려 기본값으로서 최고의 미덕이었다. 특정 지표에서 1등인 포맷은 다른 지표에서 대개 약점을 드러내는데, 기본값은 최고점이 아니라 모든 상황에서 낙제하지 않는 균형이 더 중요했다. webp는 딱 그런 포맷이었다.


3. 더 좋은 AVIF를 기본으로 안 쓴 이유

더 새로운 AVIF는 압축률이 webp보다 확실히 좋다. 같은 화질에서 webp보다 20에서 50퍼센트까지 더 작아지고, 특히 사진성 이미지에서 강했다. 숫자만 보면 AVIF를 기본으로 삼는 게 맞아 보였다. 하지만 나는 webp를 기본으로 두고 AVIF는 보조로만 쓰기로 했는데, 이유가 세 가지 있었다.


첫째, 인코딩이 너무 느렸다. AVIF 인코딩은 같은 이미지에서 webp보다 열 배에서 마흔 배까지 느릴 수 있다. 2메가 이미지가 webp로는 1초 안이지만 AVIF로는 수십 초가 걸리기도 한다. 브라우저 안에서 사용자를 기다리게 하며 실시간 인코딩하기엔 부적합했다.


둘째, 호환성이 아직 webp만큼은 아니었다. AVIF 지원은 95퍼센트 안팎으로 올라왔지만 webp의 97퍼센트에는 못 미쳤고, 특히 값싼 구형 안드로이드 기기에서 디코딩이 눈에 띄게 느렸다. 커뮤니티 사용자층에는 이런 기기가 적지 않아 무시할 수 없었다.


그래서 절충안은 이랬다. 저장과 기본 서빙은 webp로 통일하되, 필요하면 뒤에 나올 온더플라이 변환 계층에서 요청하는 브라우저에 한해 AVIF를 내려주는 식이다. 기본은 빠르고 안전한 webp, 최적화는 지원되는 곳에서만 AVIF. 이렇게 하면 AVIF의 압축 이득을 취하면서도 인코딩 부담과 호환성 리스크를 기본 경로에서 떼어낼 수 있었다.


4. 강제 변환을 실제로 강제하는 법

정책을 세우는 것과 강제하는 것은 다르다. 업로드된 파일이 진짜 webp인지 어떻게 보장할 것인가. 확장자만 믿을 수는 없었다. 파일 이름을 webp로 바꾼다고 내용이 webp가 되는 건 아니기 때문이다. 그래서 서버는 파일이 도착하면 확장자가 아니라 파일 앞머리의 시그니처 바이트를 직접 읽어 실제 포맷을 판별했다.


이 검증은 단순한 형식 확인 이상의 역할을 했다. 클라이언트가 변환에 실패했는데도 실패를 숨기고 엉뚱한 포맷을 webp인 척 보내는 경우를 잡아내는 최후의 방어선이었다. 실제로 이 검증 덕분에 뒤에 나올 사건, 즉 브라우저가 조용히 다른 포맷을 흘려보낸 문제를 데이터로 확인할 수 있었다.


변환 흐름은 두 겹이었다. 가벼운 이미지는 브라우저가 webp로 만들어 보내고, 서버는 시그니처만 확인한다. 혹시 브라우저 변환이 어려운 케이스가 서버까지 원본으로 올라오면, 서버측 인코더가 한 번 더 webp로 바꾼다. 어느 경로로 들어오든 최종 저장물은 webp라는 것을 이 이중 구조가 보장했다.


다만 서버측 인코더는 만능이 아니었다. 정적 이미지 한 장은 잘 바꾸지만 움직이는 이미지나 아주 큰 이미지에서는 한계가 있었다. 이 한계들이 각각 별도의 편을 차지할 만큼 골치 아픈 문제로 자라났고, 그 첫 번째가 바로 브라우저 변환의 함정이었다.


5. 남은 예외와 트레이드오프

webp 통일이 모든 걸 공짜로 해결하진 않았다. 가장 큰 예외는 움직이는 이미지였다. GIF나 애니메이션 webp는 여러 프레임을 담는데, 단순한 정적 인코더로 바꾸면 첫 프레임만 남고 움직임이 사라진다. 이 문제는 별도 편에서 제대로 다뤄야 할 만큼 까다로웠다.


품질 손실도 신경 써야 했다. 손실 압축은 사진에는 티가 안 나지만 텍스트가 많은 스크린샷에서는 글자 가장자리가 뭉개질 수 있다. 그래서 나는 사진과 텍스트성 이미지에 대해 품질 값을 다르게 가져가는 쪽을 검토했다. 텍스트가 많으면 품질을 조금 더 높여 가장자리를 살리는 식이다.


투명도가 필요한 이미지도 챙겨야 했다. webp는 알파 채널을 지원하니 다행이지만, 변환 과정에서 배경을 잘못 채우면 투명이 검정으로 바뀔 수 있다. 인코더 옵션에서 알파 보존을 명시적으로 켜두는 것이 필요했다. 이런 세부는 하나라도 놓치면 특정 이미지에서만 조용히 깨진다. 그리고 이런 종류의 버그는 대다수 이미지에서 멀쩡하다가 어쩌다 한 장에서만 터지기 때문에, 재현과 발견이 늦어져 사용자 신뢰를 야금야금 갉아먹는다.


이런 예외들이 있었지만 하나의 포맷으로 통일한 결정 자체는 후회한 적이 없다. 예외는 예외로 특별 처리하면 되지만, 파편화는 시스템 전체를 상시 복잡하게 만들기 때문이다. 다음 편에서는 이 통일을 실제로 구현하며 처음 크게 데인 문제, 브라우저 안에서 이미지를 압축할 때 인코더가 조용히 다른 포맷을 뱉어내던 함정을 다룬다.