찜하기 기능

Minhyuk Song·2024년 12월 10일
0

쇼핑몰 기능 탐방

목록 보기
2/6

요구사항

  • 옵티미스틱 업데이트란 네트워크 요청이 성공할 거라는 낙관적인 믿음을 가지고 업데이트를 진행한다는 것이다.
    (네트워크의 환경과 속도에 상관없이 우선적으로 UI를 변경하고 경우에 따라 다르게 처리하자)

    옵티미스틱 업데이트

    1. 이전 상태를 기억할 수 있어야 합니다.
    2. 서버의 응답을 예상할 수 있어야 합니다.
    3. 경우에 따라 상태를 적절하게 업데이트할 수 있어야 합니다.
      (onMutate, onError, onSettled)

구현결과

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { fetcher, QueryKeys } from '@/queryClient';
import { ProductType } from '@/shared/types/data.type';


const useLike = () => {
  const queryClient = useQueryClient();

  // 1️⃣ 좋아요 기능을 담당하는 함수(likeProduct) 생성
  const { mutate: likeProduct } = useMutation(
    // 🚨 실제로 좋아요를 담당하는 API를 없어서 동작하지 않았습니다.
    (productId: number) =>
      fetcher({ method: 'POST', path: `/products/${productId}/like` }),
    {
      // ✅ cancelQueries를 통해 QueryKeys.PRODUCTS 키를 가진 모든 쿼리를 취소하고
      // 이를 통해 잠재적인 데이터 충돌을 방지
      
      // ✅ getQueryData를 통해 QueryKeys.PRODUCTS 키를 가진 쿼리의 현재 캐시된 데이터를 가져와 previousProducts 변수에 저장하고 
      // 이를 통해 나중에 오류가 발생했을 때 이전 상태로 되돌리기 위해 사용
      
      // ✅ setQueryData를 통해 사용자가 "좋아요" 버튼을 클릭했을 때 UI를 즉시 업데이트
      onMutate: async (productId: number) => {
        await queryClient.cancelQueries({ queryKey: QueryKeys.PRODUCTS });

        const previousProducts = queryClient.getQueryData(QueryKeys.PRODUCTS);

        queryClient.setQueryData(QueryKeys.PRODUCTS, (old: ProductType[]) => {
          return old.map((product: ProductType) =>
            product.id === productId ? { ...product, liked: true } : product
          );
        });

        return { previousProducts };
      },
      // ✅ setQueryData와 위에 있는 previousProducts를 통해 실패 시 이전 데이터로 돌아가도록 변경
      onError: (err: Error, context: { previousProducts: ProductType[] }) => {
        console.log(err);
        queryClient.setQueryData(QueryKeys.PRODUCTS, context.previousProducts);
      },
      // ✅ invalidateQueries를 통해 QueryKeys.PRODUCTS 키를 가진 쿼리를 무효화하여
      // 다음 번에 해당 쿼리가 요청될 때 최신 데이터를 가져오도록 함. 
      // 이는 낙관적 업데이트가 완료된 후, 서버의 실제 상태와 일치하도록 데이터를 동기화하는 데 사용
      onSettled: () => {
        queryClient.invalidateQueries({ queryKey: QueryKeys.PRODUCTS });
      }
    }
  );

  // 2️⃣ 좋아요 해제 기능을 담당하는 함수(unlikeProduct) 생성
  const { mutate: unlikeProduct } = useMutation(
    // 🚨 실제로 안좋아요를 담당하는 API를 없어서 동작하지 않았습니다.
    (productId: number) =>
      fetcher({ method: 'POST', path: `/products/${productId}/unlike` }),
    {
      // ✅ 위의 내용 참고
      onMutate: async (productId: number) => {
        await queryClient.cancelQueries({ queryKey: QueryKeys.PRODUCTS });

        const previousProducts = queryClient.getQueryData(QueryKeys.PRODUCTS);

        queryClient.setQueryData(QueryKeys.PRODUCTS, (old: ProductType[]) => {
          return old.map((product: ProductType) =>
            product.id === productId ? { ...product, liked: false } : product
          );
        });

        return { previousProducts };
      },
      // ✅ 위의 내용 참고
      onError: (err: Error, context: { previousProducts: ProductType[] }) => {
        console.log(err);
        queryClient.setQueryData(QueryKeys.PRODUCTS, context.previousProducts);
      },
      // ✅ 위의 내용 참고
      onSettled: () => {
        queryClient.invalidateQueries({ queryKey: QueryKeys.PRODUCTS });
      }
    }
  );

  return { likeProduct, unlikeProduct };
};

export default useLike;
import { ProductType } from '@/shared/types/data.type';
import { Link } from 'react-router-dom';
import useLike from '@/hooks/useLike';
import { Button } from '../ui/button';

interface ProductItemProps {
  product: ProductType;
}

const ProductItem = (props: ProductItemProps) => {
  const {
    product: { image, price, title, id, liked }
  } = props;

  const { likeProduct, unlikeProduct } = useLike();

  const handleLike = () => {
    if (liked) {
      unlikeProduct(id);
    } else {
      likeProduct(id);
    }
  };

  return (
    <li className="flex h-[395px] w-[200px] flex-col items-center gap-4 p-4">
      <Link to={`/products/${id}`}>
        <img
          src={image}
          alt={title}
          className="min-h-[300px] w-full object-contain"
        />
        <h3 className="line-clamp-1 font-medium">{title}</h3>
        <p className="font-extrabold">{`${price} $`}</p>
      </Link>
      <Button onClick={handleLike}>
        {liked ? '빈 하트 UI' : '채워진 하트 UI'}
      </Button>
    </li>
  );
};

export default ProductItem;

실제 구현 화면이 아니고 이해를 돕기 위한 사진입니다.

배운 점

  • 사용자 경험을 개선하기 위해 낙관적인 업데이트를 자주 사용한다. RQ의 useMutation을 사용하면 데이터 요청에 대한 결과에 따라 onMutate, onError, onSettled를 통해 쉽게 처리할 수 있었다.
  • cancelQueries, getQueryData, setQueryData, invalidateQueries를 통해 쿼리 데이터를 조작하는 방법에 대해 학습할 수 있었다.
    • cancelQueries는 특정 쿼리의 진행 중인 요청을 취소하는 메서드입니다. 주로 낙관적 업데이트를 수행할 때, 현재 진행 중인 쿼리를 취소하고, 잠재적인 데이터 충돌을 방지하기 위해 사용됩니다.
    • getQueryData는 특정 쿼리 키에 해당하는 캐시된 데이터를 가져오는 메서드입니다. 주로 현재 캐시된 데이터를 읽어와서 낙관적 업데이트를 수행하기 전에 이전 상태를 저장하는 데 사용됩니다.
    • setQueryData는 특정 쿼리 키에 해당하는 캐시된 데이터를 업데이트하는 메서드입니다. 주로 낙관적 업데이트를 수행할 때, 서버 응답을 기다리지 않고 UI를 즉시 업데이트하는 데 사용됩니다.
    • invalidateQueries는 특정 쿼리 키에 해당하는 쿼리를 무효화하는 메서드입니다. 무효화된 쿼리는 다음 번에 다시 요청될 때 새로 데이터를 가져오게 됩니다. 주로 서버에서 데이터가 변경된 후, 최신 데이터를 가져오기 위해 사용됩니다.
profile
어제보다 더 나은 오늘을 만들 수 있게

0개의 댓글

관련 채용 정보