관리 메뉴

거니의 velog

(54) 리액트 쿼리와 리코일 6 본문

SpringBoot_React 풀스택 프로젝트

(54) 리액트 쿼리와 리코일 6

Unlimited00 2024. 3. 11. 20:42

6. 장바구니 처리

* 장바구니 처리는 서버와 연동해야 하는 부분은 리액트 쿼리를 이용해서 처리하고, 장바구니의 아이템에 대한 상태 처리는 리코일을 이용한다.

import { atom } from "recoil";

export const cartState = atom({
  key: "cartState",
  default: [],
});

* cartState는 카트에 담긴 장바구니 아이템의 배열을 선언한다.


(1) 리코일의 Selector

* 리코일의 Atom이 데이터 자체를 의미한다면, Selector는 데이터를 이용해서 처리할 수 있는 기능을 의미한다. 예를 들어 장바구니의 경우 해당 상품의 가격과 수량을 이용해서 전체 장바구니의 총액을 구하는 기능을 사용할 수 있다.

* 리코일의 Selector는 데이터를 가공해서 원하는 기능을 제공하기 때문에 getter처럼 사용되지만, Atom 으로 관리되는 데이터를 변경하는 setter의 기능도 같이 사용할 수 있다.

import { atom, selector } from "recoil";

export const cartState = atom({
  key: "cartState",
  default: [],
});

export const cartTotalState = selector({
  key: "cartTotalState",
  get: ({ get }) => {
    const arr = get(cartState);
    const initialValue = 0;
    const total = arr.reduce(
      (total, current) => total + current.price * current.qty,
      initialValue
    );

    return total;
  },
});

(2) 장바구니 데이터 보관

* 대부분의 작업은 useCustomCart를 통해서 처리된다. useMutation을 통해서 서버에 장바구니 아이템을 조정하고 useQuery를 이용해서 리액트 내에 데이터를 보관하게 만든다. 다른 컴포넌트들이 장바구니 데이터를 공유해서 사용할 수도 있기 때문에 이 부분은 리코일로 만든 cartState를 이용한다.

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getCartItems, postChangeCart } from "../api/cartApi";
import { useRecoilState } from "recoil";
import { cartState } from "../atoms/cartState";
import { useEffect } from "react";

const useCustomCart = () => {
  const [cartItems, setCartItems] = useRecoilState(cartState);
  const queryClient = useQueryClient();
  const changeMutation = useMutation((param) => postChangeCart(param), {
    onSuccess: (result) => {
      setCartItems(result);
    },
  });
  const query = useQuery(["cart"], getCartItems, { staleTime: 1000 * 60 * 60 }); // 1 hour

  useEffect(() => {
    if (query.isSuccess) {
      queryClient.invalidateQueries("cart");
      setCartItems(query.data);
    }
  }, [query.isSuccess, query.data]);

  const changeCart = (param) => {
    changeMutation.mutate(param);
  };

  return { cartItems, changeCart };
};

export default useCustomCart;

* useQuery()를 이용할 때 1시간의 staleTime을 지정한 이유는 외부에서 어떤 영향으로 상품의 정보가 변경될 수 있기 때문에 자주는 아니지만 가끔은 장바구니 안에 있는 상품 정보를 다시 가져오기 위해서이다.

* components/menus/CartComponent는 리액트 쿼리와 리코일로 처리된 useCustomCart를 이용하고 리코일의 Seletor로 만든 총액(total)을 보여주도록 한다.

import useCustomLogin from "../../hooks/useCustomLogin";
import useCustomCart from "../../hooks/useCustomCart";
import CartItemComponent from "../cart/CartItemComponent";
import { useRecoilValue } from "recoil";
import { cartTotalState } from "../../atoms/cartState";

