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 사용자 모듈 만들기 황제낙엽 2019.07.09 41735
236 User Agent 정보 모음 file 황제낙엽 2011.02.22 7768
235 페이지 스크롤 끝 확인 황제낙엽 2011.10.24 6230
234 숫자 여부와 자리수를 체크 하는 예제 황제낙엽 2009.01.12 5265
233 User Agent Parser들 황제낙엽 2017.11.20 4132
232 ActiveX 설치 여부를 검사하는 스크립트 황제낙엽 2011.02.13 4053
231 [JavaScript Tutorials] Handling runtime errors in JavaScript using try/catch/finally (해석중) 황제낙엽 2009.04.08 2784
230 브라우저의 새로고침과 종료에 대한 이벤트 황제낙엽 2017.08.11 2725
229 연속해서 스트림 받기 (flush data from servlet to jsp with ajax) 황제낙엽 2013.01.04 2428
228 오류:호출자(서버 응용 프로그램이 아닌 서버)가 사용될 수 없어서 사라졌습니다. file 황제낙엽 2012.03.14 1949
227 외부 라이브러리 (.js) 의 바람직하지 않은 동적 로딩 (eval함수 이용) 황제낙엽 2012.01.18 1851
226 window.postMessage 이해하기 file 황제낙엽 2017.10.16 1612
225 부동소수점 (floating-point) file 황제낙엽 2018.03.26 1122
224 javascirpt IME-Mode 설정하기 황제낙엽 2010.08.17 1112
223 경과 시간 구하기 황제낙엽 2019.10.04 1071
222 CORS(Cross-Origin Resource Sharing) - 4 file 황제낙엽 2017.03.07 873
221 각 브라우저 별 User Agent 정보 황제낙엽 2011.02.22 823
220 중첩 함수, 함수 클로저 황제낙엽 2008.08.12 820
219 자바스크립트의 쉬프트 연산자 (Shift Operator) 와 음수 (Negative) 이야기 황제낙엽 2012.05.31 726
218 Memory leak 및 성능 측정 도구 file 황제낙엽 2011.11.23 666