본문으로 바로가기

전제 지식들

 

staleTime : 마운트 되어 있는 시점에서 데이터가 구식인지 판단함.

cacheTime : 언마운트된 후 어느 시점까지 메모리에 데이터를 저장하여 캐싱할 것인지를 결정함.

 

 

refetching의 조건

  • New instances of the query mount
    • 작동 되는 경우 : query key에 react state를 주고, state가 바뀌면 다시 fetch
  • The window is refocused
    • 작동 되는 경우 : 유저가 다른 작업하다가 다시 typed에 돌아오면 다시 fetch
  • The network is reconnected.
    • 작동 되는 경우 : 네트워크가 끊겼다가 다시 연결되면 fetch됨
  • The query is optionally configured with a refetch interval.
    • refetch 설정을 명시적으로 준 경우.
  • 추가적으로, 고의적으로 invalidate하여 refetching하는 것이 실전에서 자주 사용되는 편임. CUD가 이루어진 직후 새로운 데이터를 받아오기 위해 invalidate를 함. => 즉각 stale이 되어 refetching될 수 있음.

 

react-query에서의 cache의 life cycle

https://react-query.tanstack.com/guides/caching#basic-example

 

 

몇가지 질문과 답들

 

refetching을 왜 하는가? => 지금 가지고 있는 정보가 fresh하지 않고 stale하니까

refetching 조건을 만족해도 fresh하다면 refetching하지 않는가? => 그렇다.

staleTime이 0이라면 어떻게 되는가? => 곧바로 stale이 되므로 refetching이 요구되는 상황이 오면 무조건 refetching하게 된다.

 

언제 데이터가 cache되는가? => query를 보내고 데이터를 받아오자마자 cache된다. 그러나 cacheTime은 이 때 발동하지 않는다.

그렇다면 cacheTime은 언제부터 시작인가? => unmount된 시점부터. 즉, inactive된 시점부터 시작한다.

cacheTime이 지나기 전에 다시 쿼리가 발동되면 어떻게 되는가? => cache된 값을 사용하고 background에서 다시 fetching된다.

cacheTime이 지나면 어떻게 되는가? => 메모리에 존재하는 데이터가 GC에 의해 삭제. 따라서 다시 active되면 hard loading한다.

cacheTime이 0이라면 어떻게 되는가? => 매번 GC 당하므로 매번 hard loading을 하게 된다.

 

 

그 외의 지식들

 

only query once

같은 키를 사용하는 쿼리가 여러개라도 요청은 단 한 번만 간다. 키를 기반으로 요청을 관리하기 때문이다.

 

query 결과값에 대한 레퍼런스 유지

쿼리 결과는 기본적으로 공유되어 데이터가 실제로 변경되었는지 감지하고, 변경되지 않은 경우 데이터 참조가 변경되지 않는다고 함.

쉽게 말해서 {name: ‘darren’} 이란 json을 받은 후, 다시 refetching 되었는데 결과값이 같은 {name: ‘darren’}을 반환하였다면, reference type임에도 불구하고 같은 참조값을 가진 결과물을 반환한다는 것임. 같은 값을 가진 객체를 메모리에 다시 할당하지 않으므로 메모리상 이점이 있을 것으로 판단됨.

 

idle?

useQuery가 반환하는 상태값중 idle이란 녀석이 존재함. enabled 플래그가 false이고, initialData도 존재하지 않을때 idle 상태이다.

보통 dependent quries, 즉, 다른 쿼리의 결과물을 받아 다시 query를 날려야할 때 의존하는 값을 enable에 설정하는 경우가 많은데, 이 때 idle 상태가 보인다.

 

initialData를 설정하는 경우에는 initialStale을 줄지 고민해봐야 한다.

initialData가 주어지면 보통 해당 데이터는 stale이 아니라 한 번 fetching이 된 것으로 친다.

즉, staleTime이 지나기 전까지는 fresh한 데이터로 간주한다는 것이다. 이를 막고 싶다면 initialStale을 설정하자

 

 

If your query function depends on a variable, include it in your query key

function Todos({ todoId }) {
  const result = useQuery(['todos', todoId], () => fetchTodoById(todoId))
}

 

queryCache를 직접 사용하는 것은 그만. queryClient로

 

과거에는 직접적으로 queryCache를 이용하여 특정 키의 쿼리를 통해 요청된 캐시값을 가져오곤했다.