const CartComponent = () => {
  const { isLogin, loginState } = useCustomLogin();
  const { cartItems, changeCart } = useCustomCart();
  const totalValue = useRecoilValue(cartTotalState);

  return (
    <div className="w-full">
      {isLogin ? (
        <div className="flex flex-col">
          <div className="w-full flex">
            <div className="font-extrabold text-2xl w-4/5">
              {loginState.nickname}'s Cart
            </div>
            <div className="bg-orange-600 text-center text-white font-bold w-1/5 rounded-full m-1">
              {cartItems.length}
            </div>
          </div>
          <div>
            <ul>
              {cartItems.map((item) => (
                <CartItemComponent
                  {...item}
                  key={item.cino}
                  changeCart={changeCart}
                  email={loginState.email}
                />
              ))}
            </ul>
          </div>
          <div className="m-2 text-3xl ">TOTAL: {totalValue}</div>
        </div>
      ) : (
        <div></div>
      )}
    </div>
  );
};

export default CartComponent;

* 브라우저를 이용해서 로그인된 상황에서는 장바구니의 내용물들이 출력되는지 확인하고 마지막에 총액(total)이 출력되는지 확인한다.


(3) 장바구니 아이템 추가

* 상품 조회 화면에서는 장바구니에 아이템을 추가하는 기능을 추가하기 위해서 useCustomLogin()과 useCustomCart()를 이용한다.

import React from "react";
import { getOne } from "../../api/productsApi";
import { API_SERVER_HOST } from "../../api/todoApi";
import useCustomMove from "../../hooks/useCustomMove";
import FetchingModal from "../common/FetchingModal";
import useCustomCart from "../../hooks/useCustomCart";
import useCustomLogin from "../../hooks/useCustomLogin";
import { useQuery } from "@tanstack/react-query";

const initState = {
  pno: 0,
  pname: "",
  pdesc: "",
  price: 0,
  uploadFileNames: [],
};

const host = API_SERVER_HOST;

const ReadComponent = ({ pno }) => {
  //화면 이동용 함수
  const { moveToList, moveToModify } = useCustomMove();
  const { loginState } = useCustomLogin();
  const { cartItems, changeCart } = useCustomCart();
  const { isFetching, data } = useQuery(["products", pno], () => getOne(pno), {
    staleTime: 1000 * 10 * 60,
    retry: 1,
  });

  const handleClickAddCart = () => {
    let qty = 1;
    const addedItem = cartItems.filter((item) => item.pno === parseInt(pno))[0];

    if (addedItem) {
      if (
        window.confirm("이미 추가된 상품입니다. 추가하시겠습니까? ") === false
      ) {
        return;
      }
      qty = addedItem.qty + 1;
    }

    changeCart({ email: loginState.email, pno: pno, qty: qty });
  };

  const product = data || initState;

  return (
    (...)
  );
};

export default ReadComponent;


(4) 로그아웃 처리

* 로그인이나 로그아웃은 모두 useCustomLogin()을 이용해서 처리하고 있는 상황이지만, 로그아웃 시에는 리코일로 유지되는 장바구니 아이템들도 모두 삭제한다. 리코일의 useResetRecoilState()를 이용해서 장바구니 데이터들 역시 삭제하도록 한다.

import { Navigate, useNavigate, createSearchParams } from "react-router-dom";
import { useRecoilState, useResetRecoilState } from "recoil";
import signinState from "../atoms/signinState";
import { loginPost } from "../api/memberApi";
import { removeCookie, setCookie } from "../util/cookieUtil";
import { cartState } from "../atoms/cartState";

const useCustomLogin = () => {
  const navigate = useNavigate();
  const [loginState, setLoginState] = useRecoilState(signinState);
  const resetState = useResetRecoilState(signinState);
  const resetCartState = useResetRecoilState(cartState); // 장바구니 비우기

  const isLogin = loginState.email ? true : false; // 로그인 여부

  (...)

  // 로그아웃 함수
  const doLogout = () => {
    removeCookie("member");
    resetState();
    resetCartState();
  };

  (...)
};

export default useCustomLogin;

[리덕스 툴킷 설정 지우기]

* 마지막으로 리덕스 툴킷을 사용하지 않도록 index.js를 수정해서 스토어 관련 설정을 삭제한다.

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { RecoilRoot } from "recoil";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>
);

reportWebVitals();

* 기존에 리덕스 툴킷을 제거한 후에도 정상적으로 동작하는지 확인해 준다.