반응형

1. 문제 상황: Prop Drilling
이미지에서 보시는 것처럼, App 컴포넌트에서 Cart State를 관리하고 있습니다. 이 상태는:
- Shop → Product 경로로 전달되어 장바구니에 추가 (Update Cart)
- Header → CartModal → Cart 경로로 전달되어 장바구니 보기 (Display Cart)
이처럼 중간 컴포넌트(Shop, Header, CartModal)가 상태를 단순히 전달만 하는 경우, Prop Drilling 문제가 발생합니다.
이는 코드 유지보수성과 가독성을 떨어뜨립니다.
✅ 해결책: React Context 사용
2. React Context란?
React Context는 컴포넌트 트리 전체에 걸쳐 데이터를 공유할 수 있도록 해주는 기능입니다.
주로 테마, 사용자 정보, 로그인 상태, 장바구니 상태 등 전역적으로 필요한 상태를 관리할 때 사용됩니다.
핵심 개념:
- createContext: Context 객체 생성
- Provider: 상태를 제공하는 컴포넌트
- useContext: 상태를 소비하는 컴포넌트
3. 장바구니 Context 설계
우리는 아래와 같은 구조로 Context를 설계합니다:
// CartContext.tsx
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartContextType {
cart: CartItem[];
addToCart: (item: Omit<CartItem, 'quantity'>) => void;
removeFromCart: (id: number) => void;
clearCart: () => void;
totalItems: number;
totalPrice: number;
}
React Context로 장바구니 구현
🗂️ 프로젝트 구조
src/
├── contexts/
│ └── CartContext.tsx
├── components/
│ ├── Header.tsx
│ ├── CartModal.tsx
│ ├── Cart.tsx
│ ├── Shop.tsx
│ └── Product.tsx
└── App.tsx
1. contexts/CartContext.tsx
// contexts/CartContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
// 장바구니 아이템 타입 정의
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
// Context 타입 정의
interface CartContextType {
cart: CartItem[];
addToCart: (item: Omit<CartItem, 'quantity'>) => void;
removeFromCart: (id: number) => void;
clearCart: () => void;
totalItems: number;
totalPrice: number;
}
// 초기값 설정
const CartContext = createContext<CartContextType | undefined>(undefined);
// 커스텀 훅: useContext를 더 쉽게 사용하기 위해
export const useCart = () => {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
};
// Provider 컴포넌트
export const CartProvider = ({ children }: { children: ReactNode }) => {
const [cart, setCart] = useState<CartItem[]>([]);
// 장바구니에 상품 추가
const addToCart = (item: Omit<CartItem, 'quantity'>) => {
setCart(prevCart => {
const existingItem = prevCart.find(i => i.id === item.id);
if (existingItem) {
return prevCart.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...prevCart, { ...item, quantity: 1 }];
});
};
// 장바구니에서 상품 제거
const removeFromCart = (id: number) => {
setCart(prevCart => prevCart.filter(item => item.id !== id));
};
// 장바구니 비우기
const clearCart = () => {
setCart([]);
};
// 총 개수 및 총 가격 계산
const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
return (
<CartContext.Provider
value={{
cart,
addToCart,
removeFromCart,
clearCart,
totalItems,
totalPrice,
}}
>
{children}
</CartContext.Provider>
);
};
2. App.tsx — Context Provider 설정
// App.tsx
import { CartProvider } from './contexts/CartContext';
import Header from './components/Header';
import Shop from './components/Shop';
function App() {
return (
<CartProvider>
<div className="App">
<Header />
<Shop />
</div>
</CartProvider>
);
}
export default App;
3. components/Product.tsx — 장바구니에 상품 추가
// components/Product.tsx
import { useCart } from '../contexts/CartContext';
interface ProductProps {
id: number;
name: string;
price: number;
}
const Product = ({ id, name, price }: ProductProps) => {
const { addToCart } = useCart();
const handleAddToCart = () => {
addToCart({ id, name, price });
alert(`${name}이(가) 장바구니에 추가되었습니다.`);
};
return (
<div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px' }}>
<h3>{name}</h3>
<p>₩{price.toLocaleString()}</p>
<button onClick={handleAddToCart}>장바구니에 담기</button>
</div>
);
};
export default Product;
4. components/Shop.tsx
// components/Shop.tsx
import Product from './Product';
const products = [
{ id: 1, name: '노트북', price: 1500000 },
{ id: 2, name: '마우스', price: 30000 },
{ id: 3, name: '키보드', price: 80000 },
];
const Shop = () => {
return (
<div>
<h2>상품 목록</h2>
{products.map(product => (
<Product key={product.id} {...product} />
))}
</div>
);
};
export default Shop;
5. components/Header.tsx — 장바구니 모달 열기
// components/Header.tsx
import { useState } from 'react';
import CartModal from './CartModal';
const Header = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<header style={{ display: 'flex', justifyContent: 'space-between', padding: '10px', background: '#f5f5f5' }}>
<h1>쇼핑몰</h1>
<button onClick={() => setIsModalOpen(true)}>🛒 장바구니 ({useCart().totalItems})</button>
{isModalOpen && <CartModal onClose={() => setIsModalOpen(false)} />}
</header>
);
};
export default Header;
⚠️ useCart()를 여기서 사용하려면 Header도 CartProvider 안에 있어야 합니다. 이미 App.tsx에서 감싸고 있으므로 문제 없음.
6. components/CartModal.tsx
// components/CartModal.tsx
import { useCart } from '../contexts/CartContext';
interface CartModalProps {
onClose: () => void;
}
const CartModal = ({ onClose }: CartModalProps) => {
const { cart, removeFromCart, clearCart, totalPrice } = useCart();
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}>
<div style={{
background: 'white',
padding: '20px',
borderRadius: '8px',
width: '400px',
maxHeight: '80vh',
overflowY: 'auto',
}}>
<h2>장바구니</h2>
{cart.length === 0 ? (
<p>장바구니가 비어있습니다.</p>
) : (
<>
<ul>
{cart.map(item => (
<li key={item.id} style={{ marginBottom: '10px' }}>
{item.name} x {item.quantity} — ₩{(item.price * item.quantity).toLocaleString()}
<button onClick={() => removeFromCart(item.id)} style={{ marginLeft: '10px' }}>삭제</button>
</li>
))}
</ul>
<p><strong>총 금액: ₩{totalPrice.toLocaleString()}</strong></p>
<button onClick={clearCart} style={{ marginRight: '10px' }}>장바구니 비우기</button>
<button onClick={onClose}>닫기</button>
</>
)}
</div>
</div>
);
};
export default CartModal;
7. components/Cart.tsx — 장바구니 표시 (예시용)
// components/Cart.tsx
import { useCart } from '../contexts/CartContext';
const Cart = () => {
const { cart, totalPrice } = useCart();
return (
<div>
<h3>장바구니 요약</h3>
<p>총 상품 수: {cart.reduce((sum, item) => sum + item.quantity, 0)}</p>
<p>총 가격: ₩{totalPrice.toLocaleString()}</p>
</div>
);
};
export default Cart;
핵심 학습 포인트
|
Context 생성
|
createContext<T>()로 타입 지정
|
|
Provider 사용
|
App에서 최상위에CartProvider로 감싸기
|
|
useContext 훅
|
useCart()로 상태와 함수 접근
|
|
상태 업데이트
|
setCart를 통해 불변성 유지하며 상태 변경
|
|
재사용성
|
Product,CartModal등 어디서든 장바구니 상태 사용 가능
|
Omit<T, K>란?
Omit은 TypeScript의 유틸리티 타입(Utility Type) 중 하나로,
기존 타입 T에서 특정 속성 K를 제외한 새 타입을 생성합니다.
Omit<T, K>
- T: 기존 타입 (예: 인터페이스 또는 타입 별칭)
- K: 제외할 속성명 (문자열 리터럴 또는 유니언)
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
// quantity를 제외한 타입
type CartItemWithoutQuantity = Omit<CartItem, 'quantity'>;
// 👇 위와 동일합니다:
// type CartItemWithoutQuantity = {
// id: number;
// name: string;
// price: number;
// };
즉, Omit<CartItem, 'quantity'>는
CartItem에서 quantity 필드만 빼고 나머지 필드만 허용하는 타입입니다.
왜 Omit<CartItem, 'quantity'>를 쓸까요?
→ 사용자(외부)가 quantity를 직접 지정하지 못하도록 제어하기 위함입니다.
예를 들어, 상품을 장바구니에 처음 담을 때, 일반적으로:
- 상품 정보(id, name, price)는 외부에서 제공되고,
- quantity는 자동으로 1로 설정되거나, 기존에 있으면 +1됩니다.
따라서 함수 시그니처에서 quantity를 의도적으로 제외하여:
- ❌ addToCart({ id: 1, name: '노트북', price: 1500000, quantity: 999 }) 같은 잘못된 호출을 방지
- ✅ 사용자가 quantity를 신경 쓸 필요 없이, 로직 내부에서 안전하게 관리 가능
const addToCart = (item: Omit<CartItem, 'quantity'>) => {
setCart(prevCart => {
const existingItem = prevCart.find(i => i.id === item.id);
if (existingItem) {
return prevCart.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...prevCart, { ...item, quantity: 1 }];
});
};
|
Omit<T, K>
|
타입T에서 속성K를 제거한 새 타입 생성
|
|
사용 목적
|
외부 입력에서 불필요하거나 위험한 필드(quantity)를 차단
|
|
장점
|
타입 안정성 ↑, API 설계 명확성 ↑, 실수 방지
|
|
실제 활용
|
addToCart는quantity없이 호출되며, 내부에서 자동 관리
|
첨부하신 이미지를 AI 이미지 생성기에 입력할 수 있도록, 정확하고 상세한 프롬프트를 만들어주세요. 단 배경은 투명하거나 흰색으로 만들어주세요.

반응형
'교육' 카테고리의 다른 글
| react-helmet의 기본 개념과 사용법 (0) | 2025.11.18 |
|---|---|
| 「React 18 + TypeScript + MobX 기반 구조에서 useStore 훅 설계와 스토어 계층 아키텍처 이해」 (0) | 2025.11.17 |
| "TypeScript 제네릭 기반 Form 재사용 패턴: React Hook Form과 함께하는 고급 Form 설계" (0) | 2025.11.17 |
| #007 React useMemo : 성능 최적화의 정석 (0) | 2025.11.13 |
| #006 Context Provider (컨텍스트 프로바이더) (0) | 2025.11.13 |