
WebP가 Safari에서 까맣게 변한 날
Safari에서 WebP 배경이 검정으로 변한 원인과 MSW 서비스 워커로 인한 Accept 헤더 문제 해결
2025년 9월 18일
Safari에서 WebP 배경이 검정으로 변한 원인과 MSW 서비스 워커로 인한 Accept 헤더 문제 해결
2025년 9월 18일
<Image>
최적화를 쓰면, 굳이 원본을 webp로 바꿀 필요가 있을까?프로젝트를 하다 보면 로컬에 이미지를 모아두는 경우가 많습니다.
이미지가 많아지면 당연히 사용자 경험이 좋아질 수 있겠죠. 하지만 이미지 용량이 너무 크면 로딩 속도가 느려져서 오히려 불편해집니다.
그래서 저는 프로젝트를 진행하면서 png
파일들을 webp
로 변환하려 하였습니다.
그런데 Next.js <Image>
태그는 png 파일을 넣어도 자동으로 webp나 avif 같은 최신 포맷으로 변환해줍니다.
이러한 장점으로 인해 문득 이런 생각이 들었습니다. 굳이 원본을 webp로 바꿀 필요가 있을까?
사용자에게 제공되는 이미지는 Next.js의 이미지 최적화 과정을 거쳐 WebP나 AVIF 같은 최신 포맷으로 변환되기 때문에, 사용자 경험 측면에서는 큰 차이가 없을 수 있습니다.
하지만 원본 파일을 무엇으로 사용했느냐에 따라 몇 가지 중요한 차이가 발생합니다.
1.빌드 및 배포 시간
WebP는 PNG보다 파일 크기가 훨씬 작습니다.
따라서 프로젝트에 포함되는 원본 이미지들의 총 용량이 줄어들어 빌드 및 배포 시간이 단축될 수 있습니다. 특히 이미지가 많은 프로젝트라면 그 차이는 더욱 두드러집니다.
2.초기 최적화 효율
Next.js는 PNG와 같은 용량이 큰 이미지를 WebP로 변환하는 데 더 많은 처리 과정이 필요합니다.
이미 원본이 WebP라면, Next.js는 추가적인 포맷 변환 없이 크기 최적화(Resizing)에 집중할 수 있어 최적화 과정의 효율이 더 높아집니다.
그래서 최종적으로, 저는 원본 이미지를 png 대신 WebP로 변환해 로컬에 저장해서 사용하기로 했습니다.
그런데 막상 WebP로 적용해보니, 또 다른 문제가 발생했습니다.
바로 Safari와 모바일에서 이미지 배경이 검정색으로 보이는 문제였습니다. 🥹
처음엔 이렇게 두가지 원인을 생각했습니다.
하지만 진짜 원인은 따로 있었습니다.
이미지를 불러오는 방식을 확인하기 위해 개발자 도구를 켜서 확인해보았습니다.
하지만, 이 내용은 제가 이전에 보았던 것과 달라 이 부분을 더 관심을 가지고 파해치기 시작했습니다.
다른 사이트를 가보니 아래처럼 나오는 것을 확인할 수 있었습니다.
이 상황이 이상해서 "도대체 왜 이럴까?" 싶어 더 파고들었고,
결국 서비스 워커와 Accept 헤더가 문제를 추적할 수 있게 해준 실마리라는 걸 알게 됐습니다.
Accept
헤더가 뭐길래?브라우저는 Accept
헤더를 통해 어떤 포맷을 지원하는지 서버에 알려줍니다.
Next.js는 <Image>
컴포넌트를 쓰면 이 값을 보고 브라우저 맞춤 포맷으로 변환합니다.
서비스 워커는 브라우저와 서버 사이에서 동작하는 프록시 같은 존재입니다.
브라우저가 서버로 네트워크 요청을 보내면, 서비스 워커가 먼저 그 요청을 가로챕니다.
MSW는 이 서비스 워커 위에서 동작하는 API 요청 모킹(mocking) 라이브러리입니다
프론트엔드에서 실제 서버 없이도 API 통신을 흉내내 백엔드 API가 아직 완성되지 않은 상황에서도 프론트엔드 개발을 진행할 수 있습니다.
여기서 드러난 첫 번째 문제는 운영 서버에서도 MSW가 켜져 있었다는 점입니다.
위와 같이 개발했던 이유는 NEXT_PUBLIC_API_MOCKING
가 없을 때와
NEXT_PUBLIC_API_MOCKING=false
일 때, mocking이 켜지지 않게 하기 위함이였습니다.
그런데 실제로 저렇게 코드를 짤 경우 문제가 생겼습니다.
- NEXT_PUBLIC_API_MOCKING=false 로 존재하는 경우
- Next.js 컴파일러가 process.env.NEXT_PUBLIC_API_MOCKING를 문자열 "false"로 치환
- 번들된 JS 파일:
experimental__runtimeEnv: { NEXT_PUBLIC_API_MOCKING: "false" }
2번의 과정은 github nextjs 코드에서 찾아볼 수 있었습니다.
- env.ts 실행 z.coerce.boolean()이 "false" 문자열 받음
- Boolean("false") → true
- MSWProvider에서 env.NEXT_PUBLIC_API_MOCKING가 true로 msw 활성화
실제 배포한 사이트에도 NEXT_PUBLIC_API_MOCKING가 false
로 들어가있는 것을 확인하여 위와 같이 true
로 변환됬을 것이라 생각합니다.
위와 같이 수정하여 "true"
→ true
, 그 외 모든 값 → false
가 나올 수 있도록 수정하였습니다.
nextjs에서 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로 변경되는걸까도 궁금하여 nextjs의 Image 최적화 코드를 찾아보게되었습니다.
즉, 제 경우엔
→ 그래서 jpeg로 강제 변환이 되었다는 것을 확인 할 수 있었습니다.
그렇다면, 해결하기 위해 nextjs의 API 엔드포인트(/_next/image
)는 msw가 모킹하지 않도록 수정해야했습니다.
기존엔 msw에서 _next 요청이 아래와 같이 처리 되어있었습니다
이는 서비스워커가 이미 요청을 가로 챈 후에 호출이되어 요청 헤더는 이미 변조된 상태이므로 해결되지 않았습니다.
그래서 아래와 같이 서비스워커가 애초에 특정 경로만 가로챌 수 있도록 수정하였습니다
이렇게 수정한 후엔 nextjs의 API 엔드포인트(/_next/image
)를 모킹하지 않아 정상적으로 헤더가 보이고,
정상적으로 webp가 내려와서 검정 배경 문제도 해결됐습니다.
아마 api를 한개라도 연결한 상태였다면 원인을 조금 더 빠르게 찾을 수 있었을 것 같지만,
이미지 문제를 해결하기 위해 요청 헤더, nextjs github 코드를 보면서 해결해 볼 수 있었던 어쩌면 좋은 기회가 된 것 같습니다.
같은 문제를 겪을 경우는 별로 없을 것 같지만,
<Image>
의 최적화 진행방식 코드등을 알아가시면 좋을 것 같습니다 !
결국 원인을 찾는 과정 자체가 큰 공부가 되었고, 혹시라도 같은 문제를 겪는 분께 도움이 되었으면 좋겠습니다 🫠