WebP가 Safari에서 까맣게 변한 날

Safari에서 WebP 배경이 검정으로 변한 원인과 MSW 서비스 워커로 인한 Accept 헤더 문제 해결

2025년 9월 18일

🤔 Next.js <Image> 최적화를 쓰면, 굳이 원본을 WebP로 바꿀 필요가 있을까?

프로젝트를 하다 보면 로컬에 이미지를 모아두는 경우가 많습니다. 이미지가 많아지면 사용자 경험이 좋아질 수 있지만, 용량이 너무 크면 로딩 속도가 느려져서 오히려 불편해집니다.

그래서 png 파일들을 webp로 변환하려 했습니다. 그런데 Next.js <Image> 태그는 png 파일을 넣어도 자동으로 WebP나 AVIF 같은 최신 포맷으로 변환해줍니다.

굳이 원본을 WebP로 바꿀 필요가 있을까?

이미지 포맷 비교
PNG vs WebP 포맷 비교

사용자에게 제공되는 이미지는 Next.js 최적화를 거쳐 WebP나 AVIF로 변환되기 때문에, 사용자 경험 측면에서는 큰 차이가 없을 수 있습니다. 하지만 원본 파일 포맷에 따라 몇 가지 중요한 차이가 발생합니다.

1. 빌드 및 배포 시간

WebP는 PNG보다 파일 크기가 훨씬 작습니다. 프로젝트에 포함되는 원본 이미지 총 용량이 줄어들어 빌드 및 배포 시간이 단축됩니다. 이미지가 많은 프로젝트라면 차이가 더 두드러집니다.

2. 초기 최적화 효율

Next.js는 PNG처럼 용량이 큰 이미지를 WebP로 변환하는 데 더 많은 처리가 필요합니다. 원본이 이미 WebP라면 추가 포맷 변환 없이 크기 최적화(Resizing)에 집중할 수 있어 효율이 높아집니다.

결론적으로, 원본 이미지를 PNG 대신 WebP로 변환해 로컬에 저장하기로 했습니다.


🕶️ 이미지에 검정 배경이 뜬다?

WebP로 적용해보니 또 다른 문제가 발생했습니다. Safari와 모바일에서 이미지 배경이 검정색으로 보이는 문제였습니다. 🥹

  • 크롬, 파이어폭스 → 정상
  • Safari, 모바일 → 배경이 검정
검정 배경 문제
원래 투명이었던 이미지가 검정 배경으로 표시됨

처음엔 두 가지 원인을 생각했습니다.

  1. Safari 자체의 문제인가?
  2. WebP 변환을 잘못했나?

하지만 진짜 원인은 따로 있었습니다.

🔍 개발자 도구로 확인

이미지를 불러오는 방식을 확인하기 위해 개발자 도구를 열었습니다.

개발자도구 네트워크탭
네트워크 탭에서 확인한 요청 정보

다른 사이트에서는 아래처럼 나왔습니다.

서비스 워커와 Accept 헤더가 문제의 실마리였습니다.

📬 Accept 헤더란?

브라우저는 Accept 헤더를 통해 어떤 포맷을 지원하는지 서버에 알려줍니다. Next.js <Image> 컴포넌트는 이 값을 보고 브라우저에 맞는 포맷으로 변환합니다.

🛠️ 서비스 워커란? (feat. MSW)

서비스 워커는 브라우저와 서버 사이에서 동작하는 프록시 같은 존재입니다. 브라우저가 서버로 요청을 보내면, 서비스 워커가 먼저 그 요청을 가로챕니다.

MSW는 서비스 워커 위에서 동작하는 API 요청 모킹(mocking) 라이브러리입니다. 실제 서버 없이도 API 통신을 흉내내서 백엔드 API가 완성되지 않은 상황에서도 프론트엔드 개발을 진행할 수 있습니다.


🐛 첫 번째 문제: 운영 서버에서도 MSW가 켜져 있었다

첫 번째 문제는 운영 서버에서도 MSW가 활성화되어 있었다는 점입니다.

NEXT_PUBLIC_API_MOCKING이 없거나 false일 때 모킹이 켜지지 않게 하려는 의도였습니다. 하지만 실제로는 문제가 발생했습니다.

