장바구니 담기/빼기
라는 버튼을 다르게 보여줘야 한다.
- Context 저장소 생성
- Provider로 사용할 컴포넌트들을 감싸주기
- 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;
};
// 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>
);
// 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;
세션 스토리지는 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));
};
// 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 조합도 있어서 상태관리를 꼼꼼하게 할 수 있을 것 같다는 생각이 들어 충분히 고려해볼 만하다.