AWS S3 이미지 링크를 <img>의 src 속성에 직접 임베드하지 않고 Fetch API 요청으로 blob 객체를 받아올 때, CORS 오류가 발생함. S3 서버에 이미 CORS 설정이 되어있는데다가, 모든 이미지에서 CORS 오류가 발생하는 것은 아니었기 때문에 디버깅에 어려움이 있었음. 검색을 해보니 HTTP 캐시와 관련된 문제임을 확인할 수 있었고, 오류도 아래와 같은 상황에서 재현할 수 있었음.
① 웹페이지에 들어가고 나서 시간이 흐른 후 다시 들어갔을 때, CORS 오류가 발생함.
② 개발자 도구에서 '캐시 사용 중지' 기능을 활성화 했을 때 CORS 오류가 발생하지 않음.
브라우저에서 이미지를 캐싱할 때, 이미지만 캐싱을 하는 것이 아니라 HTTP 응답 그 자체를 캐싱함. 아래와 같이 CORS 오류가 발생했음에도 최초 요청은 성공했기 때문에 응답 상태 코드가 200인 것에서도 그 점을 확인할 수 있음.
CORS 오류가 발생한 요청: 요청까지 200(Success)으로 캐싱되어 있음
또한, 캐싱된 요청의 헤더에는 사진에서도 볼 수 있듯이 Access-Control-Allow-Origin 헤더가 존재하지 않기 때문에 CORS 오류가 발생함. 크롬과 AWS S3에서 캐시 데이터를 처리하는 과정에서 충돌이 발생해 발생하는 오류.
Chrome considers the cached response to be usable, apparently because the response didn't include a Vary: Origin header. But S3 does not return Vary: Origin when an object is requested without an Origin: request header, even when CORS is configured on the bucket. Vary: Origin is only sent when an Origin header is present in the request. That is exactly what I believe to be the case. Chrome and S3 are interacting in a way that breaks a CORS request that follows a non-CORS request for the same object. Arguably, both of them are wrong... but arguably, neither of them is wrong. #
HTTP 응답을 캐싱할 때, 캐싱된 데이터를 식별하기 위해 키(Key)를 URL로 설정하는 방법이 있음. 만약, 사용자가 처음에 ko-KR 언어로 사이트에 접속을 해서 이 데이터가 캐싱이 되었을 때, en-US로 언어 설정을 바꾼 후 다시 해당 사이트에 접속하게 되면 어떻게 될까? 현재로선 캐싱된 데이터를 식별하는 방법이 URL뿐이기 때문에 이전에 캐싱되었던 ko-KR 데이터가 화면에 표시되지만 그것은 바람직하지 않음. 이럴 때, Vary: Accept-Language 헤더를 사용하면 그러한 문제를 해결할 수 있음.
* 아래 이미지도 똑같이 캐시된 이미지인데 왜 Access-Control-Allow-Origin 헤더가 존재하나?
3. 해결
A. 처음으로 오류가 발생한 위치
오류가 발생한 위치를 찾기 위해 React-Query에서 error, isError, onError를 이용해봤지만 이상한 현상이 관찰됨. 캐시 이미지에서 CORS 오류가 뜬 경우, useQuery()에서 onError 콜백함수가 실행되는 것이 아니라 onSuccess 콜백함수에서 첫번째 parameter인 data가 undefined 인 상태로 실행이 됨을 확인할 수 있었음. 최초 네트워크 요청이 성공하고, 이후 캐시된 이미지를 요청하는 과정에서 발생한 오류이기 때문인 것으로 추정됨. 따라서, 아래와 같이 useQuery()에서 제공하는 isError, error, onError을 사용해서 에러를 처리할 수가 없음. onSuccess에서 처리할 수도 있지만 오류를 onSuccess에서 처리하는 것도 어색함.
해당 오류가 처음으로 발생한 위치는 axios.get()임을 확인할 수 있었음. 하지만, React-Query가 이 오류를 제대로 처리하지 못하기 때문에 queryFunction 자체에서 try, catch문을 사용하는 방식으로 해결해야 했음.
* 캐싱된 요청에서 CORS 오류가 발생할 때는 왜 React-Query에서 error로 처리되지 않는가?
B. 이미지 캐시 무효화하기
queryClient.invalidateQueries()를 이용해 쿼리 캐시를 무효화했으나 React-Query에서 곧바로 새로운 요청을 보내지 않았고, 페이지를 새로고침 하더라도 마찬가지였음. 따라서, Cache-Control 헤더를 no-cache로 설정한 새 요청을 직접 보내 브라우저에 저장되어 있던 기존 캐시를 무효화하는 방식으로 해결함.
// 이미지 캐시를 무효화하는 함수
async function invalidateImageCache(image: string, storeId: string) {
// 1. CORS 오류가 발생한 이미지를 no-cache 설정으로 재요청
const { data } = await axios.get<Blob>(image, {
responseType: "blob",
headers: {
"Cache-Control": "no-cache",
},
});
// 2. React-Query에서 캐시된 쿼리 무효화
queryClient.invalidateQueries(["place", "mainPicture", storeId]);
return data;
}
const { data } = useQuery(
["place", "mainPicture", storeId],
async () => {
let imageBlob: Blob;
try {
// AWS CORS 오류 발생 시 catch 문으로 넘어감
const { data } = await axios.get<Blob>(image, {
responseType: "blob",
});
imageBlob = data;
} catch (err) {
// 이미지 캐시 무효화
const data = await invalidateImageCache(image, storeId);
imageBlob = data;
}
return URL.createObjectURL(imageBlob);
},
{/* */ }
);
CORS 오류는 발생하지만 이후 새로운 요청을 보내 데이터를 정상적으로 받아옴CORS 오류가 발생한 이미지
CORS 오류가 발생하는 것을 해결할 수는 없지만, 오류가 발생하면 새로운 요청을 다시 보내 이미지를 정상적으로 받아옴을 확인할 수 있음.
CORS 오류가 발생했던 use-4d0e89 이미지도 정상적으로 디스크에 캐시가 됨
CORS 오류가 발생했던 이미지도 새로운 요청을 받아온 이후에는 캐시된 이미지를 아무 문제 없이 사용할 수 있음.
AWS S3 이미지 캐시 CORS 오류
1. AWS S3 이미지 네트워크 요청
AWS S3 이미지 링크를 <img>의 src 속성에 직접 임베드하지 않고 Fetch API 요청으로 blob 객체를 받아올 때, CORS 오류가 발생함. S3 서버에 이미 CORS 설정이 되어있는데다가, 모든 이미지에서 CORS 오류가 발생하는 것은 아니었기 때문에 디버깅에 어려움이 있었음. 검색을 해보니 HTTP 캐시와 관련된 문제임을 확인할 수 있었고, 오류도 아래와 같은 상황에서 재현할 수 있었음.
① 웹페이지에 들어가고 나서 시간이 흐른 후 다시 들어갔을 때, CORS 오류가 발생함.
② 개발자 도구에서 '캐시 사용 중지' 기능을 활성화 했을 때 CORS 오류가 발생하지 않음.
두 가지 현상을 미뤄보아 캐시로 인한 문제임을 확신하게 됨.
2. 원인
브라우저에서 이미지를 캐싱할 때, 이미지만 캐싱을 하는 것이 아니라 HTTP 응답 그 자체를 캐싱함. 아래와 같이 CORS 오류가 발생했음에도 최초 요청은 성공했기 때문에 응답 상태 코드가 200인 것에서도 그 점을 확인할 수 있음.
또한, 캐싱된 요청의 헤더에는 사진에서도 볼 수 있듯이 Access-Control-Allow-Origin 헤더가 존재하지 않기 때문에 CORS 오류가 발생함. 크롬과 AWS S3에서 캐시 데이터를 처리하는 과정에서 충돌이 발생해 발생하는 오류.
A. Vary 헤더 #
서버가 클라이언트에 보내는 HTTP 콘텐츠 협상 헤더로, Vary에 포함된 헤더가 달라질 경우 서버 응답도 달라질 수 있으므로 클라이언트에서 캐시된 응답을 사용하지 말라고 알려주는 역할을 함.
예를 들어, Vary에 Origin 헤더가 포함되어 있다면 서버는 클라이언트에 "Origin 헤더 값이 달라지면 우리가 보내는 응답도 달라질 수 있으니까 캐시된 응답을 사용하지 마!"라고 알려주는 것이라고 생각할 수 있음.
HTTP 응답을 캐싱할 때, 캐싱된 데이터를 식별하기 위해 키(Key)를 URL로 설정하는 방법이 있음. 만약, 사용자가 처음에 ko-KR 언어로 사이트에 접속을 해서 이 데이터가 캐싱이 되었을 때, en-US로 언어 설정을 바꾼 후 다시 해당 사이트에 접속하게 되면 어떻게 될까? 현재로선 캐싱된 데이터를 식별하는 방법이 URL뿐이기 때문에 이전에 캐싱되었던 ko-KR 데이터가 화면에 표시되지만 그것은 바람직하지 않음. 이럴 때, Vary: Accept-Language 헤더를 사용하면 그러한 문제를 해결할 수 있음.
* 아래 이미지도 똑같이 캐시된 이미지인데 왜 Access-Control-Allow-Origin 헤더가 존재하나?
3. 해결
A. 처음으로 오류가 발생한 위치
오류가 발생한 위치를 찾기 위해 React-Query에서 error, isError, onError를 이용해봤지만 이상한 현상이 관찰됨. 캐시 이미지에서 CORS 오류가 뜬 경우, useQuery()에서 onError 콜백함수가 실행되는 것이 아니라 onSuccess 콜백함수에서 첫번째 parameter인 data가 undefined 인 상태로 실행이 됨을 확인할 수 있었음. 최초 네트워크 요청이 성공하고, 이후 캐시된 이미지를 요청하는 과정에서 발생한 오류이기 때문인 것으로 추정됨. 따라서, 아래와 같이 useQuery()에서 제공하는 isError, error, onError을 사용해서 에러를 처리할 수가 없음. onSuccess에서 처리할 수도 있지만 오류를 onSuccess에서 처리하는 것도 어색함.
해당 오류가 처음으로 발생한 위치는 axios.get()임을 확인할 수 있었음. 하지만, React-Query가 이 오류를 제대로 처리하지 못하기 때문에 queryFunction 자체에서 try, catch문을 사용하는 방식으로 해결해야 했음.
* 캐싱된 요청에서 CORS 오류가 발생할 때는 왜 React-Query에서 error로 처리되지 않는가?
B. 이미지 캐시 무효화하기
queryClient.invalidateQueries()를 이용해 쿼리 캐시를 무효화했으나 React-Query에서 곧바로 새로운 요청을 보내지 않았고, 페이지를 새로고침 하더라도 마찬가지였음. 따라서, Cache-Control 헤더를 no-cache로 설정한 새 요청을 직접 보내 브라우저에 저장되어 있던 기존 캐시를 무효화하는 방식으로 해결함.
CORS 오류가 발생하는 것을 해결할 수는 없지만, 오류가 발생하면 새로운 요청을 다시 보내 이미지를 정상적으로 받아옴을 확인할 수 있음.
CORS 오류가 발생했던 이미지도 새로운 요청을 받아온 이후에는 캐시된 이미지를 아무 문제 없이 사용할 수 있음.
[참고]
Cross-origin requests AJAX requests to AWS S3 sometimes result in CORS error
웹브라우저의 이미지 캐싱 이슈 해결하기
Chrome S3 Cloudfront: No 'Access-Control-Allow-Origin' header on initial XHR request
Getting images from AWS blocked by CORS if "disable cache" in network tab is unchecked
CORS settings for images in canvas
'문제해결 일지' 카테고리의 다른 글