장바구니 기능

Minhyuk Song·2024년 12월 10일
0

쇼핑몰 기능 탐방

목록 보기
1/6

요구사항

  • 상품 목록에서 상품을 장바구니에 담을 수 있다.
  • 장바구니 페이지에서 상품을 뺄 수 있다.
  • 상품에 해당하는 ID를 담을 공간이 필요하다.
    • 상태나 스토리지
  • ID를 담는 상태는 여러 군데서 사용이 된다. (장바구니 페이지, 상품목록 페이지, 결제 페이지)
    • 전역 상태 (Context API, Zustand) 혹은 웹 스토리지 (세션 스토리지, 로컬 스토리지)를 사용
  • 장바구니에 있는지에 따라 장바구니 담기/빼기 라는 버튼을 다르게 보여줘야 한다.

구현 결과

Context API로 장바구니 구현

  1. Context 저장소 생성
  2. Provider로 사용할 컴포넌트들을 감싸주기
  3. Context 값을 꺼내서 사용하기

1) Context 저장소 생성

// src/context/CartContext.tsx

import { createContext, useContext, useState, ReactNode } from 'react';

// 1️⃣ CartContext에 대한 타입 정의
interface CartContextType {
  cart: string[];
  addToCart: (id: string) => void;
  removeFromCart: (id: string) => void;
}

// 2️⃣ createContext()를 이용하여 CartContext 초기 생성
const CartContext = createContext<CartContextType | undefined>(undefined);

// 3️⃣ CartContext에서 사용되는 변수와 함수를 정의 (카트, 장바구니에 추가, 장바구니에서 삭제)
export const CartProvider = ({ children }: { children: ReactNode }) => {
  const [cart, setCart] = useState<string[]>([]);

  const addToCart = (id: string) => {
    setCart((prevCart) => [...prevCart, id]);
  };

  const removeFromCart = (id: string) => {
    setCart((prevCart) => prevCart.filter((cartId) => cartId !== id));
  };

  return (
    <CartContext.Provider value={{ cart, addToCart, removeFromCart }}>
      {children}
    </CartContext.Provider>
  );
};

// 4️⃣ CartContext를 쉽게 꺼낼 수 있게 useCart라는 훅으로 관리
export const useCart = () => {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart must be used within a CartProvider');
  }
  return context;
};

2) Provider로 사용할 컴포넌트들을 감싸주기

// main.tsx
createRoot(document.getElementById('root')!).render(
  <StrictMode>
    // ✅ CartContext를 공유할 곳애 Provider로 감싸기
    <CartProvider>
      <div className="flex w-dvw justify-center border-x-2 border-gray-300">
        <div className="flex w-[480px] flex-col items-center justify-center border-x border-solid border-gray-300">
          <NavBar />
          <App />
          <Footer />
        </div>
      </div>
    </CartProvider>
  </StrictMode>
);

3) Context 값을 꺼내서 사용하기

// src/components/product/ProductItem.tsx

import { ProductType } from '@/shared/types/data.type';
import { Link } from 'react-router-dom';
import { Button } from '../ui/button';
import { useCart } from '@/context/CartContext';

interface ProductItemProps {
  product: ProductType;
}

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

  // ✅ CartContext를 쉽게 꺼내서 사용할 수 있습니다.
  const { addToCart } = useCart();

  return (
    <li className="flex h-[395px] w-[200px] flex-col items-center gap-4 p-4">
      <a href={`/products/${id}`}>
        <img src={image} alt={title} className="w-full h-48 object-cover rounded-md" />
        <h2 className="text-xl font-bold mt-2">{title}</h2>
        <p className="text-lg font-semibold">${price}</p>
      </a>
      // ✅ 장바구니에 추가
      <Button onClick={() => addToCart(id)}>장바구니 추가</Button>
    </li>
  );
};

export default ProductItem;
// src/pages/CartPage.tsx

import React from 'react';
import { useCart } from '@/context/CartContext';
import { ProductType } from '@/shared/types/data.type';
import { Link } from 'react-router-dom';
import { Button } from '../ui/button';

