iOS 16에서 AVIF 이미지가 깨지는 문제 — 지원한다면서 왜 안 보이는 걸까
iOS 16.0~16.3의 Safari가 AVIF를 "지원한다"고 선언하면서 실제로는 렌더링에 실패하는 문제의 원인을 분석하고, picture 요소의 폴백이 왜 작동하지 않는지, 그리고 이를 해결한 과정을 정리합니다.
증상
포케코리아에서 사용자로부터 “iOS에서 포켓몬 이미지가 안 보인다”는 제보가 들어왔다. 확인해보니 iOS 16 버전의 Safari에서 포켓몬 이미지가 아예 표시되지 않고 빈 영역만 보이는 상태였다. iOS 15에서는 정상적으로 보이고 있었다.
이미지 로딩 자체가 실패하는 것이 아니라, 로드는 되는데 렌더링이 안 되는 이상한 상황이었다.
원인 분석
AVIF와 <picture> 요소
포케코리아는 이미지 최적화를 위해 <picture> 요소로 AVIF → WebP 순서의 폴백 체인을 구성하고 있었다.
<picture className="w-full h-full block">
<source type="image/avif" srcSet={generateAvifSrcSet()} />
<source type="image/webp" srcSet={generateWebpSrcSet()} />
<img src={fallbackSrc} alt={alt} />
</picture>
<picture> 요소는 <source>를 위에서 아래로 순회하며, 브라우저가 type을 지원하면 해당 소스를 선택하고, 지원하지 않으면 다음 소스로 넘어간다. 모두 불가능하면 <img>의 src를 폴백으로 사용한다.
iOS 15 이전 버전은 AVIF를 인식하지 못하므로 자동으로 WebP <source>가 선택되어 정상 동작했다. 문제는 iOS 16에서 시작됐다.
iOS 16의 AVIF “지원”
Can I Use에 따르면 iOS Safari는 16.0부터 AVIF를 지원한다. 그런데 초기 버전(16.0~16.3)의 AVIF 구현에는 심각한 버그가 있었다.
- Safari가 AVIF를 **“지원한다고 인식”**하여
<source type="image/avif">를 선택 - 선택한 AVIF 파일의 디코딩에 실패
- 디코딩 실패 후 다음
<source>로 폴백하지 않음 - 결과: 빈 이미지
이것이 이 버그의 핵심이다. 브라우저가 type을 “지원한다”고 판단하면 해당 source를 선택하고, 이후 실제 디코딩이 실패해도 다음 source로 넘어가지 않는다. <picture> 요소의 폴백 메커니즘이 “지원하지만 버그가 있는” 상황에 대응하지 못하는 구조적 한계다.
Accept 헤더도 마찬가지
서버 사이드에서 Accept 헤더 기반으로 포맷을 결정하는 경우에도 같은 문제가 발생한다. iOS 16.0~16.3 Safari는 HTTP Accept 헤더에 image/avif를 포함시키므로, CDN이나 이미지 최적화 서비스(next/image, Cloudflare Image Resizing 등)가 AVIF를 서빙하게 된다.
Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8
결국 클라이언트의 <picture> 폴백도, 서버의 Accept 헤더 기반 선택도, 둘 다 iOS 16의 “false positive” 앞에서 무력하다.
참고: Cloudflare Community — iOS 16, AVIF and image resizing bug
버그의 기술적 배경
WebKit은 libavif와 dav1d 디코더를 사용하여 AVIF를 지원한다. iOS 16.0이 AVIF를 최초로 지원한 버전이었기 때문에 디코더의 엣지 케이스 처리가 부족했다.
보고된 구체적인 증상들은 다음과 같다.
- 이미지가 아예 빈 영역으로 표시
- 색상이 완전히 틀리게 렌더링 (녹색/보라색 틴트)
- 이미지 일부만 표시되고 나머지가 회색/검정
- 알파 채널이 포함된 AVIF에서 빈번하게 발생
- 큰 해상도 AVIF에서 디코딩 실패
iOS에서 Safari/WebKit은 OS 업데이트로만 갱신되므로, 사용자가 iOS를 업데이트하지 않으면 버그가 지속된다는 점도 문제였다.
참고: WebKit Bug #241904 — AVIF 이미지 지원 관련, WebKit Bug #207750 — AVIF 디코딩 지원
해결: AVIF source 제거
여러 해결 방법을 검토했다.
| 방법 | 장점 | 단점 |
|---|---|---|
| UA 기반으로 iOS 16.0~16.3만 제외 | AVIF 혜택 유지 | UA 파싱 복잡, 유지보수 부담 |
| JavaScript 디코딩 테스트 | 정확한 감지 | 초기 로딩 지연, 복잡도 증가 |
| AVIF source 제거, WebP만 사용 | 간단, 확실 | AVIF 대비 약간의 용량 증가 |
결론은 AVIF <source>를 제거하고 WebP만 사용하는 것이었다.
// Before
<picture className="w-full h-full block">
<source type="image/avif" srcSet={generateAvifSrcSet()} />
<source type="image/webp" srcSet={generateWebpSrcSet()} />
<img src={fallbackSrc} alt={alt} />
</picture>
// After
<picture className="w-full h-full block">
<source type="image/webp" srcSet={generateWebpSrcSet()} />
<img src={fallbackSrc} alt={alt} />
</picture>
generateAvifSrcSet() 함수도 함께 제거했다.
WebP로 충분한 이유
AVIF는 WebP보다 약 20% 더 작은 파일 크기를 달성할 수 있지만, 포케코리아의 이미지는 대부분 포켓몬 일러스트로 크기가 크지 않다. WebP의 압축 효율도 JPEG 대비 25~34% 우수하며, 알파 채널과 애니메이션도 지원한다.
| 포맷 | JPEG 대비 압축 효율 | iOS 지원 | 안정성 |
|---|---|---|---|
| AVIF | ~50% 절감 | 16.0+ (버그 있음) | 불안정 (16.0~16.3) |
| WebP | ~30% 절감 | 14+ | 완벽 지원 |
AVIF의 20% 추가 절감보다 전 iOS 버전에서의 안정적인 렌더링이 더 중요했다. 호환성 문제로 이미지가 아예 안 보이는 것은 용량 절감 이상의 손해다.
AVIF 포맷에 대한 정리
이 트러블슈팅을 계기로 AVIF 도입 시 고려해야 할 사항을 정리했다.
AVIF란
AVIF(AV1 Image File Format)는 AOMedia가 개발한 차세대 이미지 포맷으로, AV1 비디오 코덱의 키프레임 압축 기술을 정지 이미지에 적용한 것이다.
| 항목 | JPEG | WebP | AVIF |
|---|---|---|---|
| 출시 | 1992 | 2010 | 2019 |
| 압축 효율 | 기준 | +25~34% | +50%+ |
| HDR | 미지원 | 제한적 | 10/12비트 지원 |
| 알파 채널 | 미지원 | 지원 | 지원 |
| 인코딩 속도 | 빠름 | 보통 | 느림 |
| 로열티 | 프리 | 프리 | 프리 |
압축 효율에서는 확실한 장점이 있지만, 인코딩이 느리고 브라우저 지원 역사가 짧다는 점이 걸림돌이다.
참고: Jake Archibald — AVIF has landed, Netflix — AVIF for Next Generation Image Coding
AVIF 도입 시 체크리스트
이번 경험을 바탕으로 AVIF 도입 전 확인해야 할 사항을 정리했다.
| 항목 | 확인 내용 |
|---|---|
| 타겟 브라우저 | iOS 16.4+ / Safari 17+ 이상인지 |
| 폴백 전략 | <picture> 폴백만으로는 부족, 서버사이드 감지 필요 |
| CDN 설정 | Accept 헤더 기반 자동 포맷 선택 시 문제 iOS 버전 제외 가능한지 |
| 이미지 특성 | 사진 위주인지 (AVIF 효과 큼), 일러스트 위주인지 (WebP로 충분) |
| 인코딩 파이프라인 | 빌드 타임 인코딩 가능한지 (AVIF 인코딩은 느림) |
현재 시점(2026년)에서의 판단
iOS 16.4 이후와 iOS 17/18에서는 AVIF 지원이 안정화되었다. iOS 16.0~16.3 사용자 비율은 현재 매우 낮으므로 AVIF를 다시 도입할 수 있는 상황이다. 다만 방어적 폴백 코드는 여전히 권장된다.
포케코리아에서는 현재 WebP로 충분한 품질과 성능을 확보하고 있어, AVIF 재도입은 우선순위가 높지 않다.
포맷 감지 방법 비교
이 버그 이후 이미지 포맷 지원 여부를 감지하는 방법들을 조사했다. 향후 AVIF를 다시 도입할 때를 대비한 정리다.
1. <picture> + type 속성
<picture>
<source type="image/avif" srcset="image.avif">
<source type="image/webp" srcset="image.webp">
<img src="image.jpg" alt="설명">
</picture>
한계: 이번 버그에서 확인한 것처럼, type을 “지원한다”고 인식하지만 실제 디코딩이 실패하는 경우에 대응 불가.
2. JavaScript 디코딩 테스트
const supportsAvif = async () => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img.width > 0 && img.height > 0);
img.onerror = () => resolve(false);
img.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlm...'; // 1x1 AVIF
});
};
장점: 실제 디코딩을 시도하므로 false positive를 걸러낼 수 있다. 한계: 1×1 테스트 이미지는 성공하지만 큰 이미지에서는 실패하는 엣지 케이스가 존재한다.
3. UA 기반 서버사이드 감지
const isProblematicIOSSafari = (userAgent) => {
const match = userAgent.match(/iPhone OS (\d+)_(\d+)/);
if (!match) return false;
const major = parseInt(match[1]);
const minor = parseInt(match[2]);
return major === 16 && minor <= 3;
};
장점: 서버에서 즉시 판단 가능, 클라이언트 코드 불필요. 한계: UA 문자열에 의존하므로 정확도가 떨어질 수 있다.
4. CSS @supports
CSS @supports로는 이미지 포맷 지원 여부를 직접 확인할 수 없다. CSS 속성/값의 지원 여부만 확인 가능하다.
가장 확실한 방법은 JavaScript 디코딩 테스트 + UA 기반 감지를 조합하는 것이다. 하지만 복잡도가 높아지므로, 호환성 문제가 있는 포맷은 과감히 빼는 것도 좋은 선택이다.
정리하며
이 버그에서 배운 교훈은 **“브라우저가 지원한다고 말하는 것과, 실제로 동작하는 것은 다를 수 있다”**는 것이다.
<picture> 요소의 type 기반 폴백은 “지원 여부”를 기준으로 분기하지, “정상 동작 여부”를 기준으로 분기하지 않는다. 새로운 이미지 포맷을 도입할 때는 Can I Use의 초록색 체크마크만 보고 판단하면 안 된다. 특히 해당 포맷을 최초로 지원하기 시작한 버전에서는 초기 구현의 불안정성을 항상 고려해야 한다.
결국 가장 중요한 것은 사용자에게 이미지가 보이는가이다. 최신 포맷의 압축 효율보다 안정적인 렌더링이 항상 우선이다.
참고 자료
- Can I Use — AVIF
- WebKit Bug #241904 — AVIF 이미지 지원
- WebKit Bug #207750 — AVIF 디코딩 지원
- Cloudflare Community — iOS 16, AVIF and image resizing bug
- Jake Archibald — AVIF has landed
- Netflix — AVIF for Next Generation Image Coding
- Google Developers — WebP
- web.dev — Serve images in modern formats
- MDN —
<picture>element