sitelink1 https://blog.naver.com/jukrang/222403268969 
sitelink2  
sitelink3  
sitelink4  
extra_vars4  
extra_vars5  
extra_vars6  

무슨 상황?

고객에게 받은 재현 영상(보안상 공개 불가)을 분석해보니 상황은 이랬다.

1) 메인 페이지에 진입을 한다.

2) 더 보기를 몇 번 누른 후에 상세 보기 버튼을 클릭해서 모달을 열면,

3) CORS 에러가 발생하며,

4) 페이지가 새로고침 된다.

고객이 Edge 브라우저를 사용하고 있기에 크로스 브라우징 이슈를 의심했지만, 나는 재현할 수 없었다. 삽질이 길어질까 팀에 상황을 공유하고 재현할 수 있는 사람을 찾았다. 다행히 한 동료의 테스트 계정으로 재현을 할 수 있었다.

재현을 하면서 네트워크 탭의 변화를 확인했다. 아래와 같이 HTTP 요청이 두 번, 시간차를 두고 발생하고 있었다. 그 중에 두 번째 요청이 실패한다.

122139069-0e3f5000-ce83-11eb-9a3b-1d5a03c825f6.png

 

둘 다 같은 자원을 요청하는데 요청하는 시점과 요청의 출처가 다르다.

1. 첫 번째 요청(stylesheet): 페이지를 최초 로딩할 때 CSS 청크를 link 태그로 요청

2. 두 번째 요청(fetch): 런타임에 동일한 CSS 청크를 한 번 더 fetch로 요청

재미있는 점은 2번 요청은 특정 상황에서만 발생한다는 사실. 내 테스트 환경에서는 재현을 할 수가 없었다. Next.js가 CSS 자원을 청크로 빌드하고 특정 상황일 때만 자원을 서버로 요청하는데, 동작 규칙을 아직 정확하게 파악하지 못했다. 다만 런타임에 Next.js가 동일한 CSS 청크를 한 번 더 불러오는 상황이 있고, 이때 fetch 함수를 이용한다는 사실만 확인했다.

상황을 정리해보면,

1) 브라우저는 1번 요청의 응답을 받아서 아래의 Response Header를 갖는 응답 캐시를 저장한다.

age: 370 cache-control: max-age=31536000 content-encoding: gzip content-type: text/css date: Tue, 15 Jun 2021 08:26:53 GMT etag: W/"c4c14f4ef32118ba7dd1c402ed4b3c13" last-modified: Tue, 15 Jun 2021 08:19:19 GMT server: AmazonS3 vary: Accept-Encoding via: 1.1 b4d3f424b1e6960b9f71e8cf3b9e1a57.cloudfront.net (CloudFront) x-amz-cf-id: UHT0r3ylQvy5gfdrdkeRGKr9psKP2gwBWw_NhuSUZo5jXFgtgHN8kA== x-amz-cf-pop: ICN51-C1 x-cache: Hit from cloudfront

2) 클라이언트가 fetch로 2번 요청을 보낼 때 브라우저는 1번의 응답 캐시를 가지고 요청을 처리해버린다. fetch 함수를 이용하기 때문에 이 과정은 CORS 제약을 받는다.

3) 브라우저가 캐시한 1번 응답의 헤더는 CORS 관련 속성(Access-Control-Allow-*)을 갖고 있지 않기 때문에 브라우저는 2번 요청을 CORS 정책 위반으로 판단한다.

아, 그리고 이 현상은 (당연히) 브라우저 캐시를 사용할 때만 발생했다.

다른 요청인데 왜 캐시가 동작하죠?

구글링을 하다가 Server Fault에서 비슷한 상황을 질문하는 포스트를 발견했다. 포스트에 달린 답글에 설명이 매우 잘 되어 있다. 각박해져 가지만, 여전히 세상에는 좋은 사람이 많다.

참고로 우리 제품은 AWS 인프라에 호스팅되어 있다. 정적 자원(JS, CSS)을 S3에 올려놓고 CloudFront로 라우팅한다. 클라이언트의 정적 자원 접근 요청은 CloudFront → S3로 흘러들어 간다.