번들링/컴파일 시점

  1. NEXT_PUBLIC_API_MOCKING=false로 설정
  2. Next.js 컴파일러가 process.env.NEXT_PUBLIC_API_MOCKING을 문자열 "false"로 치환
  3. 번들된 JS 파일: experimental__runtimeEnv: { NEXT_PUBLIC_API_MOCKING: "false" }

2번 과정은 Next.js GitHub 코드에서 확인할 수 있습니다.

런타임 시점

  1. env.ts 실행, z.coerce.boolean()"false" 문자열을 받음
  2. Boolean("false")true
  3. MSWProvider에서 env.NEXT_PUBLIC_API_MOCKINGtrue로 평가되어 MSW 활성화

실제 배포 사이트에서도 NEXT_PUBLIC_API_MOCKING=false로 설정되어 있었고, 위 과정을 거쳐 true로 변환됐을 것입니다.

위와 같이 수정하여 "true"일 때만 true, 그 외 모든 값은 false가 되도록 했습니다.


🧩 두 번째 문제: Next.js Image API까지 가로챘다

Next.js <Image> 컴포넌트의 최적화는 서버 사이드에서 실시간으로 이루어집니다. 정적 파일을 직접 서빙하는 것이 아니라 API 엔드포인트(/_next/image)를 통해 처리됩니다.

서비스 워커는 네트워크 요청을 인터셉트하는데, /_next/image API 요청도 가로채서 헤더를 변조합니다. 브라우저가 어떤 포맷을 선호하는지 서버가 알 수 없게 되고, Next.js가 WebP 지원을 감지하지 못해 기본 포맷인 JPEG로 변환합니다.

JPEG는 투명도를 지원하지 않으므로 WebP 이미지를 JPEG로 변환하면 배경이 검정색으로 보입니다.

요청 흐름:

  1. 브라우저: GET /_next/image?url=/image.webp&w=300&q=75
    Headers: Accept: image/webp,image/avif,image/*,*/*;q=0.8

  2. MSW 인터셉트: 서비스 워커가 요청을 가로챔
    Headers: Accept: */* (헤더 변조 발생)

  3. Next.js Image API: 변조된 헤더를 받음
    → WebP 지원을 감지하지 못함
    → JPEG로 변환

  4. 결과: WebP → JPEG 변환으로 투명도 손실

❓ 왜 하필 JPEG로 바뀔까?

Next.js Image 최적화 코드를 확인했습니다.

제 경우:

  • 원본이 WebP
  • Accept가 변조되어 WebP 지원을 감지 못함
  • → JPEG로 강제 변환

🚫 서비스 워커에서 Next.js API 제외하기

/_next/image 엔드포인트를 MSW가 모킹하지 않도록 수정해야 했습니다.

기존 코드:

서비스 워커가 이미 요청을 가로챈 후에 호출되므로 헤더는 이미 변조된 상태입니다.

서비스 워커가 애초에 특정 경로만 가로채도록 수정했습니다:

수정 후 /_next/image 엔드포인트를 모킹하지 않아 헤더가 정상적으로 전달되고, WebP가 올바르게 내려와 검정 배경 문제가 해결됐습니다.

해결된 모습
정상적인 요청 헤더와 소스

📝 마치며

API를 하나라도 연결한 상태였다면 원인을 더 빠르게 찾을 수 있었을 것 같습니다. 하지만 이미지 문제를 해결하면서 요청 헤더와 Next.js GitHub 코드를 살펴볼 수 있었던 좋은 기회였습니다.

같은 문제를 겪을 경우는 드물겠지만, 다음 내용을 알아두시면 좋을 것 같습니다.

  1. 각 이미지 포맷의 특성
  2. Next.js <Image> 최적화 동작 방식
  3. Next.js의 env 처리 방식 (env 변수를 문자열로 치환하므로 boolean 처리에 주의)
  4. 서비스 워커 스코프 설정

원인을 찾는 과정 자체가 큰 공부가 되었고, 같은 문제를 겪는 분께 도움이 되었으면 좋겠습니다 🫠