ToyProject

[Catarie] 상용 배포 후 이미지들이 다르게 뜨는 이슈

sihanni 2025. 9. 13. 07:05

ㅜㅜ

Catarie를 드디어 세상에 내놓고 터진 도파민은 얼마 못가 식어버렸는데, 버튼 이미지가 이상하게 보이는 것이었다.

Next.js next/image 는 /_next/image?url=...&w=...&q=... 형태로 쿼리스트링에 의존하게 되는데 CloudFront가 쿼리스트링을 캐시 키에 포함하지 않으면, 서로 다른 이미지가 같은 캐시 오브젝트로 덮여서 뒤바뀌어 보인다고 한다.

즉, 코드에 /public/images/*.png를 넣었더라도 <Image />를 쓰는 순간 Next의 이미지 최적화 프록시 (/_next/image)를 타게 되고, 여기서 캐시 키 설정을 잘못하면 아이콘이 뒤바뀐다는 것이다!

/_next ..?

/_next/* 는 Next.js가 내부적으로 사용하는 내부 전용 경로이다. 빌드 산출물(JS/CSS 청크 등)과 이미지 최적화 엔드포인트가 여기에 있다.

  • /_next/static/* : 빌드된 정적 청크들 (해시 포함된 파일명)
  • /_next/image : <Image /> (next/image)를 썼을 때 동작하는 이미지 최적화 프록시 URL로, 쿼리스트링 ( ?url=...&w=...&q=...)이 핵심이다.

이 때 만약 코드내에서 <img src="/images/play.png" /> 와 같은 형태로 /public 폴더로 바로 사용하면 /_next/image 는 아예 쓰이지 않는다고 한다. 하지만 반대로 <Image src="....">를 쓰면 내부에서 /_next/image?... 로 요청이 나간다고 한다.

 

그럼 이미지의 캐시에 문제가 있다고 추측하게 되었고, AWS CloudFront 쪽을 살펴보기로 했다.

프로필 이미지의 경우 S3에 저장을하고, 그에 맞게 CloudFront에 경로 패턴을 지정해주었는데, web을 구성하는 이미지는 S3에 있는 것이 아니라 깃 리포지토리 내의 public/ 내에 존재한다는 차이점이 있고, 그리고 S3에서 가져오지 않는 이미지여서 따로 경로 패턴이나 원본의 원본 경로를 지정해주지 않았었다. 이미지를 S3에 두고 가져오는 것이 아니라면 /images/* 가 항상 같은 오리진의 Next.js 서버로 가도록 동작 (Behavior)를 분리/ 우선순위 설정을 해줬어야 했던 것 같다.

그리고 지금같은 사용 형태 ( <Image/> ) 를 쓰면 요청이 /_next/image?url=...&w=...&q=... 와 같은 형태로 나가서 CloudFront가 QueryFront가 Query String을 캐시 키/오리진에 전달해야 해상도/품질별 이미지가 섞이지 않게 될 수 있다고한다.

그래서 <Image> 경로 (/_next/image*)는 Query strings = All로 설정해야한다고 한다.

 

실제로 curl을 통해 확인을 해보자.

# 같은 파일, 다른 w/q로 요청했을 때 서로 다른 캐시 키로 취급되는지
curl -sI "https://<도메인>/_next/image?url=%2Fimages%2Fpause.png&w=32&q=75" | sed -n '1,20p'
curl -sI "https://<도메인>/_next/image?url=%2Fimages%2Fpause.png&w=64&q=75" | sed -n '1,20p'

 

결과는 아래와 같았다.

content-length: 0
etag: 47DEQpj8HBSa...  
x-nextjs-cache: MISS
x-cache: Hit from cloudfront
  • content-length: 0 으로 빈 응답이 캐시에 들어간 상태. (이미지 본문이 0 바이트로 캐시됨)
    • 47DEQpj8HBSa...  는 빈 바디의 해시로 유명한 값이라고 한다.
  • x-cache: Hit from cloudfront: CloudFront가 그 빈 응답을 캐시해서 바로 내주고 있는 상황
  • x-nextjs-cache: MISS: Next.js 쪽은 매번 새로 처리하려고 하지만(혹은 제대로 파라미터를 못 받아서), 어쨌든 CloudFront가 먼저 캐시에서 빈 응답을 꺼내 주는 상태

현재 /_next/image?... 로 가는 요청에서 쿼리스트링/Accept가 캐시 키(또는 오리진 포워딩)에 포함되어 있지 않아서, 한 번 잘못 생성된 빈 응답이 모든 경우에 재사용되고 있는 것이었다. 그 결과, 아이콘이 랜덤으로 보였던 것이다.

해결

web쪽 CloudFront 배포에 가서 /_next/image* 전용 Behavior(동작)를 만들고, 캐시 정책 + 오리진 요청 정책을 변경해보기로 했다.

경로 패턴을 추가

 

캐시 정책을 새롭게 추가
원본 요청 정책도 새롭게 생성

그리고 마지막으로 추가한 동작은 우선순위를 기존 것보다 높혀 둔다. 그리고 무효화를 진행해보았다.

ㅎㅎㅎㅎ

짜잔 해결되었다.

오늘 새로 배운 내용 정리

CloudFront가 캐시 키에 이 쿼리스트링(url,w,q)Accept 헤더를 포함하지 않으면 서로 다른 아이콘/크기가 한 캐시에 섞여 버린다.

'ToyProject' 카테고리의 다른 글

[catarie] AWS 서비스 정리  (0) 2025.12.16
[Catarie] 회고  (0) 2025.11.23
[Catarie] error-case: audio HLS 변환 실패, 재생실패  (1) 2025.09.01
[Catarie] HLS와 업로드 흐름  (1) 2025.08.28
[Catarie 작업일지] 업로드  (0) 2025.08.27