Server Fault의 논의를 읽고 내가 이해한 문제가 발생하는 과정은 이렇다.

1. 페이지를 렌더링 하는 시점에 브라우저는 link 태그를 처리하며 요청 헤더에 Origin이 없는 non-cors 요청을 서버로 보낸다.

2. 이 요청은 CloudFront에서 S3로 전송되는데, S3는 요청 헤더에 Origin이 없기에 응답 헤더에 Vary: Origin을 설정하지 않는다.

3. CloudFront도 응답 헤더에 Vary: Origin을 넣지 않는다.(Whitelist Header에 Origin을 설정해도 마찬가지)

5. 클라이언트는 Vary: Origin 헤더가 없는 응답을 받아서 캐시 한다.

6. 두 번째 fetch 요청을 브라우저는 캐시 히트로 판단하고 저장해 둔 non-cors 응답을 꺼내서 반환한다.

7. fetch는 cors 요청이지만 브라우저가 캐시에서 꺼낸 응답은 non-cors이므로 CORS를 위반한다.

Vary 응답 헤더?

Vary 헤더에 설정한 속성이, HTTP Request마다 변할 수 있다는 것을 표현한다. 브라우저는 요청의 유일성을 식별할 때 이 헤더 속성을 참조한다. 서버가 브라우저에게 캐시를 검토할 때 기준으로 삼을 값을 이 헤더를 이용해 알려줄 수 있다.

예) 서버가 Vary: user-agent를 응답 헤더로 보내면, 브라우저는 동일한 요청을 보내기 전에 user-agent를 확인해서 캐시 된 정보를 반환할지, 서버로 요청을 전달할지 판단함.

Fetch Standard(https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches)에는 서버가 "Access-Control-Allow-Origin" 헤더에 와일드 카드(*)나 고정 Origin이 아닌 값을 설정한다면 응답 헤더에 "Vary: Origin" 을 명시해야 한다고 정의하고 있다. Origin에 따라 응답 결과가 다를 수 있기 때문이다.

크로미움 측은 이를 버그가 아닌 정상 동작이라고 판단(won’t fix)하고 있다.

내 생각에도 크로미움은 죄가 없다. 응답에 Vary: Origin이 없으니 나머지 정보만 보고 캐시 히트로 처리하는 게 맞잖아? 앗, 그러고보니 이 상황을 Fetch Standard도 언급하고 있다.

In particular, consider what happens if `Vary` is not used and a server is configured to send `Access-Control-Allow-Origin` for a certain resource only in response to a CORS request. When a user agent receives a response to a non-CORS request for that resource (for example, as the result of a navigation request), the response will lack `Access-Control-Allow-Origin` and the user agent will cache that response. Then, if the user agent subsequently encounters a CORS request for the resource, it will use that cached response from the previous non-CORS request, without `Access-Control-Allow-Origin`.

https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches

S3나 CloudFront가 잘못 처리하고 있다고 보기에도 애매하다. 나도 죄가 없다. 모르는 게 죄는 아니지 않은가! Next.js가 왜 불필요하게 이미 로딩한 CSS Chunk를 요청하는지 궁금하지만 고객이 고통 받고 있으니 탐구는 잠시 미루고 문제부터 빨리 해결하기로 했다.

삽질은 길고 해결은 짧다.

브라우저가 첫 번째와 두 번째 요청을 동일한 요청으로 취급하여 캐시를 적용한다면, 첫 번째 요청의 응답 결과에 CORS 응답 헤더가 있으면 이 문제는 자연스레 해결이 된다. 두 번째 요청을 처리할 때 브라우저가 CORS 헤더가 있는 첫 번째 응답을 참조할 테니까.

link 태그에 crossorigin 속성을 추가하면 브라우저는 CSS 자원을 요청할 때 헤더에 Origin 프로퍼티를 담아서 서버로 전송한다. 그러고보니 정적 자원 도메인과 애플리케이션의 도메인이 서로 다른데 이 설정을 아직까지 안 하고 있었네?

