프론트엔드 첫걸음

QueryClient 대신 useQueryClient 사용하는 이유 본문

개발 공부/React

QueryClient 대신 useQueryClient 사용하는 이유

차정 2023. 2. 20. 22:02

[출처]

https://stackoverflow.com/questions/71540973/why-use-usequeryclient-from-react-query-library

https://tkdodo.eu/blog/react-query-fa-qs#why-should-i-usequeryclient

QueryClientProvider는 생성된 queryClient 를  React Context에 넣어  전체에 배포한다. useQueryClient로 가장 잘 읽을 수 있다. 이렇게 하면 추가 구독이 생성되지 않으며 추가 재렌더링이 발생하지 않는다(클라이언트가 안정적인 경우).

//클라이언트가 안정적인 경우
export default function App() {
  // ✅ this is stable
  const [queryClient] = React.useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

//클라이언트가 안정적이지 않은 경우
export default function App() {
  // 🚨 this is not good
  const queryClient = new QueryClient()

  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

 

이런경우엔 client를 prop으로 전달하지 않아도 된다. 아니면 client를 export하고 원하는 곳에서 import하면 된다.

exported-query-client

// ⬇️ exported so that we can import it
export const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

 

hook 사용이 선호되는 몇가지 이유가  있다.

1: useQuery는 훅을 사용한다.
useQuery를 호출하면 내부적으로 useQueryClient를 호출한다. 
이는 React Context에서 가장 가까운 클라이언트를 찾는 것을 의미한다. 큰 문제는 아니지만, import한 클라이언트가 컨텍스트 내의 클라이언트와 다른 클라이언트를 가져오는 경우 추적하기 어려운 버그가 발생할 수 있다. 이러한 상황을 피할 수 있다.

2: 앱과 클라이언트의 결합도를 낮춘다.
앱에서 정의한 클라이언트는 프로덕션 클라이언트다. 프로덕션에서 잘 작동하는 몇 가지 기본 설정이 있을 수 있다. 그러나 테스트에서는 다른 기본값을 사용하는 것이 좋을 수 있다. 예를 들어 테스트 중에 잘못된 쿼리를 시도하면 시간 초과로 테스트가 실패할 수 있기 때문에 테스트중에는 재시도 기능을 끄는 것(turning off retries)이 좋을 수 있다. (https://tkdodo.eu/blog/testing-react-query#turn-off-retries)

의존성 주입 메커니즘으로 사용할 때 React Context의 큰 장점 중 하나는 앱과 그 의존성 사이의 결합도를 낮출 수 있다는 것이다. useQueryClient는 위에 어떤 클라이언트가 있던지 상관하지 않고 사용할 수 있다. 프로덕션 클라이언트를 직접 가져온다면 이 장점을 잃게 된다.

3: 가끔 export 할 수 없을 때가 있다.
앱 컴포넌트 내에서 queryClient를 생성해야 하는 경우가 있다(위의 예시). 서버 측 렌더링을 사용하는 경우와 같이 다수의 사용자가 같은 클라이언트를 공유하지 않도록하려면 이렇게 한다.

마이크로프론트엔드를 사용할 때도 그렇다. 앱은 격리되어야 한다. 클라이언트를 앱 외부에서 생성하면 동일한 앱을 두 번 사용하면 동일한 페이지에서 클라이언트를 공유하게 된다.

마지막으로, queryClient의 기본값에 다른 hooks를 사용하려면 App 내에서 queryClient를 만들어한다. 예를 들어, 모든 실패한 mutation마다 토스트를 표시하는 전역 오류 핸들러가 있는 경우를 고려해보아라. 

use-other-hooks

export default function App() {
  // ✅ we couldn't useToast outside of the App
  const toast = useToast()
  const [queryClient] = React.useState(
    () =>
      new QueryClient({
        mutationCache: new MutationCache({
          // ⬇️ but we need it here
          onError: (error) => toast.show({ type: 'error', error }),
        }),
      })
  )

  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

이렇게 queryClient를 만들 경우, 그냥 export하고 App에서 import해서 사용하는 것은 불가능하다.

네가 client를 export하고싶은 이유는 레거시 클래스 컴포넌트에서 쿼리 무효화를 수행해야 할 때일 것이며, 거기서는 훅을 사용할 수 없다. 그런 경우에 함수형 컴포넌트로 쉽게 리팩토링할 수 없다면, render props 버전을 만드는 것을 고려해라.

useQueryClient-render-props

const UseQueryClient = ({ children }) => children(useQueryClient())

usage

<UseQueryClient>
  {(queryClient) => (
    <button
      onClick={() => queryClient.invalidateQueries({ queryKey: ['items'] })}
    >
      invalidate items
    </button>
  )}
</UseQueryClient>

 

useQuery나 다른 모든 hook에 대해서도 같은 방법을 사용할 수 있다.

useQuery-render-props

const UseQuery = ({ children, ...props }) => children(useQuery(props))

usage

<UseQuery queryKey={["items"]} queryFn={fetchItems}>
  {({ data, isLoading, isError }) => (
    // 🙌 return jsx here
  )}
</UseQuery>