But Normally, you will not interact with the QueryCache directly and instead use the QueryClient for a specific cache.

 const query = queryCache.find('posts')

 

그러나 일반적으로 cache에 저장되어 있는 데이터들은 직접 가져오기보다 queryClient를 기반으로 가져오고, 심지어 저장한다.

const data = queryClient.getQueryData(queryKey)

queryClient.setQueryData(queryKey, updater)

 

 

react-query를 활용한 여러 tricks

 

cache Seeding

 

- pull 형태의 seeding

queryClient를 활용하여 캐싱된 key의 데이터를 가져왔다고 해보자. 이 데이터는 다른 query의 initialData로 설정한다면, fetching을 기다리는 시간 전에도 data를 보여줄 수 있다.

 

- push 형태의 seeding

queryClient를 활용하여 특정 key로 데이터를 캐싱한 다음, 해당 키로 Query를 날려서 이미 있는 캐싱 값을 사용할 수도 있다. 

 

어느 형태의 cache seeding을 사용하던간에, key는 팀 간에 엄격한 규칙을 지켜서 관리해야 한다.

 

scroll restoration

 

쉽게 말해서 특정 스크롤의 위치에 있다가 => 다른 경로로 이동한 다음 => 뒤로가기를 눌러 => 다시 원 페이지로 돌아왔을 때 스크롤의 위치를 복구하는 것이다.

 

흔히 infinityScroll을 구현했다고 하면, fetching 까지만 신경쓰고, 다시 돌아올 경우에 다시 제자리로 돌아가는 로직을 신경쓰지 않는 경우가 대부분이다. 그러나 feed 형태의 서비스를 개발한다면, 다시 제위치로 돌아오는 것은 매우 중요한 안건이다.

이 경우 일반적으로 history.scrollRestoration을 사용하지만, react-query를 사용하게 된다면 해당 feed가 cache되어 있다는 전제 하에 해당 scroll로 돌아가게 된다. 물론 cacheTime이 짧아 뒤로가기를 하는 순간 참고할 cache가 없다면, restoration은 되지 않는다.

 

polling

 

일반적으로 폴링을 구현하는 많은 방법이 있지만 가장 간단한 형태는 setInterval 내에 요청 콜백을 담는 것이다.

react-query를 사용하자면 (물론 굳이 사용할 필요는 없다) refetchInterval을 지정하여 간단하게 폴링을 구현할 수 있고, 좀 더 깔끔하게 refetchIntervalInBackground을 지정하여 focus된 상황이 아니라도 폴링을 지속하게 할 수 있다. 

 

번외

 

setQueryData, getQueryData를 이용해 global state management로써 사용하기

 

하면 된다.

 

(rtk 기준) reducer 내부에 react-query의 setQueryData 등을 사용할 때

 

reducer내부에서 가져온 state는 proxy로 한번 wrapping 되어있다. 그래서 setQueryData 할때 모델로 변환을 안해주면 Proxy가 그대로 query data에 들어가더라구요.. 나중에 다른곳에서 getQueryData 해서 가져올때 revoke된 proxy가 튀어나와서 우리가 원하는 데이터가 없는 경우가 생긴다. 안티패턴이지만! 만약에 slice 내부에서 setQueryData를 할 일이 생긴다면 모델 인스턴스로 변환해서 넣어줘야한다.

 

같은 레퍼런스를 반환하기 위한 작업들

 

같은 레퍼런스 값을 반환하기 위해 recoil에서는 stringify를 사용한다.  아마 react-query에서도 비슷하지 않을까 싶다.

다른 곳에서는 JSON.stringify, JSON.parse로 땜방하는 곳이 많다.

여튼 이 기술에 대해서는 나중에 직렬화와 함께 같이 다뤄보려고 한다.