우리 제품은 script와 css 청크를 구성하는 책임을 Next.js에게 맡기고 있다. Next.js가 제공하는 옵션을 이용해서 쉽게 이 문제를 풀 수 있었다. Next.js는 8 버전부터 crossOrigin 설정을 제공한다.

// next.config.js module.exports = { crossOrigin: 'anonymous' }

crossOrigin 설정을 적용하고 빌드를 하면 Next Router가 페이지를 빌드 할 때 script와 link 태그에 crossorigin 속성을 추가한다. 이렇게.

Untitled_(1).png

 

그렇게 문제가 사라졌다!

Untitled_(2).png

 

네트워크 탭에서 첫 번째 요청의 응답 헤더에 CORS 관련 헤더(access-control-allow-...)가 생긴 걸 볼 수 있다.

access-control-allow-methods: GET access-control-allow-origin: * age: 722 cache-control: max-age=31536000 content-encoding: gzip content-type: text/css date: Wed, 16 Jun 2021 02:24:38 GMT etag: W/"c4c14f4ef32118ba7dd1c402ed4b3c13" last-modified: Wed, 16 Jun 2021 01:47:55 GMT server: AmazonS3 vary: Accept-Encoding,Origin via: 1.1 417550d6bf90c93959de0cfa030c8ae5.cloudfront.net (CloudFront) x-amz-cf-id: tTiq4kXQEGZXNSLnX3aWoNQtkK72fktzRAoJnuIEf3QHgUWz4lOrFA== x-amz-cf-pop: ICN51-C1 x-cache: Hit from cloudfront


해법은 매우 간단하지만 이런 문제는 여러 도구가 상호작용하며 문제를 만들기 때문에 원인을 찾기가 어렵다. Server Fault에서 답을 찾지 못했다면 며칠 동안 이 문제를 잡고 있었을지도? 내가 Server Fault에서 도움을 얻었듯, 누군가에게 이 글이 도움이 되기를.

 

번호 제목 글쓴이 날짜 조회 수
237 json 데이터 내의 변수명에 prefix 로 type 표현하기 황제낙엽 2024.04.15 0
236 fetch() 함수 사용 예제 file 황제낙엽 2023.11.23 1
235 현재 document 의 host 와 port 를 얻는 방법 황제낙엽 2023.10.03 1
234 (Bard) FileReader 로 여러개의 파일을 read 하는 법 file 황제낙엽 2023.08.23 0
233 How to build a file upload service with vanilla JavaScript file 황제낙엽 2023.08.22 0
232 (Bard) JavaScript로 JSON 배열을 작성하는 방법 황제낙엽 2023.08.21 0
231 모바일 브라우저에서 file input element 를 이용하여 여러장의 이미지를 서버에 전송하려 할때 황제낙엽 2023.08.21 0
230 navigator.mediaDevices 황제낙엽 2023.08.21 1
229 Barcode Detection API 황제낙엽 2023.08.06 6
228 정규식을 이용한 이메일 검증 스크립트 file 황제낙엽 2023.06.25 0
227 체크박스에 체크된 항목 개수 구하기 황제낙엽 2023.06.10 1
226 JSON 클래스가 지원하는 function 황제낙엽 2023.03.31 1
225 (Copilot) JSON 객체의 내부 데이터 리스트 길이를 구하는 방법 황제낙엽 2023.03.30 2
224 배열에 대한 루프문 조회 (loop iterator) 황제낙엽 2023.03.01 3
223 (Copilot) 바닐라 스크립트가 뭐지? 황제낙엽 2023.02.24 7
222 CryptoJS 를 이용한 암호화 황제낙엽 2023.02.15 1
221 [URLSearchParams] URL 파라미터(매개변수) 값 가져오기 file 황제낙엽 2023.02.02 0
220 Fetch API (CORS 극복을 위한 노력) 황제낙엽 2021.12.05 26
» 두 서버의 자원을 접근하는 클라이언트 프레임웍(Next.js)에서의 CORS오류 file 황제낙엽 2021.12.05 228
218 CORS 의 내용과 이에 대한 우회 방안들 file 황제낙엽 2021.12.05 139