React Query Caching 구현하기

이수빈·2023년 2월 21일
1
post-thumbnail

Caching을 제대로 이해하려면?

  • React Query를 이용해 Caching 기능을 구현 할 수 있다. 캐싱이란, API 호출결과를 저장해 필요할때 이를 재사용하는 기법이다.

  • React Query에서 캐싱기능을 제대로 사용하려면 query의 상태는 어떤게 있는지, useQuery가 어떻게 동작하는지, option들은 무엇이 있는지 알아야 한다.

query의 상태

  • 기본적으로 쿼리는 다음과 같은 상태가 존재한다.
  • stale : "상한" 이라는 뜻으로 fresh한 상태와 반대이다. query가 stale한 상태에 있다면 react query는 캐싱된 데이터를 사용하지 않는다.

  • fresh : 신선한 즉, 쿼리의 상태가 상하지 않았다는 의미이다. fresh한 상태라면 react query는 캐싱된 데이터를 사용한다.

  • fetching : 현재 fetching이 일어나는 쿼리들의 상태이다.

  • inactive : 사용되지 않는 쿼리들의 상태이다.
  • React Query에서는 staleTime 과 cacheTime을 통해 캐싱을 구현한다.

  • staleTime 이란 query가 stale -> fresh 한 상태로 전환되는데 걸리는 시간이다.

  • cacheTime 이란 캐시에서 데이터가 제거되는데 까지 걸리는 시간이다. cacheTime이 지난 데이터는 가비지컬렉터에 의해 캐시에서 제거된다.

  • react Query에서 staleTime의 default값은 0이고, cacheTime의 default는 5분이다. 즉, useQuery를 통해 fetching된 정보는 cache에 저장되는데, 아무런 설정을 해주지 않았다면 stale하다고 여겨지는 것이다.

  • queryClient의 default option을 지정해주거나, useQuery에 option으로 staleTime을 지정해줌으로써 캐싱을 구현 할 수 있다.


caching이 이루어지는 과정?

  • 캐싱과정은 아래 블로그를 참고하여 직접 소스코드를 확인해보았다.

  • 먼저 React Query를 사용하려면 APP을 QueryClientProvider를 통해 감싼 후 queryClient를 생성해주어야 한다.

  • QueryClientProvider는 React Context를 이용해 전역으로 query를 사용할 수 있도록 해준다. client라는 prop을 넘겨받는데 여기 queryClient를 생성해서 제공해 주어야 한다.

  • 여기서 getQueryClientContext 함수는 context가 있다면 context를 return 해준다. contextSharing은 공식문서에 따르면 더이상 사용되지 않는다고 나와있다.

