본문 바로가기

FrontEnd

리액트 쿼리 : FAQ(자주 묻는 질문)

반응형

원문 : https://tkdodo.eu/blog/react-query-fa-qs

 

React Query FAQs

Answering the top 3 most frequently asked React Query questions

tkdodo.eu

상당히 의역한 내용이라 본문과 다를 수 있습니다. (의미가 다르진 않을껍니다... 군복무 시절에도 토익 900은 넘었습니다...)

React Query : FAQs

Refetch에 파라미터 넘기기

불가능합니다. 이유는? : Refetch라 생각한 경우는 대부분 Refetch가 아닙니다.

const { data, refetch } = useQuery(['item'], () => fetchItem({ id: 1 }))

<button onClick={() => {
  // 🚨 this is not how it works
  refetch({ id: 2 })
}})>Show Item 2</button>

파라미터 또는 변수는 쿼리에 대한 종속성입니다.
위의 코드에서 QueryKey ['item']을 정의하므로 해당 쿼리로 가져오는 모든 데이터가 해당 키 아래에 저장됩니다.
다른 ID로 다시 가져오면 키가 item으로 동일하게 유지되기 때문에 캐시의 동일한 위치에 계속 기록됩니다.
즉, id 2는 id 1의 데이터를 덮어씁니다.
다시 id 1로 전환하면 해당 데이터가 사라집니다.
따라서 우리가 원하는 것은 refetch가 아니라 다른 id를 이용한 새로운 id의 fetching입니다.
 
다른 쿼리 키를 활용하여 다른 응답을 캐싱하는 것은 React Query의 가장 큰 장점 중 하나입니다.
이것이 refetch가 동일한 변수로 요청을 재생하기 위한 것인 이유입니다.
 
따라서 본질적으로 우리가 원하는 것은 refetch가 아닙니다. 다른 ID에 대한 새로운 데이터 가져오기를 원합니다!
const [id, setId] = useState(1)

const { data } = useQuery(['item', id], () => fetchItem({ id }))

<button onClick={() => {
  // ✅ set id without explicitly refetching
  setId(2)
}})>Show Item 2</button>

또한 해당 ID를 useState에 저장할 필요가 없습니다.

클라이언트 측 상태(zustand, redux, ...)를 저장하기 위한 어떤 방법으로든 수행할 수 있습니다.
위의 예에서 URL은 ID를 저장하기에 좋은 장소이기도 합니다.
선언적 접근 방식은 또한 ID를 업데이트하는 위치 또는 방법에 관계없이 쿼리 데이터가 항상 ID와 "동기화"되도록 합니다.
따라서 귀하의 생각은 "해당 버튼을 클릭하면 다시 가져오고 싶습니다"에서 "항상 현재 ID에 대한 데이터를 보고 싶습니다"로 이동합니다.

const { id } = useParams()

const { data } = useQuery(['item', id], () => fetchItem({ id }))

// ✅ change url, make useParams pick it up
// 항상 현재 URL의 id에 해당하는 데이터를 보고 싶습니다.
<Link to="/2">Show Item 2</Link>​

이 접근 방식의 가장 좋은 점은 상태를 관리할 필요가 없이 공유 가능한 URL을 가져오고

브라우저 뒤로가기 네비게이션 버튼이 사용자가 item 사이를 탐색(네비게이션)하는 데에도 동작한다는 점입니다.

 


로딩 상태

쿼리 키를 전환하면 쿼리가 다시 하드 로드 상태가 됨을 알 수 있습니다.
우리가 키를 변경하고 해당 키에 대한 데이터가 아직 없기 때문에 예상됩니다.
해당 키에 대한 placeholderData를 설정하거나
새 키에 대한 데이터를 미리 가져오는 것과 같이 전환을 용이하게 하는 많은 방법이 있습니다.
이 문제를 해결하는 좋은 방법은 쿼리에 이전 데이터를 유지하도록 지시하는 것입니다.
const { data, isPreviousData } = useQuery(
  ['item', id],
  () => fetchItem({ id }),
  // ⬇️ like this️
  { keepPreviousData: true }
)

