
placeholderData와 initialData의 공통점
placeholderData 와 initialData는 언뜻보면 용도가 비슷해보입니다.
- 둘 중 하나의 방법이 제공되면 쿼리가 로딩 상태가 아니라 바로 성공 상태로 전환됩니다.
- 그렇기 때문에 로딩없이 화면상 컨텐츠의 매끄러운 초기 진입이 가능하게 해줍니다.
- 둘 다 값을 반환하는 함수 또는 값 자체를 반환할 수 있습니다.
- 캐시에 데이터가 이미 있는 경우 둘 다 영향을 미치지 않습니다.
하지만 둘은 내부 동작이 다릅니다.
*밑에서 observer 얘기가 계속 나올 것이기 때문에 observer 생성 과정에 대하여 간단하게만 알아둡시다.
useQuery 호출 ▶ 쿼리 생성 ▶ observer 생성 및 observer mount
각각의 내부 동작
placeholderData
- 실제 데이터에 대한 응답을 받기 전까지 "임시" 데이터를 보여주기 위한 용도입니다. 가짜 데이터 = 진짜 생성하기 전까지의 더미 데이터입니다.
- 내가 요청한 API의 응답이 도착하는 순간 화면에 보여지는 데이터는 실제 데이터가 됩니다.
- 캐시에 아무것도 남지 않기 때문에 staleTime에 영향을 받지 않습니다.
- observer level 에서 동작하며 이론적으로 여러 컴포넌트단에서 다른 placeholderData를 가질 수도 있습니다.
placeholderData를 사용하면 observer를 처음 마운트 할 때 곧바로 background-refetch 를 합니다. 실제 데이터가 아니므로 Tanstack Query가 실제 데이터를 가져옵니다. 이 때 useQuery 에서 isPlaceholderData 플래그가 반환됩니다. 이 플래그를 사용하여 사용자가 보고 있는 데이터가 placeholderData 인지 아니면 background-refetch를 통해 가져온 실제 서버 데이터인지 시각적으로 알 수 있습니다. 실제 데이터가 들어오는 즉시 다시 isPlaceholderData 플래그는 false로 전환됩니다.
initialData
- "초기" 데이터를 보여주기 위한 용도로 사용됩니다.
- 해당 쿼리 키 데이터가 캐시에 없다면 initialData 를 해당 쿼리의 캐시에 저장합니다.
- staleTime 지정으로 인해 해당 쿼리 키의 데이터가 'fresh'한 상태라면 캐시에 저장된 initialData가 화면에 보여지게 됩니다. 이 때background-refetch는 진행하지 않습니다. 즉, 초기 데이터가 stale 상태인지 판단 후 background-refetch를 진행하는 것입니다.
- 캐시 수준에서 동작하며, 만약, 백엔드에서 가져온 것과 같이 '좋은(최신)' 데이터가 있다면 캐시 레벨에서 작동하므로 초기 데이터는 placeholderData와 다르게 언제 어디서나 하나의 초기 데이터의 일관성을 보장할 수 있습니다. 또한 해당 데이터는 캐시 항목이 생성되는 즉시 캐시에 저장됩니다(useQuery가 호출되어 첫 번째 observer가 만들어지고 마운트 될 때). 초기 데이터가 다른 두 번째 observer를 마운트 하려고 해도 아무것도 하지 않습니다.
initialData는 캐시에 적합하고 유효한 데이터로 간주되기 때문에 실제로 저장되어 오래된 시간동안 남아있게 됩니다.
그러나 만약 staleTime이 0이라면(기본값) 쿼리는 마운트 즉시 여전히 background-refetch로 바뀐 값으로 교체가 됩니다.
둘의 동작이 비슷하게 느껴지는 이유는 staleTime이 0이여서 그랬을 것입니다.
다른 점을 확실히 알아보기 위해 staleTime을 첫 번째는 '0'으로, 두 번째는 '1000*60'으로 설정할 경우를 가정해보겠습니다. fetch 과정과 여부는 fetchStatus의 'idle', 'paused', 'fetching' 상태에 명령형 조건문을 써서 확인할 수 있습니다.
staleTime 이 0인 경우
- API 요청을 했을 때 placeholderData의 경우에는 이전에 캐싱된 데이터가 없다면, placeholderData를 먼저 보여주게 됩니다. 이후에 API 응답 결과에 따라 화면에 보여지는 데이터로 바뀌게 됩니다.
placeholderData 확인 ▶ background-refetch 바로 시작 ▶ 새로운 서버 데이터로 교체 및 화면에 반영
- API 요청을 했을 때 initialData의 경우에는 이전에 캐싱된 데이터의 여부에 상관없이 stale 0 이므로 fetch 전에initialData를 잠깐 보여주고 곧바로 fetch를 하여 새로운 데이터로 바뀌게 됩니다.
initialData 확인 ▶ stale 상태 확인 ▶background-refetch 시작 ▶ 새로운 서버 데이터로 교체 및 화면에 반영
staleTime이 1000 * 60 (1분)인 경우
- placeholderData의 경우 background-refetch를 진행합니다. 이후에 새로 받아온 데이터가 fresh 상태를 1분동안 유지하게 됩니다.
placeholderData 확인 ▶ background-refetch 바로 시작 ▶ 새로운 서버 데이터로 교체 및 화면에 반영(새로운 쿼리 데이터에 fresh 1분 적용)
- initialData의 경우 background-refetch를 진행하지 않습니다. 1분동안 해당 initialData를 유지하게 됩니다. 이와 관련해서는 아래의 `일반적인 사용` 카테고리를 보시면 됩니다.
initialData 확인 ▶ stale 상태 확인 ▶ initialData fresh 1분 유지 ▶stale 상태가 되면 다음 observer mount 때 그 이후 쿼리 데이터로 교체 및 staleTime 적용
Error transitions
만약 initialData 또는 placeholderData를 제공하여 background-refetch가 트리거 되었지만 이 background-refetch 작업은 실패한다고 가정해 봅시다. 각각의 상황에서 어떤 일이 일어날까요?
일단 화면에 데이터가 보이고 있는 상황 이후의 에러발생을 얘기하고 있는 것입니다.
- initialData: initialData는 캐시에 유지되기 때문에 refetch 오류는 다른 백그라운드 오류와 동일하게 처리됩니다. 쿼리는 error 상태가 되고 화면도 그에 따른 에러 boundary를 구현하든가 해야겠습니다. 이와는 별개로 initialData 데이터는 캐시에 그대로 있습니다. 에러로 인해 새로운 데이터가 없으니 에러 처리 후에 다음 번에도 initialData가 보여지고, staleTime 여부에 따라서 background refetch를 하여 새로운 데이터로 교체되든가 말든가 하겠군요.
- placeholderData: placeholderData는 "진짜 생성하기 전까지의 더미" 데이터이므로, 우리는 이 데이터를 더 이상 볼 수 없습니다.
🎁 일반적인 사용
tanstack query 버전 관리자 tkDodo는 보통 다른 쿼리로부터 쿼리를 미리 채울 때 initialData를 사용하고, 그 외에는 placeholderData를 사용한다고 합니다. 그리고 initialData에 staleTime이 있을 때 fresh한 동안에는 background-refetch가 진행되지 않으므로 initialDataUpdatedAt을 제공해서 initialData를 가져온 다른 출처쿼리의 최근 업데이트 시간보다 오래된 쿼리인지 판단해서 background refetch를 trigger한다고 합니다. 사실 더 정확히 말하자면 아래와 같습니다.
- 즉, initialDataUpdatedAt 을 사용해서 staleTime의 기준 시간을 바꾸게 되는 것인데요? staleTime의 기준을 컴포넌트단에서 초기에 useQuery가 호출됐을 때 + 30초가 아니라, 우리가 initialData로 일부분을 가공해서 사용할 기존의 다른 쿼리 데이터의 최근 업데이트 시간을 기준으로 + 30초로 계산하게 되는 것입니다.
- 만약 (기존의) 다른 출처쿼리가 새로운 데이터를 업데이트 한 지 이미 30초가 지난 stale한 상태라면, 컴포넌트단에서 useQuery를 호출할 때 곧바로 background refetch를 하고 fresh한 데이터를 화면에 보여주겠군요.
- 이 때 유용한 것이 getQueryState의 dataUpdatedAt 입니다(우리가 수동으로 생성하는 것이 아닌 옵저버가 바라보는 쿼리데이터가 업데이트 됐을 때를 의미하는 query state의 dataUpdatedAt 으로서 자동 생성 + 자동 추천으로 뜹니다). 즉, 쿼리 데이터의 최신 업데이트 시간을 의미합니다. dataUpdatedAt 을 사용하면 우리가 가공하여 캐싱할 (기존) 데이터의 최근 업데이트 시간을 가져와서 staleTime이 지났는지 체크하여 업데이트를 할 수 있겠군요.
- 현재 컴포넌트에서 사용하는 useQuery와 불러오는 initialData의 쿼리 키가 완전히(exact) 동일(쿼리 키 배열 전체가 동일)하다면 staleTime이 그 쿼리키를 추적해서 생성됐을 때의 dataUpdatedAt 을 알 수 있기 때문에 이 때는 불필요하게 initialDataUpdatedAt을 설정하지 않아도 됩니다. 그러나 그런 경우는 아마 보기 드물 것입니다. 그리고 어차피 쿼리 키가 완전히 동일하고 똑같은 키 데이터를 이용한다면 initialData도 필요가 없겠습니다. 이미 어딘가에서 쿼리가 생성되어 있다는 뜻일테니까요. 또한 쿼리 키가 exact하게 같을 경우, 현재 바라보고 있는[생성한] 옵저버에서 설정한 staleTime도 그 어딘가의 쿼리 데이터의 업데이트 시간을 기준으로 작동하게 됩니다.(수입해오는 쿼리 데이터의 최근 업데이트 시간 + 현재 옵저버에서 설정한 staleTime 으로 시간이 지났는지 계산한다는 뜻)
- 이런 이유로 완전 키 끝자리까지 동일한 키의 쿼리 데이터를 이용하는 게 아니라면 그 출처 쿼리의 dataUpdatedAt을 initialDataUpdatedAt에 무조건 사용해주는 것이 좋습니다.
const useTodo = (id) => {
const queryClient = useQueryClient()
return useQuery(['todo', id], () => 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,
})
prefetchQuery
유저가 필요로 하는 데이터를 미리 알고 있다면 미리 fetch를 사용하는 것이 좋습니다. 이 경우 prefetchQuery 메서드를 사용하여 캐시에 넣을 쿼리 결과를 prefetch 할 수 있습니다. prefetchQuery는 initialData와 짝궁이라고 할 수 있습니다. prefetch된 데이터는 캐싱 처리되어 다른 곳에서 staleTime(적당히 10초 정도?)을 지정하여 loading 없이 유용하게 사용할 수 있기 때문입니다.
- 키가 완전 일치할 경우에는 initialData 지정이 필요 없습니다.
- 물론 완전 일치가 아닌 쿼리 키에서 이 쿼리 데이터를 가공하여 이용할 때는 initialData에서 얘기했듯이 dataUpdatedAt을 지정해줍시다.
- fetch의 특성과 마찬가지로 staleTime이 지나면 강제로 fetch를 진행합니다.
const prefetchTodos = async () => {
// The results of this query will be cached like a normal query
await queryClient.prefetchQuery(['todos'], fetchTodos)
}
레퍼런스
https://tkdodo.eu/blog/placeholder-and-initial-data-in-react-query