상황
메인 페이지가 있고 상품 페이지가 있다.
메인 페이지는 ["product", "main"] 라는 키로 데이터를 페칭한다. useQuery를 사용한다.
상품 페이지는 ["products"] 키로 데이터를 페칭하고 useInfiniteQuery를 사용한다.
하위 키의 차이는 있지만 둘 다 동일한 상품 데이터를 가져오는 역할을 하기 때문에 한쪽에서 데이터를 가져왔다면 다른쪽에서는 페칭을 굳이 또 할 필요 없이 캐싱된 값을 사용하면 되겠다는 생각을 했다.
useInfiniteQuery의 코드를 건드리는것은 꽤 복잡해질 것 같아 상품 페이지를 먼저 방문했고(혹은 상품페이지에서 새로고침을 한 후) 메인 페이지로 이동할 경우 캐싱된 데이터를 쓰도록 만들어보자! 하면서 코드를 수정하기 시작했다.
// MainPage.tsx
const queryClient = useQueryClient();
const cacheData = (
queryClient.getQueryData(["products"]) as InfiniteData<TItem[]>
)?.pages
.map((page) => page)
.flat();
const getData = async () => {
try {
const sortQuery = query(
productsRef,
orderBy("createdAt", "desc"),
limit(LIMIT)
);
const productsSnapshot = await getDocs(sortQuery);
const itemArr = productsSnapshot.docs.map((doc) => {
return {
productId: doc.id,
...doc.data(),
createdAt: doc.data().createdAt.toDate(),
} as TItem;
});
return productsSnapshot;
} catch (err) {
console.log(err);
return;
}
};
const { data = cacheData || [], isLoading } = useQuery({
queryKey: ["products", "main"],
queryFn: getData,
enabled: !cacheData,
select: (data) => {
const items = data?.docs.map((doc) => {
return {
productId: doc.id,
...doc.data(),
createdAt: doc.data().createdAt.toDate(),
} as TItem;
});
return items;
},
});
상품 페이지에 먼저 방문했었다면 cacheData 변수에 캐싱된 값이 담길 것이고 그렇지 않다면 undefined가 출력될 것이다.
unabled에 !cacheData를 지정해 캐싱되지 않은 경우에만 새롭게 요청을 하도록 설정했다. 또한 data의 기본값도 cacheData로 설정하고 캐싱된 데이터가 없을 경우에는 오류 방지 목적으로 빈 배열이 할당되도록 했다.
문제 발생
조건부 렌더링 관련 문제가 발생했다.
수정한 코드를 실행시켜보니 기대한 대로 동작하지 않고 로딩 중일때 보이는 fallback UI만 보였다.
콘솔을 찍어보니 isLoading이 계속 true인 상태로 멈춰있었다.
검색해보니 react query v4에서는 enabled를 이용해 요청을 막은 상태에서는 isLoading이 true로 간주되는 듯 했다.(https://github.com/TanStack/query/issues/3584) 이와 관련한 불만을 많이 볼 수 있었다..
공식 문서를 보니 마운트 이후 데이터가 없는 쿼리는 로딩중으로 간주된다고 한다.
해결 방안
저 링크에 나온 답변 중 useQuery의 리턴값인 fetchStatus와 status를 사용하는 방법이 있었다.
// 전
{isLoading &&
Array.from({ length: MAIN_LIMIT }).map((el, i) => (
<Skeleton key={i} />
))}
{!isLoading &&
data &&
data.slice(0, MAIN_LIMIT).map((product) => {
return (
<div className="relative" key={product.productId}>
<ItemSet data={product} />
<ItemSet.Label>NEW</ItemSet.Label>
</div>
);
})}
// 후
{fetchStatus === "fetching" &&
status === "loading" &&
Array.from({ length: MAIN_LIMIT }).map((el, i) => (
<Skeleton key={i} />
))}
{(fetchStatus !== "fetching" || status !== "loading") &&
data &&
data.slice(0, MAIN_LIMIT).map((product) => {
return (
<div className="relative" key={product.productId}>
<ItemSet data={product} />
<ItemSet.Label>NEW</ItemSet.Label>
</div>
);
})}
요청을 막아놓은 상황(= 캐싱된 값이 있을 때) 에 isLoading은 항상 true로 나타나고 status를 확인해도 "loading"이 출력되고 있었는데 fetchStatus는 "idle"로 나타났다. loading은 아직 쿼리에 데이터가 없다는 의미이고 idle은 쿼리가 현재 아무 작업도 수행하지 않는다는 뜻이다.
이렇게 status와 fetchStatus를 함께 확인하여 원래 의도대로 조건부 렌더링을 할 수 있었다.
로딩 없이 메인 화면을 볼 수 있다.
+ fetchStatus와 status의 차이
- The status gives information about the data: Do we have any or not?
- The fetchStatus gives information about the queryFn: Is it running or not?
데이터와 관련한 상태는 status, 쿼리 요청 함수에 관련한 상태는 fetchStatus이다.
'문제 기록' 카테고리의 다른 글
[framer-motion] 줄 당기는 효과 만들기 (0) | 2023.06.24 |
---|---|
[react] 상태 업데이트 후 바로 변경된 UI를 조작하기 (0) | 2023.04.26 |
[tanstack-query] invalidateQueries를 사용해도 업데이트가 되지 않는 문제 (0) | 2023.02.27 |
''ts-node'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는배치 파일이 아닙니다. 해결방법 (0) | 2023.02.13 |
[git] 변경 사항 다른 브랜치에 커밋하기 (0) | 2022.11.19 |
댓글