이 플래그를 켜면
  • React Query는 id 2의 데이터를 가져오는 동안 여전히 id 1의 데이터를 표시합니다.
  • 쿼리 result의 isPreviousData 플래그가 true로 설정되어 UI에서 그에 따라 조치를 취할 수 있습니다.
    • 데이터와 함께 백그라운드 로딩 스피너를 표시하거나
    • 표시되는 데이터에 불투명도를 추가하여 오래된 데이터임을 나타낼 수 있습니다.
그것은 전적으로 당신에게 달려 있습니다 - React Query는 당신에게 그렇게 할 수 있는 수단을 제공합니다. 🙌


왜 화면에 업데이트가 반영되지 않나요?

쿼리 캐시와 직접 상호 작용할 때
  • 뮤테이션 응답에서 업데이트를 수행하거나
  • 뮤테이션에서 캐시를 무효화하려는 경우
때때로 업데이트가 화면에 반영되지 않는다는 질문을 받았씁니다.
 
대부분 다음 두 가지 문제 중 하나로 요약됩니다.
 

1 :  쿼리키가 일치하지 않음

쿼리 키는 결정적으로 해시되므로 참조 안정성이나 객체 키 순서를 염두에 둘 필요가 없습니다.
그러나 queryClient.setQueryData를 호출할 때 키는 여전히 기존 키와 완전히 일치해야 합니다.
예를 들어 다음 두 키는 일치하지 않습니다.
['item', '1']
['item', 1]

키 배열의 두 번째 값은 첫 번째 예에서는 문자열이고 두 번째 예에서는 숫자입니다.
이것은 일반적으로 숫자로 작업하는 경우 발생할 수 있지만 useParams를 사용하여 URL에서 읽는 경우 문자열을 가져옵니다.
React Query Devtools는 어떤 키가 존재하고 현재 어떤 키를 가져오고 있는지 명확하게 볼 수 있으므로 이 경우 큰 도움이 됩니다.
그래도 그 성가신 세부 사항을 주시하십시오! 이 문제를 해결하려면 TypeScriptQuery Key Factory를 사용하는 것이 좋습니다.

2 :  QueryClient가 참조 안정(stable)하지 않음.

대부분의 예에서 App 컴포넌트 외부에 queryClient를 생성하여 참조적으로 안정하게 만듭니다.
// ✅ created outside of the App
const queryClient = new QueryClient()

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

QueryClient는 QueryCache를 보유하므로 새 클라이언트를 생성하면 비어 있는 새 캐시도 얻게 됩니다.
클라이언트 생성을 App 컴포넌트 내부로 옮기고 컴포넌트가 다른 이유로 다시 렌더링되면(예: 경로 변경) 캐시가 삭제됩니다.
 