function stringify(x, opt, key) {
  // A optimization to avoid the more expensive JSON.stringify() for simple strings
  // This may lose protection for u2028 and u2029, though.
  if (typeof x === 'string' && !x.includes('"') && !x.includes('\\')) {
    return `"${x}"`;
  } // Handle primitive types


  switch (typeof x) {
    case 'undefined':
      return '';
    // JSON.stringify(undefined) returns undefined, but we always want to return a string

    case 'boolean':
      return x ? 'true' : 'false';

    case 'number':
    case 'symbol':
      // case 'bigint': // BigInt is not supported in www
      return String(x);

    case 'string':
      // Add surrounding quotes and escape internal quotes
      return JSON.stringify(x);

    case 'function':
      if ((opt === null || opt === void 0 ? void 0 : opt.allowFunctions) !== true) {
        throw new Error('Attempt to serialize function in a Recoil cache key');
      }

      return `__FUNCTION(${x.name})__`;
  }

  if (x === null) {
    return 'null';
  } // Fallback case for unknown types


  if (typeof x !== 'object') {
    var _JSON$stringify;

    return (_JSON$stringify = JSON.stringify(x)) !== null && _JSON$stringify !== void 0 ? _JSON$stringify : '';
  } // Deal with all promises as equivalent for now.


  if (Recoil_isPromise(x)) {
    return '__PROMISE__';
  } // Arrays handle recursive stringification


  if (Array.isArray(x)) {
    return `[${x.map((v, i) => stringify(v, opt, i.toString()))}]`;
  } // If an object defines a toJSON() method, then use that to override the
  // serialization.  This matches the behavior of JSON.stringify().
  // Pass the key for compatibility.
  // Immutable.js collections define this method to allow us to serialize them.


  if (typeof x.toJSON === 'function') {
    // flowlint-next-line unclear-type: off
    return stringify(x.toJSON(key), opt, key);
  } // For built-in Maps, sort the keys in a stable order instead of the
  // default insertion order.  Support non-string keys.


  if (x instanceof Map) {
    const obj = {};

    for (const [k, v] of x) {
      // Stringify will escape any nested quotes
      obj[typeof k === 'string' ? k : stringify(k, opt)] = v;
    }

    return stringify(obj, opt, key);
  } // For built-in Sets, sort the keys in a stable order instead of the
  // default insertion order.


  if (x instanceof Set) {
    return stringify(Array.from(x).sort((a, b) => stringify(a, opt).localeCompare(stringify(b, opt))), opt, key);
  } // Anything else that is iterable serialize as an Array.


  if (Symbol !== undefined && x[Symbol.iterator] != null && typeof x[Symbol.iterator] === 'function') {
    // flowlint-next-line unclear-type: off
    return stringify(Array.from(x), opt, key);
  } // For all other Objects, sort the keys in a stable order.


  return `{${Object.keys(x).filter(key => x[key] !== undefined).sort() // stringify the key to add quotes and escape any nested slashes or quotes.
  .map(key => `${stringify(key, opt)}:${stringify(x[key], opt, key)}`).join(',')}}`;
} // Utility similar to JSON.stringify() except:
// * Serialize built-in Sets as an Array
// * Serialize built-in Maps as an Object.  Supports non-string keys.
// * Serialize other iterables as arrays
// * Sort the keys of Objects and Maps to have a stable order based on string conversion.
//    This overrides their default insertion order.
// * Still uses toJSON() of any object to override serialization
// * Support Symbols (though don't guarantee uniqueness)
// * We could support BigInt, but Flow doesn't seem to like it.
// See Recoil_stableStringify-test.js for examples

 

query cancellation

 

https://react-query.tanstack.com/guides/query-cancellation#_top

 

요청이 너무 오랜 시간이 걸리거나, debounce가 필요할만큼 잦은 요청이 간다면, cancellation을 고려함직하다.

queryClient.cancelQueries는 주어진 key의 query 내에 promise.cancel을 사용할 수 있는 경우 발동하여 요청을 취소한다.

 

axios를 기반으로 살펴보자면 다음과 같다.

 import axios from 'axios'
 
 const query = useQuery('todos', () => {
   // Create a new CancelToken source for this request
   const CancelToken = axios.CancelToken
   const source = CancelToken.source()
 
   const promise = axios.get('/todos', {
     // Pass the source token to your request
     cancelToken: source.token,
   })
 
   // Cancel the request if React Query calls the `promise.cancel` method
   promise.cancel = () => {
     source.cancel('Query was cancelled by React Query')
   }
 
   return promise
 })
 const queryClient = useQueryClient();
 
 return (
   <button onClick={(e) => {
     e.preventDefault();
     queryClient.cancelQueries(queryKey);
    }}>Cancel</button>
 )

 


댓글을 달아 주세요

  1. 허흥 2022.02.27 13:10

    관리자의 승인을 기다리고 있는 댓글입니다

  2. 지우개발자 2022.05.01 20:15

    관리자의 승인을 기다리고 있는 댓글입니다

darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘39 / 전체634,781