🔥 React Query에서 초기값과 임시값의 활용

823자
11분

React Query를 사용할 때 사용자 경험을 개선하는 방법에 대해 알아보겠습니다. 대부분의 경우 우리와 사용자들은 짜증 나는 로딩 스피너를 싫어합니다. 때로는 필요하지만, 가능하면 피하고 싶어 합니다.

React Query는 이미 많은 상황에서 로딩 스피너를 제거할 수 있는 도구를 제공합니다. 백그라운드 업데이트가 진행되는 동안 캐시에서 오래된 데이터를 가져올 수 있고, 나중에 필요할 것을 알고 있는 데이터를 미리 가져올 수 있으며, 심지어 이전 데이터를 유지하여 쿼리 키가 변경될 때 로딩 상태를 피할 수 있습니다.

또 다른 방법은 우리의 사용 사례에 맞을 것으로 예상되는 데이터로 캐시를 동기적으로 미리 채우는 것입니다. 이를 위해 React Query는 두 가지 다른 접근 방식을 제공합니다: 임시 데이터(Placeholder Data)초기 데이터(Initial Data)입니다.

먼저 이 두 가지 방식의 공통점을 살펴본 다음, 차이점과 각각이 더 적합한 상황을 알아보겠습니다.

공통점

이미 언급했듯이, 두 방식 모두 동기적으로 사용 가능한 데이터로 캐시를 미리 채우는 방법을 제공합니다. 더 나아가 이 중 하나라도 제공되면 쿼리가 loading 상태가 아니라 바로 success 상태로 전환됩니다. 또한 둘 다 _값_이거나 _값_을 반환하는 함수일 수 있습니다. 이는 값을 계산하는 데 비용이 많이 들 때 유용합니다:

function Component() {
  // ✅ 아직 데이터를 가져오지 않았더라도 상태는 성공입니다
  const { data, status } = useQuery({
    queryKey: ['number'],
    queryFn: fetchNumber,
    placeholderData: 23,
  })
 
  // ✅ initialData에도 동일하게 적용됩니다
  const { data, status } = useQuery({
    queryKey: ['number'],
    queryFn: fetchNumber,
    initialData: () => 42,
  })
}
 
javascript

마지막으로, 캐시에 이미 데이터가 있으면 두 방식 모두 영향을 미치지 않습니다. 그렇다면 둘 중 하나를 사용하는 것이 어떤 차이를 만들까요? 이를 이해하기 위해 React Query에서 옵션이 어떻게 (그리고 어떤 "수준"에서) 작동하는지 간단히 살펴보겠습니다:

캐시 수준에서

각 쿼리 키에 대해 하나의 캐시 항목만 있습니다. 이는 React Query의 장점 중 하나인 애플리케이션에서 "전역적으로" 동일한 데이터를 공유할 수 있는 가능성 때문에 당연한 것입니다.

useQuery에 제공하는 일부 옵션은 해당 캐시 항목에 영향을 미칩니다. 대표적인 예로 staleTimegcTime이 있습니다. 캐시 항목이 하나뿐이므로, 이러한 옵션은 해당 항목이 언제 오래된 것으로 간주되거나 언제 가비지 컬렉션될 수 있는지를 지정합니다.

관찰자 수준에서

React Query에서 관찰자는 간단히 말해 하나의 캐시 항목에 대해 생성된 구독입니다. 관찰자는 캐시 항목의 변경 사항을 감시하고 변경될 때마다 알림을 받습니다.

관찰자를 생성하는 기본적인 방법은 useQuery를 호출하는 것입니다. 이를 할 때마다 관찰자를 생성하고, 데이터가 변경되면 컴포넌트가 다시 렌더링됩니다. 물론 이는 동일한 캐시 항목을 감시하는 여러 관찰자를 가질 수 있음을 의미합니다.

참고로, React Query 개발자 도구에서 쿼리 키 왼쪽에 있는 숫자로 쿼리가 가진 관찰자 수를 확인할 수 있습니다(이 예에서는 3):

lecture image

관찰자 수준에서 작동하는 일부 옵션으로는 selectkeepPreviousData가 있습니다. 실제로 select데이터 변환에 좋은 이유는 동일한 캐시 항목을 감시하면서도 다른 컴포넌트에서 데이터의 다른 부분을 구독할 수 있기 때문입니다.

차이점

initialData는 캐시 수준에서 작동하고, placeholderData는 관찰자 수준에서 작동합니다. 이로 인해 몇 가지 영향이 있습니다:

지속성

첫째, initialData는 캐시에 저장됩니다. 이는 React Query에게 "이미 사용 사례에 적합한 '좋은' 데이터가 있다"고 말하는 방법 중 하나입니다. 이 데이터는 백엔드에서 가져온 것만큼 좋은 데이터입니다. 캐시 수준에서 작동하기 때문에 initialData는 하나만 있을 수 있으며, 캐시 항목이 생성될 때(즉, 첫 번째 관찰자가 마운트될 때) 이 데이터가 캐시에 저장됩니다. 다른 initialData로 두 번째 관찰자를 마운트하려고 해도 아무 일도 일어나지 않습니다.

반면에 placeholderData는 절대 캐시에 저장되지 않습니다. 이를 "만들 때까지 가짜로 하는" 데이터로 볼 수 있습니다. 이는 "진짜가 아닙니다". React Query는 실제 데이터를 가져오는 동안 이를 보여줄 수 있도록 제공합니다. 관찰자 수준에서 작동하기 때문에 이론적으로 다른 컴포넌트에 대해 다른 placeholderData를 가질 수도 있습니다.

백그라운드 리페치

placeholderData를 사용하면 처음 관찰자를 마운트할 때 항상 백그라운드 리페치가 발생합니다. 데이터가 "진짜가 아니기" 때문에 React Query가 실제 데이터를 가져옵니다. 이 과정에서 useQuery에서 isPlaceholderData 플래그도 반환됩니다. 이 플래그를 사용하여 사용자에게 보고 있는 데이터가 실제로 임시 데이터라는 것을 시각적으로 알려줄 수 있습니다. 실제 데이터가 들어오면 이 플래그는 다시 false로 전환됩니다.

반면에 initialData는 캐시에 실제로 넣은 좋고 유효한 데이터로 간주되기 때문에 staleTime을 준수합니다. staleTime이 0(기본값)이면 여전히 백그라운드 리페치가 발생합니다.

하지만 쿼리에 staleTime을 설정했다면(예: 30초), React Query는 initialData를 보고 이렇게 생각할 것입니다:

오, 동기적으로 새롭고 신선한 데이터를 받았군요. 고마워요. 이제 이 데이터는 30초 동안 유효하니 백엔드에 갈 필요가 없겠네요.

initialDatastaleTime을 본 React Query의 반응

이것이 원하는 동작이 아니라면 쿼리에 initialDataUpdatedAt을 제공할 수 있습니다. 이는 React Query에게 이 초기 데이터가 언제 생성되었는지 알려주며, 이를 고려하여 백그라운드 리페치가 트리거됩니다. 이는 기존 캐시 항목에서 사용 가능한 dataUpdatedAt 타임스탬프를 사용하여 초기 데이터를 가져올 때 매우 유용합니다:

const useTodo = (id) => {
  const queryClient = useQueryClient()
 
  return useQuery({
    queryKey: ['todo', id],
    queryFn: () => fetchTodo(id),
    staleTime: 30 * 1000,
    initialData: () =>
      queryClient
        .getQueryData(['todo', 'list'])
        ?.find((todo) => todo.id === id),
    initialDataUpdatedAt: () =>
      // ✅ 리스트 쿼리 데이터가 제공된 staleTime(30초)보다
      // 오래되었다면 백그라운드에서 리페치합니다
      queryClient.getQueryState(['todo', 'list'])?.dataUpdatedAt,
  })
}
 
javascript

오류 전환

initialDataplaceholderData를 제공하고 백그라운드 리페치가 트리거되었는데 실패한다고 가정해 봅시다. 각 상황에서 어떤 일이 일어날 것 같나요? 답변을 숨겼으니 원한다면 직접 생각해보고 답변을 확인해 보세요.

initialData
initialData는 캐시에 저장되므로 리페치 오류는 다른 백그라운드 오류와 같이 처리됩니다. 쿼리는 error 상태가 되지만 data는 여전히 존재합니다.

placeholderData
placeholderData는 "만들 때까지 가짜로 하는" 데이터이고 우리가 만들지 못했으므로 더 이상 그 데이터를 볼 수 없습니다. 쿼리는 error 상태가 되고 data는 undefined가 됩니다.

언제 무엇을 사용해야 할까

항상 그렇듯이 이는 전적으로 여러분에게 달려 있습니다. 개인적으로 저는 다른 쿼리에서 쿼리를 미리 채울 때 initialData를 사용하고, 그 외의 모든 경우에 placeholderData를 사용합니다.