콘솔상으로 데이터는 업데이트가 되고 있다. 그러나 요청은 날리지 않는다! 이것이 바로 캐싱!
사실 나는 이제까지 리엑트 쿼리를 내가 잘 이해하고 있었다고 생각했고, 이번에 기업과제 제작을 하면서 팀원들이 react-query를 사용해보자는 요청에 혼쾌하게 응하였다. 그러나 이내 내가 캐싱에 대해서 크게 착각하고 있었다는 사실을 깨닫게 되었다.
공식문서상 React-Query의 캐싱개념은 stale과 cachetime을 통해 이루어진다
useQuery라고 하는 훅을 이용해 호출할 시 전달되는 옵션으로 staletime과 cachetime을 보낼 수 있는데 이 둘을 잘 구별할 줄 알아야 캐싱에 대해 이해할 수 있다.
-staletime : 전달받은 데이터는 리엑트 쿼리의 자료구조 내용 중 캐시에 저장이 되는데, 이때 이 캐시데이터의 "신선한 상태" 가 언제까지 될지를 말해주는 옵션이다. default는 0으로, 받아오는 즉시 stale하다고 판단하며 캐싱 데이터와 무관하게 계속해서 feching을 진행한다.
신선하다 함은, 서버에서 조회한 데이터는 그때 당시의 데이터 snapshot이고, 외부 요청으로 서버 데이터가 변경이 되었다면 내 브라우저가 가진 데이터는 이미 오래된 낡은 데이터가 되었으므로 stale하다고 말하는 것이다.
-cachetime : 캐시 구조에 저장된 데이터는 메모리상에 존재하게 된다. 이 때, 메모리에 저장되어 있는 캐시 데이터가 언제까지 유지될지를 말해주는 옵션이다. 즉, 캐싱된 쿼리의 결과값은 계속 유지되는 것이 아니라 시간이 지나면 메모리에서 사라진다. 쉽게 말해 contextAPI에 빗대자면 Provider의 value 내부에 저장된 객체내부 구조에서 존재했다가 사라지는 것과 비슷한 현상이다. default는 5분이다.
위에 언급된 캐싱과 관련된 내용은 React-Query에서 제공해주는 메서드, useQueryClient를 통해 조회를 할 수 있다.
참고로, 해당 데이터는 마치 React의 Context API와 같이 prop을 자식들에게 전달하는 구조처럼 생겼다고 이해하면 된다.
자식 내에서 useQueryClient를 호출하면 위에서 생성자 함수, QueryClient를 통해 만들어진 객체의 정보를 얻을 수 있다.
이때, 이 내용 안에 캐싱과 관련된 내용들 역시 useQueryClient를 호출하면 자식들 내부에서 확인이 가능하다. Context API와 매우 흡사한 사용감을 보여준다
위의 사진에서 볼 수 있듯, QueriesMap이라는 프로퍼티로 브라우저에서 요청했던 ajax 요청결과를 캐싱하여 저장하고 있음을 알 수 있다.
이 순간의 키값은 useQuery를 통해 호출할 당시 첫번째 인자로 전달한 값으로,
(string 전달 시 자동 배열로 감싸서 전달)
이 내부 키값에 대해서 동일한 키를 가진 캐싱값이 존재할 경우, fetching을 진행하지 않고 이 캐싱값을 그대로 다시 사용한다.
단, 여기서 주의할 점이 있다.
만약 해당 useQuery를 호출할 당시에 옵션으로 staletime을 따로 지정해주지 않았었다면, 항상 캐싱되어 있는 데이터는 stale하다고 여기기 때문에 refetching을 하게 되어 서버에 계속적인 요청을 하게된다.
즉, staletime을 지정해주지 않고 쓴다면 react-query의 캐싱 기능을 제대로 활용할 수가 없다.
따라서 사용은 하긴 해야하나, 만약 데이터 구조가 자주자주 변하는 어플리케이션이라면 지정하지 않는 편이 좋고 해당 브라우저에 표현되는 내용의 데이터들이 정적이라면 staletime을 지정해주고 요청하는 것이 서버의 부담을 경감시키는 것이 될 것이다.
그리고 또 하나 더 기억해야 할 사항이 있다.
옵션에 존재하는 enabled:false 를 설정해놓을 경우, 초기 마운트시에 해당 useQuery가 마치 useEffect처럼 첫 마운트시 fetcher 함수 호출을 하고, 실패했을 때는 계속 retry를 하는 행위를 사전 차단할 수 있다.
그러나, enabled:false는 말 그대로 이 useQuery의 기능을 사용하지 않겠다고 말해주는 것과 같기 때문에 수동적으로 호출하는 방식이 필요하다. 그것이 바로 useQuery 함수 호출을 하여 리턴되는 객체에 포함되어 있는 refetch 함수다.
여기서 정말 중요한 것이 refetch 함수는 "캐싱 결과는 조회하지 않고 완전히 무시한 채로 그냥 ajax 요청을 날리는 메서드라는 점이다
위에서 볼 수 있듯, 아무리 QueryClient 객체에 저장되어 있는 캐싱내용에 해당 요청값의 키가 존재한다 하더라도, 이것을 무시하고 그냥 re-fetcing 요청을 진행해버린다.
따라서 결론적으로 말하자면 캐싱을 구현하려고 한다면 enabled 옵션을 false로 두면 안된다.
그러면, true인 상태와 캐싱을 둘 다 구현하려면 어떻게 해야할까.
방법은 매우 간단하다.
우선 enabled 옵션에 대해서는 특정한 상태를 충족할 때만 true로 만들고, 그 외에는 false로 하여 초기 요청을 통한 retry로 오류를 생성하는 것을 막는다. (refetch 메서드로 강제호출을 하지 않는다)
그 뒤에, 조건부로 enabled가 true로 변경되면서 요청을 날리게 되어 성공하면 data 프로퍼티에 그 값이 저장되고, 캐싱에도 저장이 될 것이다. 그렇다면 그 뒤에는 이 data를 가지고 UI를 그려내는 작업을 진행하면 된다.
마지막 추가사항으로,
useQuery를 호출할 때, 세번째 인자 객체는 그 내부에서 비동기 데이터 처리의 결과에 대해서 특정 처리를 할 수 있는 콜백함수를 담을 프로퍼티를 담는 옵션을 제공한다.
아까 위에서 언급했던 대로, 이미 useQueryClient 로 조회하는 구독값 내부의 query caching 데이터 내부에 지금 실행하려는 쿼리의 실행에 대한 키값이 존재할 경우, 해당 캐싱값을 사용하고 요청을 보내지 않는다고 하였다.
이말인 즉슨, 위에서 언급했던 콜백함수들은 캐싱값을 그대로 사용할 경우 호출되지 않는다는 점이다. (밑줄백개)
따라서, 캐싱값을 사용하게 될 때에는 해당 data의 결과값은 변동된다는 사실을 이용하여 이 데이터를 이용한 조건부 랜더링을 실행하도록 만들어야 한다!
React-Query를 이용하면 아주 손쉬운 캐싱을 구현할 수 있다. 하지만, 제대로 주어지는 메서드들이 어떤 작용을 하는지 파악하지 않았을 경우, 기대했던 결과를 만들어주지 않는다는 사실을 잘 알게 되었고 항상 공식문서를 곁에 두고, 또한 직접 실험을 통해 결과를 확인하고 사용하는 것이 옳다는 점을 배우게 되었다.