TanStack-Query (1) - 캐싱, useQuery, useMutation, key , 낙관적 업데이트

가연·2024년 6월 12일
0

우테코

목록 보기
2/10

🔥tanstack-query(v5)

마지막 미션의 비동기 상태관리는 react-query 로 해야 했다.
생각보다 내용이 많아져서 (1)캐싱 시간,query와 mutation과 낙관적 업데이트, query key (2)무한스크롤, defaultOption 과 캐시와 onError 로 나눠서 정리할 예정이다.

⏰ staleTime vs gcTime(구 cacheTime)

무한스크롤을 구현할 때 staleTime 을 추가해주지 않아서 캐싱이 되지 않았다.
staleTime 이 뭔지, 캐싱이 된다는게 뭔지 알아보자!

🥛staleTime

데이터가 신선한 상태로 남아 있는 시간이며 기본 값은 0 이다.

데이터가 신선하지 않으면(stale) 새로 데이터를 패칭해야 할 때 캐싱된 데이터를 사용하지 않고 새로운 데이터를 패칭해온다.
staleTime 을 설정해주지 않는다면 데이터는 바로 상한 데이터가 되어 매번 새로운 데이터를 요청하게 된다.(캐싱되지 않는다)

그래서 내가 카테고리를 운동기구->전자기기->운동기구 으로 바꿨을 때 패션 데이터 리스트를 새로 요청했던 것이다.

📂gcTime

비활성화된 쿼리가 캐시에서 삭제될 때 까지의 시간이며 기본 값은 5분이다.

메모리에 저장된 사용되지 않는 데이터를 삭제하는 것 이기 때문에 가비지 콜렉션과 연관이 있다. 이것도 마찬가지로 변경될 일이 없는 데이터라면 infinity 로 설정해주어도 될 것 같다.
외부 요소에 의해 변경이 자주 일어나는 데이터라면 메모리에서 삭제해주는게 좋고, 변경될 일이 없는 데이터(ex 단순한 정보를 담고 있는 데이터)라면 굳이 삭제를 해 주지 않아도 될 듯 하다.

💭 staleTime 과 gcTime의 관계

staleTime > gcTime 이라면 비합리적이라고 한다.

staleTime이 Infinity, gcTime 0일 때
캐시에 저장된 데이터가 있다면 새로운 데이터를 패칭하지 않지만, 캐시에 있는 데이터가 사용되지 않으면 즉시 삭제된다.

이 뜻은 쿼리를 사용하지 않다가(gcTime 이 0 이라 캐싱 데이터 삭제) 새로 사용해야 할 경우 캐싱된 데이터를 사용할 수 없게 된다는 것 이다. 그래서 캐싱의 효율적인 사용이 어려워진다.

💭 그래서 결론은?

개인 블로그나 장바구니 처럼 개인이 데이터를 수정 가능하나(이 경우 수정, 삭제한 사용자만 refetch 로 데이터 동기화를 하면 되기 때문에 쿼리 무효화를 사용하면 된다) 외부에 의해 바뀌지 않을 경우 staleTime 을 Infinity 로 설정하는게 좋고

쇼핑몰 상품 리스트와 같이 데이터가 자주 바뀔 수 있는 경우 staleTime 을 짧게 설정하는게 좋다.

불러오는 상품들이 다른 요소(사용자)에 의해 변경된다면 변경된 값을 동기화 하기 위해 새로운 데이터를 받아 와야 하기 때문이다.

| 참고 staleTime, gcTime

🐤 query, mutation

query 는 서버에서 데이터를 조회할 경우 사용하고
(get)
mutation 은 데이터에 변경 작업을 할 경우 사용한다
(put,delete,patch,post)

🐣 useQuery

  const { data: cartItemList } = useQuery<CartItemInfo[]>({
    queryKey: ['fetchCartItems'],
    queryFn: fetchCartItems,
  });

🐥 useMutation

  const queryClient = useQueryClient();

  const { mutate: deleteCartItemMutation } = useMutation({
    mutationFn: (cartId: number) => deleteCartItem(cartId),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['fetchCartItems'] });
    },
  });

//... 사용처
deleteCartItemMutation(cartId)

invalidateQueries 에 장바구니 상품을 불러오는 쿼리 키를 넣어주면, 해당 쿼리의 캐시가 무효화 되고 신선한 데이터를 refetching 한다.
즉 상품을 삭제하면, 삭제한 상태의 상품 리스트를 다시 불러와 동기화 시켜줄 수 있다.

🐝 낙관적 업데이트

react-query 의 useMutation 을 이용하면 낙관적 업데이트가 가능하다.
낙관적 업데이트란 서버 요청 시 성공 여부를 알기 전에 UI를 업데이트할 수 있도록 하는 것을 뜻한다. 즉, 요청을 보내기 전에 UI를 업데이트하는 기능이다.

예를 들어 좋아요 기능을 생각해보자.
네트워크 상태가 좋지 않은 사람이 많은 카페에서 좋아요를 누르게 된다면 좋아요가 눌리지 않아서 답답할 것 이다. '좋아요' 버튼의 경우 서버와의 동기화, 즉 데이터의 정확도가 크게 중요하지 않다.
(반대로 결제 기능의 경우 돈과 관련 되어 있기 때문에 빠른 피드백 보다는 정확도가 중요하다.)

