* 이글은 Placeholder and Initial data in react query를 번역하였습니다.
Placeholder and Initial Data in React Query
Learn about the different possibilities to avoid loading spinners in React Query.
tkdodo.eu
대부분의 경우 귀찮은 로딩 스피너를 싫어합니다. 때로는 로딩 스피너가 필요하지만, 가능한 한 이를 피하고 싶습니다.
React Query는 이미 많은 상황에서 로딩 스피너를 없앨 수 있는 도구를 제공합니다. 백그라운드에서 업데이트가 진행되는 동안 캐시에서 오래된 데이터를 얻을 수 있고, 나중에 필요할 데이터를 미리 가져올 수 있으며, 쿼리 키가 변경될 때 이전 데이터를 유지해 불필요한 로딩 상태를 방지할 수 있습니다.
또 다른 방법은 우리의 사용 사례에 맞을 가능성이 있는 데이터를 동기적으로 미리 캐시에 채워 넣는 것입니다.
React Query는 이를 위해 두 가지 유사한 접근 방식을 제공합니다. Placeholder Data와 Initial Data입니다.
이들이 공통적으로 가지는 점을 먼저 살펴본 후, 차이점과 어느 상황에서 하나가 다른 것보다 더 적합한지 알아보겠습니다.
Similarities
이미 언급했듯이, Placeholder Data와 Initial Data는 모두 동기적으로 사용할 수 있는 데이터를 사용해 캐시를 미리 채워 넣는 방법을 제공합니다. 이는 둘 중 하나라도 제공되면, 쿼리가 로딩 상태에 들어가지 않고 바로 성공 상태로 전환된다는 것을 의미합니다. 또한, 둘 다 값 자체일 수도 있고, 값을 반환하는 함수일 수도 있습니다. 값 계산이 비용이 많이 들 때 이러한 방식이 유용합니다.
function Component() {
// ✅ status will be success even if we have not yet fetched data
const { data, status } = useQuery({
queryKey: ['number'],
queryFn: fetchNumber,
placeholderData: 23,
})
// ✅ same goes for initialData
const { data, status } = useQuery({
queryKey: ['number'],
queryFn: fetchNumber,
initialData: () => 42,
})
}
이미 캐시에 데이터가 있는 경우에는 Placeholder Data나 Initial Data 둘 다 효과가 없습니다. 그렇다면 둘 중 하나를 사용하는 것의 차이는 무엇일까요? 이를 이해하려면 React Query의 옵션이 어떻게 작동하는지, 그리고 어떤 "레벨"에서 작동하는지 간단히 살펴봐야 합니다.
On Cache level
각 Query Key에 대해 단 하나의 캐시 항목만 존재합니다. 이는 React Query의 강점 중 하나인 데이터를 애플리케이션 내에서 "전역적으로" 공유할 수 있는 가능성 때문입니다.
우리가 useQuery에 제공하는 몇 가지 옵션은 이 캐시 항목에 영향을 미칩니다. 대표적인 예로 queryFn과 gcTime이 있습니다. 캐시 항목이 하나만 존재하기 때문에, 이러한 옵션들은 해당 항목에 대한 데이터를 어떻게 가져올지 또는 언제 캐시 항목을 제거할지를 결정합니다.
On observer level
React Query에서 Observer는 기본적으로 하나의 캐시 항목에 대한 구독(subscription)을 의미합니다. Observer는 해당 캐시 항목을 감시하며, 항목에 변경이 생길 때마다 정보를 받습니다.
Observer를 만드는 가장 기본적인 방법은 useQuery를 호출하는 것입니다. 매번 useQuery를 호출할 때마다 Observer가 생성되고, 데이터가 변경되면 해당 컴포넌트가 다시 렌더링됩니다. 즉, 동일한 캐시 항목을 여러 Observer가 감시할 수 있습니다.
React Query Devtools에서 Query Key 왼쪽에 표시된 숫자는 해당 Query를 감시하고 있는 Observer의 수를 나타냅니다. 예를 들어, 아래 예시에서는 3개의 Observer가 하나의 Query를 감시하고 있는 것입니다.
Observer 레벨에서 작동하는 몇 가지 옵션에는 select와 refetchInterval이 있습니다. 특히, select 옵션이 데이터 변환에 유용한 이유는 동일한 캐시 항목을 감시하면서도, 각기 다른 컴포넌트에서 해당 데이터의 서로 다른 부분(슬라이스)에 구독할 수 있기 때문입니다.
예를 들어, 여러 컴포넌트가 동일한 Query Key를 사용하는 경우, 각각의 컴포넌트는 select 옵션을 사용해 해당 데이터를 원하는 방식으로 변환하여 사용할 수 있습니다. 이렇게 하면 데이터를 일관되게 캐싱하면서도 각기 다른 컴포넌트에서 필요로 하는 데이터만을 구독하고, 리렌더링을 최적화할 수 있습니다.
이를 통해 React Query는 동일한 데이터 소스를 사용하면서도 각 컴포넌트가 데이터를 필요한 방식으로 처리하도록 지원합니다.
Differences
InitialData가 캐시 레벨에서 작동하고, placeholderData는 옵저버 레벨에서 작동한다는 사실은 몇 가지 중요한 차이를 만들어냅니다. 이를 통해 다음과 같은 몇 가지 결과가 나타납니다.
Persistence
InitialData는 캐시에 저장됩니다. 이는 React Query에 "내가 이미 백엔드에서 가져온 것만큼이나 좋은 데이터를 가지고 있다"는 신호를 주는 방식입니다. 이 데이터는 캐시 레벨에서 동작하기 때문에, 처음 옵저버가 마운트될 때 바로 캐시에 저장됩니다.
따라서 같은 Query Key를 사용하는 다른 옵저버가 다른 initialData로 마운트되더라도, 이미 캐시가 채워졌기 때문에 아무런 변화가 없습니다.
반면, PlaceholderData는 캐시에 저장되지 않습니다. 일종의 "임시 데이터"로, 진짜 데이터가 로딩될 때까지 보여주는 데이터라고 생각할 수 있습니다. 이는 옵저버 레벨에서 작동하기 때문에, 여러 컴포넌트에서 서로 다른 placeholderData를 사용할 수 있습니다. 다시 말해, 컴포넌트마다 다른 임시 데이터를 보여줄 수 있지만, 캐시는 영향을 받지 않습니다.
Background refetches
placeholderData를 사용할 때는 옵저버가 처음 마운트될 때 항상 백그라운드에서 리패치가 이루어집니다. 왜냐하면 placeholderData는 "실제 데이터"가 아니기 때문입니다. React Query는 이 "진짜 데이터"를 가져와야 한다고 판단하고, 로딩하는 동안 useQuery에서 isPlaceholderData 플래그를 반환합니다. 이 플래그를 사용하여 사용자에게 현재 보여지는 데이터가 placeholderData임을 시각적으로 알릴 수 있습니다. 실제 데이터가 도착하면 이 플래그는 false로 전환됩니다.
반면, initialData는 캐시에 유효한 데이터로 저장되기 때문에 staleTime 설정을 따릅니다. 기본적으로 staleTime이 0이면, 백그라운드에서 리패치가 발생합니다. 그러나 만약 staleTime을 예를 들어 30초로 설정했다면, React Query는 initialData를 보고 "이 데이터는 아직 유효하다"고 판단하고, staleTime이 지나기 전까지는 리패치를 하지 않습니다.
만약 initialData가 생성된 시점에 따라 리패치를 조정하고 싶다면, initialDataUpdatedAt 옵션을 제공할 수 있습니다. 이를 통해 React Query에게 해당 initialData가 언제 생성되었는지 알려줄 수 있으며, 백그라운드 리패치가 이 정보를 고려하여 발생하게 됩니다.
특히, 기존 캐시 항목에서 initialData를 사용할 때, 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: () =>
// ✅ will refetch in the background if our list query data
// is older than the provided staleTime (30 seconds)
queryClient.getQueryState(['todo', 'list'])?.dataUpdatedAt,
})
}
Error transitions
initialData를 제공했을 때
initialData는 유효한 데이터로 간주되어 캐시에 저장됩니다. 백그라운드 리패치가 실패하더라도 쿼리는 "성공" 상태로 유지되며, UI는 계속해서 initialData를 표시하게 됩니다. React Query는 캐시된 데이터가 여전히 충분히 유효하다고 판단하고, 오류 상태로 전환되지 않습니다.
placeholderData를 제공했을 때
이 경우, placeholderData는 유효하지 않으며 "진짜" 데이터가 아니기 때문에 리패치가 발생합니다. 리패치가 실패하면 쿼리는 오류 상태로 전환되며, UI는 오류 응답을 표시하게 됩니다(오류 처리가 구현되어 있다면). placeholderData는 임시 데이터이기 때문에 유효한 캐시 데이터로 간주되지 않기 때문입니다.
이 두 가지는 데이터의 유효성과 캐시 상태를 다르게 처리하는 방식에 따른 차이를 강조합니다.
When to use what
언제 initialData와 placeholderData를 사용하는지는 전적으로 개발자에게 달려 있습니다. 개인적으로, 저는 쿼리에서 미리 데이터를 채울 때 initialData를 사용하고, 그 외의 경우에는 placeholderData를 사용하는 것을 선호합니다.
initialData는 캐시에 저장되며 데이터를 신뢰할 수 있을 때 적합하고, placeholderData는 임시 데이터를 제공하고 즉시 갱신이 필요한 경우에 유용합니다.
< 참고자료 >
Placeholder and Initial Data in React Query
Learn about the different possibilities to avoid loading spinners in React Query.
tkdodo.eu
<React-query> Placeholder and Initial data in react query
'기타' 카테고리의 다른 글
How styled-components works: A deep dive under the hood (0) | 2024.11.20 |
---|---|
똑똑!! 레포지토리님 어디쯤이신가요? (7) | 2024.10.20 |
글또라는 퍼즐 한 조각 (7) | 2024.10.12 |
오늘보다 더 나은 내일을 살고 있나요? (2) | 2024.01.19 |
어제보다 더 나은 오늘을 살고 있어요? (2) | 2023.12.31 |