export default function App() {
  // 🚨 this is not good
  const queryClient = new QueryClient()

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

앱 내부에 클라이언트를 생성해야 하는 경우 인스턴스 ref 또는 React 상태를 사용하여 참조 안정한지 확인하세요.
export default function App() {
  // ✅ this is stable
  const [queryClient] = React.useState(() => new QueryClient())

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

해당 주제에 대한 별도의 블로그 게시물이 있습니다: 일회성 초기화를 위한 useState(useState for one-time initializations)

(이 블로그에도 해당 게시물 번역본이 있음!!)


useQueryClient()를 사용해야 하나요?

... 그냥 클라이언트를 import 하면 되지 않나요?

QueryClientProvider는 생성된 queryClient를 React Context에 넣어 앱 전체에 배포합니다.
useQueryClient를 사용하여 가장 잘 읽을 수 있습니다.
이것은 추가 구독을 생성하지 않으며 추가 재렌더링을 일으키지 않습니다(클라이언트가 안정적인 경우 - 위 참조)
클라이언트를 소품으로 전달하지 않아도 됩니다.
 
또는 클라이언트를 익스포트 하고 필요한 위치에서 임포트 할 수 있습니다.
// ⬇️ exported so that we can import it
export const queryClient = new QueryClient()

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

왜 useQueryClient를 선호하는지 설명하겠습니다.

1: useQuery도 훅를 사용합니다.

useQuery를 호출하면 내부에서 useQueryClient를 호출합니다.
이것은 React Context에서 가장 가까운 클라이언트를 찾습니다.
큰 문제는 아니지만 가져오는 클라이언트가 컨텍스트의 클라이언트와 다른 상황에 빠지면 피할 수 있는 버그를 추적하기가 어렵습니다.
 

2: 클라이언트에서 앱을 분리합니다.

앱에서 정의한 클라이언트가 프로덕션 클라이언트입니다. 프로덕션에서 잘 작동하는 몇 가지 기본 설정이 있을 수 있습니다.
 
그러나 테스트에서는 다른 기본값을 사용하는 것이 합리적일 수 있습니다.
 
한 가지 예는 테스트 중에 재시도를 끄는 것입니다. 그렇지 않으면 잘못된 쿼리를 테스트하면 테스트 시간이 초과될 수 있기 때문입니다.
의존성 주입 메커니즘으로 사용될 때 React Context의 큰 장점은 의존성에서 앱을 분리한다는 것입니다.
useQueryClient는 특정 클라이언트가 아니라 위의 트리에 있는 모든 클라이언트를 예상합니다.

프로덕션 클라이언트를 직접 가져오면 이점을 잃게 됩니다.

3: 때로는 export 할 수 없습니다.

App 컴포넌트 내부에 queryClient를 생성해야 하는 경우가 있습니다.
 
한 가지 예는 여러 사용자가 동일한 클라이언트를 공유하지 않도록 하기 위해 서버 렌더링을 사용할 때입니다.
 
마이크로프론트엔드로 작업할 때도 마찬가지입니다.
앱은 격리되어야 합니다.
앱 외부에서 클라이언트를 만든 다음 동일한 페이지에서 동일한 앱을 두 번 사용하면 클라이언트가 공유됩니다.
 
마지막으로 queryClient의 기본값에서 다른 hook을 사용하고 싶다면 App 내부에서도 생성해야 합니다.
모든 실패한 뮤테이션에 대해 알림을 표시하려는 전역 오류 처리기를 고려하십시오.
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를 이와 같이 생성하면 앱에서 내보내고 가져올 수 있는 방법이 없습니다.
 
클라이언트를 내보내려는 이유에 대한 최선의 추측은
쿼리 무효화를 수행해야 하는 레거시 클래스 컴포넌트로 작업하고 있고 거기에서 후크를 사용할 수 없는 경우입니다.
그런 경우이고 함수 컴포넌트로 쉽게 리팩토링할 수 없다면 렌더 프롭 버전을 만드는 것이 좋습니다.
// 렌더 프롭 버전
const UseQueryClient = ({ children }) => children(useQueryClient())
// 사용
<UseQueryClient>
  {(queryClient) => (
    <button
      onClick={() => queryClient.invalidateQueries(['items'])}
    >
      invalidate items
    </button>
  )}
</UseQueryClient>

해당 문제를 useQuery를 이용하거나 다른 훅을 이용해 해결할 수도 있습니다.
// useQuery-render-props
const UseQuery = ({ children, ...props }) => children(useQuery(props))
// usage
<UseQuery queryKey={["items"]} queryFn={fetchItems}>
  {({ data, isLoading, isError }) => (
    // 🙌 return jsx here
  )}
</UseQuery>
오늘은 여기까지입니다. 질문이 있는 경우 트위터에서 twitter에게 연락하거나 아래에 댓글을 남겨주세요. ⬇️
 




반응형