사용자는 가벼운 마음으로 좋아요 기능을 사용할 것 이며, 즉각적인 피드백을 받기 원할 것 이다.
그렇기 때문에 이런 경우 빠른 피드백이 가능한 낙관적 업데이트를 사용하는게 좋을 것 같다.

미션 중, 장바구니에 상품을 담을 때 즉시 결제를 하지 않기 때문에 정확성 보다는 빠른 피드백이 더 중요하다고 느꼈다. 대신 수량 변경 시 오류가 난다면 그때 사용자에게 오류 메세지를 보여주고 상태를 되돌리도록 했다.


느린 3G 에서 수량 변경을 한 결과 즉시 반영이 되는 것을 확인할 수 있다.

  const { mutate: adjustCartItemQuantityMutation } = useMutation({
    mutationFn: ({ cartItemId, quantity }: { cartItemId: number; quantity: number }) =>
      adjustCartItemQuantity(cartItemId, quantity),
        
    onMutate: async ({ cartItemId, quantity }: { cartItemId: number; quantity: number }) => {
      await queryClient.cancelQueries({ queryKey: ['fetchCartItems'] });
      // 장바구니 아이템을 refetch 하고 있다면 취소한다.
      const prevOption = queryClient.getQueryData(['fetchCartItems']);
     // 이전의 데이터를 가져온다.
      queryClient.setQueryData(['fetchCartItems'], (oldCartItems: CartItemInfo[]) => {
        return oldCartItems.map((item) => (item.id === cartItemId ? { ...item, quantity } : item));
      });
      // 이전의 데이터에서 내가 서버 요청 시 보여주고 싶은 상태를 만들어 데이터를 바꿔준다. 여기에서는 변경된 수량을 반영해주었다.
      return { prevOption };
    },
      

    onError: (error, _, context) => {
      toastError(error.message);
      queryClient.setQueryData(['fetchCartItems'], context?.prevOption);
    // 요청에서 에러가 난다면 이전 데이터로 수정해주고 , 에러 메세지를 띄워준다.                           
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['fetchCartItems'] });
    // 성공 시 장바구니 아이템 쿼리 무효화를 통해 새로운 데이터를 refetch 한다.
    },
  });

| 참고 낙관적 업데이트

🔑 key

  • query key 는 의존성 배열이다.
  • 배열에 넣으며, 순서가 다르면 서로 다른 key 로 인식된다.
  • query key 는 반드시 고유한 값이어야 한다.
  • key 가 변경될 때 자동으로 refetch 하게 된다. 그래서 query 는 선언적이라고 할 수 있다. 키 배열에 상태를 넣어주면, 해당 상태가 변할때마다 refetch 해 줄 수 있다.

    ⭐️ 선언적 이라는 뜻은, 어떤 행위가 일어날 때 refetch 를 하라고 명령 을 내리는 게 아니라 키에 조건을 걸어 그 조건이 만족할 때 refetch 할 수 있게 한다는 것 이다.

🚫 관련 이슈

  useInfiniteQuery<ProductResponse>({
      queryKey: ['fetchProductList', {category, order}],

무한스크롤에서 카테고리 혹은 가격순 정렬을 변경했을 때 파라미터가 변경된 상태로 데이터를 refetch 하기 위해 객체 상태로 상태를 추가해줬다.


그런데 카테고리 변경 시 기존의 카테고리 페이지 요청을 다시 보내고, 변경된 카테고리 요청을 보낸 것을 확인할 수 있었다.

그래서 queryKey: ['fetchProductList',category, order], 로 수정했더니

정상적으로 작동했다.

⭐️ { category, order } 객체를 queryKey로 사용할 경우, 객체의 참조가 변경될 때마다 새로운 쿼리 키가 생성된다고 한다. JavaScript에서 객체는 참조에 의해 비교되므로, 심지어 같은 내용의 객체라도 다른 참조를 가지면 다른 객체가 된다.
따라서 카테고리나 정렬 순서가 변경될 때마다 새로운 객체가 생성되고, 이는 새로운 쿼리 키로 인식되어 기존 캐시를 무시하고 모든 페이지를 다시 요청하게 된다고 한다.

카테고리와 순서 변경 시 이전 페이지까지 모두 fetch하는 버그를 해결하는 과정에서 queryKey에 {category, order} 객체를 사용한 것을 발견함. 객체가 참조값으로 비교되어 매 렌더링마다 새로운 쿼리로 인식되고, 이로 인해 이전 데이터가 캐시에 누적되어 불필요하게 로드되는 문제를 파악함. 이를 해결하기 위해 queryKey에 객체 대신 개별 값을 직접 넣어주는 방식으로 수정하여, 실제 값 변경 시에만 새로운 쿼리로 인식되도록 개선함.

객체 사용은 항상 조심하자 🥲

| 참고 1 query key 레퍼런스
| 참고 2 ⭐️ react-query 튜토리얼👍 레퍼런스

배포 페이지
깃허브 페이지

0개의 댓글