const CartPage = ({ products }: { products: ProductType[] }) => {
  // ✅ CartContext를 쉽게 꺼내서 사용할 수 있습니다.
  const { cart, removeFromCart } = useCart();

  const cartItems = products.filter((product) => cart.includes(product.id.toString()));

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">장바구니</h1>
      <ul className="grid grid-cols-2 gap-x-2 gap-y-4">
        {cartItems.map((product) => (
          <li key={product.id} className="flex h-[395px] w-[200px] flex-col items-center gap-4 p-4">
            <a href={`/products/${product.id}`}>
              <img src={product.image} alt={product.title} className="w-full h-48 object-cover rounded-md" />
              <h2 className="text-xl font-bold mt-2">{product.title}</h2>
              <p className="text-lg font-semibold">${product.price}</p>
            </a>
            // ✅ 장바구니에서 빼기
            <Button onClick={() => removeFromCart(product.id.toString())}>Remove from Cart</Button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default CartPage;

세션 스토리지로 장바구니 구현

1) 세션 스토리지를 이용하여 유틸함수를 만들기

세션 스토리지는 JSON 형태로 키-값이 문자열 데이터를 가진다. 그래서 JSON.parse()JSON.stringify()를 적절하게 사용해야 한다.

// src/utils/cart.ts

export const getCart = (): string[] => {
  const cart = sessionStorage.getItem('cart');
  return cart ? JSON.parse(cart) : [];
};

export const addToCart = (id: string) => {
  const cart = getCart();
  cart.push(id);
  sessionStorage.setItem('cart', JSON.stringify(cart));
};

export const removeFromCart = (id: string) => {
  let cart = getCart();
  cart = cart.filter((cartId) => cartId !== id);
  sessionStorage.setItem('cart', JSON.stringify(cart));
};

2) 카트에 관한 유틸함수 사용하기

// src/pages/CartPage.tsx

import { useState, useEffect } from 'react';
import { getCart, removeFromCart } from '@/utils/cart';
import { ProductType } from '@/shared/types/data.type';
import { Link } from 'react-router-dom';
import { Button } from '../ui/button';

const CartPage = ({ products }: { products: ProductType[] }) => {
  // ✅ 카트 아이템의 상태 변화에 따라 UI 변화시키기 위해 useState를 사용
  const [cartItems, setCartItems] = useState<ProductType[]>([]);

  // ✅ 렌더링 이후에 처리할 작업을 작성하기 위해 useEffect를 사용
  useEffect(() => {
    const cart = getCart();
    const items = products.filter((product) => cart.includes(product.id.toString()));
    setCartItems(items);
  }, [products]);

  const handleRemove = (id: string) => {
    removeFromCart(id);
    setCartItems((prevItems) => prevItems.filter((item) => item.id.toString() !== id));
  };

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">장바구니</h1>
      <ul className="grid grid-cols-2 gap-x-2 gap-y-4">
        {cartItems.map((product) => (
          <li key={product.id} className="flex h-[395px] w-[200px] flex-col items-center gap-4 p-4">
            <Link to={`/products/${product.id}`}>
              <img src={product.image} alt={product.title} className="w-full h-48 object-cover rounded-md" />
              <h2 className="text-xl font-bold mt-2">{product.title}</h2>
              <p className="text-lg font-semibold">${product.price}</p>
            </Link>
            <Button onClick={() => handleRemove(product.id.toString())}>Remove from Cart</Button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default CartPage;

배운 점

  • Context API로 구현을 하면 초기 설정 과정이 상태 관리 라이브러리보다 써야 하는 코드가 많아서 난도가 어느 정도 있다고 느꼈다. 다만 커스텀 훅으로 잘 관리해서 설정 단계 이후에 사용할 때는 편리하게 사용할 수 있다. 그리고 상태를 한 번만 만들면 되어서 추상화가 잘 되어있고 세션 스토리지만 쓸 때보다 덜 신경쓸 수 있다.

  • 세션 스토리지는 브라우저 탭에 존재하는 웹 스토리지이며, 탭을 종료 시 세션 스토리지에 담긴 데이터는 사라지는 특성을 가지고 있다. 그래서 로그인 상태와 장바구니에 세션 스토리지가 자주 쓰입니다.
    세션 스토리지에서 쓰이는 문법과 JSON 형태의 데이터를 신경을 잘 쓴다면 손쉽게 사용할 수 있습니다.
    다만, 장바구니에 대한 데이터를 사용하는 페이지 컴포넌트에서 따로 상태를 매번 만들어야 한다.

  • 세션 스토리지 + Context API 조합도 있어서 상태관리를 꼼꼼하게 할 수 있을 것 같다는 생각이 들어 충분히 고려해볼 만하다.

profile
어제보다 더 나은 오늘을 만들 수 있게

0개의 댓글

관련 채용 정보