최근에 프로젝트를 진행하던 중에, 리소스 요청을 하는 Rest API Request를 날렸을 때 서버로부터 응답이 오는 것이 아니라 이전의 데이터를 반복해서 가져오는 문제가 발생하였습니다.
이를 해결하기 위해 열심히 구글링을 하여 원인을 찾아보았습니다.
문제 원인
문제의 원인은 인터넷 브라우저에서 get 함수를 호출했을 때 동일한 url이면 브라우저 자체에서 캐시 처리가 되어 실제 서버를 호출하지 않는 문제 때문이었습니다.
즉 새로운 요청을 서버로부터 받아와야 하는데 크롬 브라우저 자체의 캐시로 요청을 처리한 것입니다.
브라우저 캐시의 생명주기
위와 같은 이유가 일어나는 이유를 알기 위해서는 브라우저의 캐시 생명주기를 알아야합니다.
보통 웹브라우저가 서버에서 지금까지 요청한 적이 없는 리소스를 요청하게 되면,
서버와 브라우저는 완전한 HTTP 요청과 응답을 받게 됩니다.
하지만 그 이후의 요청은 HTTP 응답에 포함된 Cache-Control 헤더에 따라 받은 리소스의 생명주기가 결정되게 됩니다.
기왕 공부한 김에 Cache-Control 헤더에 대해서 꼭 알아두어야 할 옵션들만 알아보도록 하겠습니다.
Cache-Control 헤더의 max-age
Cache-Control 헤더의 값으로 max-age=초 를 설정하면 해당 리소스의 캐시가 유효한 시간은 지정한 초가 됩니다.
예를 들어 max-age=31536000이라고 지정을 하면 해당 리소스는 브라우저에서 1년(31,536000) 동안 캐시 할 수 있게 됩니다.
캐시의 유효시간이 남아있는 동안, 브라우저는 서버에 요청을 보내지 않고 디스크 또는 메모리에서 캐시를 읽어와 계속 사용하는데요, 이는 CDN Invalidation (CDN의 캐시 무효화) 등의 어떠한 작업이 있어도 캐시 유효시간 동안은 서버로 데이터를 요청하지 않고 메모리, 디스크에서 데이터를 가져온다는 것을 의미합니다.
Note:
Cache-Control max-age 값 대신 Expires 헤더로 캐시 만료 시간을 정확히 지정할 수도 있습니다.
캐시의 유효기간이 지나고 난 후 동작
앞서 설정한 캐시의 유효시간 이후에는 캐시가 삭제될 거 같지만, 바로 삭제되지는 않습니다.
대신 브라우저는 서버에 조건부 요청을 통해 캐시가 유효한지 재검증(reinvalidation)을 수행합니다.
재검증 결과 브라우저가 가지고 있는 캐시가 유효하다면 304 not modified 응답을 해주고, 해당 응답은 HTTP의 본문을 포함하지 않기 때문에 매우 빠르게 내려받을 수 있습니다. 위 요청은 살펴보면 캐시 검증을 위한 요청으로 단지 324바이트의 네트워크만을 송수신하는 것을 알 수 있습니다.
재검증 요청 헤더로는 IF-None-Math, If-Modified-Since 같은 헤더들이 있습니다. If-None-Match는 캐시 된 리소스의 ETag값과 현재 서버 리소스의 ETag 값을 비교하는 헤더이고, If-Modified-Since는 캐시 된 리소스의 Last-Modified값 이후에 서버의 리소스가 수정되었는지를 확인합니다.
ETag와 Last-Modified값은 기존에 받았던 리소스의 응답 헤더의 있는 값을 사용합니다.
해당 재검증 결과, 캐시가 유효하지 않다면 서버는 200 OK 또는 적합한 상태 코드를 본문과 함께 내려줍니다. 추가로 HTTP 요청을 보낼 필요 없이 최신 값을 바로 내려받을 수 있기 때문에 효과적인 방법이라고 할 수 있습니다.
max-age를 통한 캐시 검증 주의점
max-age는 캐시를 관리하기 위한 좋은 방법이지만, 제가 실제로 Cache-Control 헤더의 max-age=0을 설정하여 브라우저에서 캐시가 없이 항상 서버로 매번 Resource를 요청하게 하려고 설정을 했을 때는 문제가 있었습니다.
제가 의도한 것과 달리, 리소스를 서버의 요청이 없이 브라우저 자체 메모리, 혹은 디스크 캐시에서 데이터를 가져올 때가 있었기 때문입니다.
이유를 찾아보니, 정의대로 라면 max-age=0 값이 Cache-Control 헤더로 설정되었을 때 매번 리소스를 요청할 때마다 서버에 재검증 요청을 하여야 하지만, 일부 모바일 브라우저에서는 웹 브라우저를 껐다 켜기 전까지 리소스가 만료되지 않도록 하는 경우가 있다고 합니다. 이는 네트워크 요청을 아끼고 사용자에게 빠른 웹 경험을 제공하기 위함이라고 하는데요, max-age=0 값을 설정을 하더라도 서버에 요청을 보내지 않고 브라우저의 캐시에서 데이터를 가져오는 경우가 있다는 의미입니다.
저는 어떠한 경우에도 브라우저의 자체 캐시를 이용하지 않고 서버에서 데이터를 받아오도록 해야 했기 때문에 다른 방법을 찾아보다가, Cache-Control 헤더 중 no-store라는 값을 이용해 보게 되었습니다.
no-cache와 no-store
Cache Control 헤더 중 앞서 말씀드린 no-store이라는 값과, no-cache라는 값이 있습니다. 두 값은 이름은 비슷하지만 동작은 조금 다르기 때문에 차이점을 잘 알아두셔야 합니다.
no-cache 값은 대부분의 브라우저에서 max-age=0과 동일한 의미를 갖는데요, 즉 캐시는 저장하지만 사용하려고 할 때마다 서버에 재검증 요청을 보내야 합니다.
(가끔 no-cache를 설정하더라도 브라우저 캐시에서 데이터를 가져올 때가 있으므로 주의)
no-store 값은 캐시를 절대로 해서는 안 되는 리소스일 때 사용되며, 캐시를 만들어서 저장조차 하지 말라는 가장 강력한 Cache-Control 값입니다. no-store을 사용하면 브라우저는 어떠한 경우에도 캐시 저장소에 해당 리소스를 저장하지 않습니다.
문제 해결 방법
// 기존 요청
// const response = await axios.get(`https://example.api.io/projects/1234`)
// Cache를 제거한 요청
const response = await axios.get(`https://example.api.io/projects/1234`, {
// query URL without using browser cache
headers: {
'Cache-Control': 'no-store',
Pragma: 'no-store',
Expires: '0',
},
})
return response.data.result
브라우저 캐시에 대해 알아보고 문제를 해결하기 위해 axios의 api 요청의 header에 cache-control의 no-store 옵션을 붙여서 문제를 해결하였습니다.
이후 브라우저로부터 기존 리소스를 가져오지 않고 항상 서버로부터 resource를 응답받는 것을 확인할 수 있었습니다.
마무리
프로젝트를 진행하며 위와 같은 문제를 겪어보니, 리소스에 대한 캐시에 대한 이론도 프론트엔드를 개발하는 데에 중요하다는 것을 알게 되었습니다.
앞으로도 CDN 캐시, 브라우저 캐시, 서버 캐시 등 어떻게 데이터가 캐시 되고 저장되는지에 대하 한 번쯤 생각해보면서 개발을 진행해야 될 것 같다고 생각이 들었습니다. :)
참고
'웹 프론트 > HTML | CSS | JS' 카테고리의 다른 글
CSS : 말풍선 만들기 (border 속성) (0) | 2022.12.20 |
---|---|
모바일 웹 : Device Orientation Event란? ( feat: IOS 13+ 이상에서 Device Orientation Event 권한 얻어오기 ) (1) | 2022.10.12 |
JS: chrome 브라우저에서 blob 최대 용량 구하기 (0) | 2022.03.18 |
JS : 모바일 웹앱에서 video 태그 자동재생 구현하기 ( + 추가적으로 IOS safari에서 자동 재생 시 저전력 모드일때 멈추는 문제 예외처리) (6) | 2022.01.29 |
JS : 모바일에서 오른쪽 클릭 시 옵션메뉴 안나오게 하기 (0) | 2021.07.21 |