//QueryClientProvider.tsx 
export const QueryClientProvider = ({
  client,
  children,
  context,
  contextSharing = false,
}: QueryClientProviderProps): JSX.Element => {
  React.useEffect(() => {
    client.mount()
    return () => {
      client.unmount()
    }
  }, [client])

 ...//에러처리 코드 존재

  const Context = getQueryClientContext(context, contextSharing)

  return (
    <QueryClientSharingContext.Provider value={!context && contextSharing}>
      <Context.Provider value={client}>{children}</Context.Provider>
    </QueryClientSharingContext.Provider>
  )
//QueryClientProvider.tsx 
function getQueryClientContext(
  context: React.Context<QueryClient | undefined> | undefined,
  contextSharing: boolean,
) {
  if (context) {
    return context
  }
  if (contextSharing && typeof window !== 'undefined') {
    if (!window.ReactQueryClientContext) {
      window.ReactQueryClientContext = defaultContext
    }

    return window.ReactQueryClientContext
  }

  return defaultContext
}
  • queryClient는 생성될때 queryCache라는 인스턴스를 생성한다.

  • queryCache 객체는 queries라는 배열과, queriesMap 이라는 객체를 맴버변수로 갖는다.

  • queryClient를 콘솔에 찍어보았을때 queryCache의 queries와 queriesMap을 보면 캐싱된 데이터를 조회 할 수 있다.(ReactQueryDevtools로도 볼 수 있다.)

//queryClient.ts
export class QueryClient {
  private queryCache: QueryCache
  
   constructor(config: QueryClientConfig = {}) {
    this.queryCache = config.queryCache || new QueryCache()
    this.mutationCache = config.mutationCache || new MutationCache()
    this.logger = config.logger || defaultLogger
    this.defaultOptions = config.defaultOptions || {}
    this.queryDefaults = []
    this.mutationDefaults = []
    this.mountCount = 0
     
     // ...에러처리 코드
}
//queryCache.ts
export class QueryCache extends Subscribable<QueryCacheListener> {
  config: QueryCacheConfig

  private queries: Query<any, any, any, any>[]
  private queriesMap: QueryHashMap

  constructor(config?: QueryCacheConfig) {
    super()
    this.config = config || {}
    this.queries = []
    this.queriesMap = {}
  }

queryClient 콘솔출력결과

  • queries는 query들을 모아놓은 배열이고, queriesMap은 queryKey를 통해 query를 저장해놓은 객체이다.

  • 결국 캐싱이 이루어지는 과정은, queryClient를 생성한 후 useQuery를 통해 data를 fetching 했을때, 이 데이터를 queryClient 내부에 있는 queryCache라는 객체에 저장해 놓는 것이였다.

  • 더 자세한 동작 원리는 첨부한 아래 reference에 자세하게 설명되어 있다.


QueryClient, useQuery 중요한 default option들

refetchOnMount: boolean | "always" | ((query: Query) => boolean | "always")

  • 쿼리가 stale 상태일경우 마운트시 refetch를 실행하는 option이다.
  • default로는 true 값을 가진다.

refetchOnWindowFocus: boolean | "always" | ((query: Query) => boolean | "always")

  • window가 다시 focus 되었을때 (ex : 창을 이동해서 다시 웹을 열었을때, 커서가 다른페이지에 있다가, 다시 내부를 클릭했을때) 쿼리를 refetch하는 option이다.
  • default로는 true 값을 가진다.

refetchOnReconnect: boolean | "always" | ((query: Query) => boolean | "always")

  • 재연결되을때 데이터가 stale한 상태인경우 refetch를 진행하는 option이다.
  • default로는 true값을 가진다.

retry: boolean | number | (failureCount: number, error: TError) => boolean

  • 쿼리 요청에 실패했을때 refetching을 실행하는 option이다.
  • false라면 실패한 경우 요청을 다시 보내지 않고, true라면 실패한 쿼리는 무한정 재시도한다. number로 설정하면 설정한 횟수만큼 재시도한다.
  • 이러한 옵션들은 개발자가 서버상태를 어떻게 관리하고싶은지에 따라 다르게 적용한다.

  • 예를 들어, 쿼리 데이터가 변하지 않는 정적인 데이터일때 refetchOnMount를 false로 두고, staleTime을 Infinity로 두어 캐싱된 데이터를 사용 할 수 있다.

  • 또한 실시간으로 데이터가 변동된다면 refetchOnWindowFocus를 true로 두어 실시간으로 변하는 데이터를 update 할 수 있다.


프로젝트에서 caching 구현코드

  • staleTime은 기본적으로 cacheTime과 동일하게 5분으로 설정하였고, default값으로 refetchOnWindowFocus, refetchOnReconnect는 false로 설정하였다.

  • 실시간으로 변하는 정보가 아니면 mount시 쿼리가 stale한 상태라면 재 호출되어 정보를 update해주는 과정이 필요했기 때문에 staleTime을 5분으로 설정하였다.

// App.tsx

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
      refetchOnReconnect: false,
      retry: 1,
      staleTime: 1000 * 60 * 5,
      cacheTime: 1000 * 60 * 5,
    },
  },
});
  • 변하지 않는 shop이나 menu 정보같은경우 refetchonMount를 false로, staleTime과 cacheTime을 Infinity로 설정해서 초기 mount시에만 정보를 fetching 하도록 설정하였다.
export const useShopQuery = (shopId: number) => {
  const {
    isLoading: isShopLoading,
    isError: isShopError,
    data: shopState = initialShopState,
    isSuccess,
  } = useQuery<Shops, AxiosError>(['shop', shopId], () => fetchShop(shopId), {
    refetchOnMount: false,
    staleTime: Infinity,
    cacheTime: Infinity,
  });

  return { isShopLoading, isShopError, shopState, isSuccess };
};

ref)

profile
응애 나 애기 개발자

0개의 댓글