간단한 웹 프로젝트, 백엔드로 부터 데이터를 받아와서 아이템 리스트를
프론트에 뿌리고 유저가 선택한 아이템을 카트에 담았다가
구매를 누르면 백엔드에서 해당 목록을 받을 수 있는 프로젝트 중
헷갈려서 기록해놔야 할 것들을 정리해보자.
1. createContext / useContext
웹 빌드를 하다보면 state를 공유해서 써야할때가 많다. 예를들면 유저가 버튼을 통해서 장바구니에 담았을때
장바구니 속 선택된 Item들이 공유가 되어야한다. 또는 모달과 같은 컴포넌트에 유저가 선택한 목록이 공유가 될 때 등,
이런 경우 전체 컴포넌트를 한번에 관리할 수 있도록 createContext를 통해서 만들고 useContext를 이용해서 사용할 수 있다.
그래서 보통 store라는 폴더를 통해 관리하고, createContext는 useReducer와 함께 사용하는데 그 이유는 use Reducer가 지나치게 길어진느 useState를 방지해주기 때문이다. 아래는 예시 페이지이다.
import { createContext, useReducer } from "react";
const CartContext = createContext({
items: [],
addItem: (item) => {},
removeItem: (id) => {},
});
// state에 값을 직접넣으면 해당 값이 중복으로 들어가게 되어서
// items.findIndex()를 사용하여 중복된 값이 있는지 확인하고 중복된 값이 있으면
// 해당 값의 index를 반환해서 해당 index의 값을 업데이트하도록 한다.
function cartReducer(state, action) {
if (action.type === "ADD_ITEM") {
const existingItemIndex = state.items.findIndex((item) => item.id === action.item.id);
const updatedItems = [...state.items];
if (existingItemIndex > -1) {
const existingItem = state.items[existingItemIndex];
const updatedItem = {
...existingItem,
quantity: existingItem.quantity + 1,
};
updatedItems[existingItemIndex] = updatedItem;
} else {
updatedItems.push({
...action.item,
quantity: 1,
});
}
return { ...state, items: updatedItems };
}
if (action.type === "REMOVE_ITEM") {
const existingItemIndex = state.items.findIndex((item) => item.id === action.id);
const existingItem = state.items[existingItemIndex];
const updatedItems = [...state.items];
if (existingItem.quantity === 1) {
updatedItems.splice(existingItemIndex, 1);
} else {
const updatedItem = { ...existingItem, quantity: existingItem.quantity - 1 };
updatedItems[existingItemIndex] = updatedItem;
}
return { ...state, items: updatedItems };
}
return state;
}
export function CartContextProvider({ children }) {
const [cart, dispatchCartAction] = useReducer(cartReducer, { items: [] });
function addItem(item) {
dispatchCartAction({ type: "ADD_ITEM", item });
}
function removeItem(id) {
dispatchCartAction({ type: "REMOVE_ITEM", id });
}
const CartContextValue = {
items: cart.items,
addItem,
removeItem,
};
return <CartContext.Provider value={CartContextValue}>{children}</CartContext.Provider>;
}
export default CartContext;
그리고 Provider를 통해서 공유해서 사용하는데 사용할 컴포넌트들을 감싸주면 된다. 아래는 예시이다.
import Header from "./components/Header";
import Meals from "./components/Meals";
import Cart from "./components/Cart";
import Checkout from "./components/Checkout";
import { CartContextProvider } from "./store/CartContext";
import { UserProgressContextProvider } from "./store/UserProgressContext";
function App() {
return (
<UserProgressContextProvider>
<CartContextProvider>
<Header />
<Meals />
<Cart />
<Checkout />
</CartContextProvider>
</UserProgressContextProvider>
);
}
export default App;
context를 사용할때는 원하는 변수에 Context를 가져와 쓰면 된다.
현재 만들어둔 context에 add 와 remove 기능이 있기때문에 아래와 같이 사용할 수 있다.
import { useContext } from "react";
import { currencyFormatter } from "../utils/formatting";
import Button from "./UI/Button";
import CartContext from "../store/CartContext";
export default function MealItem({ meal }) {
const cartCtx = useContext(CartContext);
function addToCartHandler() {
cartCtx.addItem(meal);
}
return (
<li className="meal-item">
<article>
<img src={`http://localhost:3000/${meal.image}`} alt={meal.name} />
<div>
<h3>{meal.name}</h3>
<p className="meal-item-price">{currencyFormatter.format(meal.price)}</p>
<p className="meal-item-description">{meal.description}</p>
</div>
<p className="meal-item-actions">
<Button onClick={addToCartHandler}>Add to Cart</Button>
</p>
</article>
</li>
);
}
버튼을 통해 선택된 애들이 store된다.
현재 프로젝트는 카트 자체가 modal 형태로 되어있어서 모달을 통해 카트 상황이 유저에게 보여지는데
모달 자체도 현재 userProgressContext로 관리된다.
해당 함수에는 유저가 모달을 열고 닫고 등의 행위를 관리하고 있다.
페이지 이동이 아닌 모달이기 때문에 유저가 카트를 눌렀을 때 카트 모달이 뜨고, 구매를 눌렀을때는
기존 모달이 닫히고 구매관련 모달이 나오게 셋팅되어있다.
아래는 context의 예시이다.
import { createContext, useState } from "react";
const UserProgressContext = createContext({
progress: "",
showCart: () => {},
hideCart: () => {},
showCheckout: () => {},
hideCheckout: () => {},
});
export function UserProgressContextProvider({ children }) {
const [userProgress, setUserProgress] = useState();
function showCart() {
setUserProgress("cart");
}
function hideCart() {
setUserProgress("");
}
function showCheckout() {
setUserProgress("checkout");
}
function hideCheckout() {
setUserProgress("");
}
const userProgressCtx = {
progress: userProgress,
showCart,
hideCart,
showCheckout,
hideCheckout,
};
return (
<UserProgressContext.Provider value={userProgressCtx}>{children}</UserProgressContext.Provider>
);
}
export default UserProgressContext;
해당 프로젝트는 많은 기능을 연습해보기 위함이었기 때문에 비교적 불필요한 것들이 있지만
기능을 전부 사용해보기에는 적절해 보인다.
아래의 예시를 보면 모달과 구매, 아이템 갯수 추가 제거 등 두가지 context를 한번에 사용한 것을 확인할 수 있다.
import { useContext } from "react";
import CartContext from "../store/CartContext";
import UserProgressContext from "../store/UserProgressContext";
import { currencyFormatter } from "../utils/formatting";
import Button from "./UI/Button";
import Modal from "./Modal";
import CartItem from "./CartItem";
export default function Cart() {
const cartCtx = useContext(CartContext);
const userProgressCtx = useContext(UserProgressContext);
const cartTotal = cartCtx.items.reduce((acc, item) => acc + item.price * item.quantity, 0);
function handleCloseCart() {
userProgressCtx.hideCart();
}
function handleCheckout() {
userProgressCtx.showCheckout();
}
return (
<Modal
className="cart"
open={userProgressCtx.progress === "cart"}
onClose={userProgressCtx.progress === "cart" ? handleCloseCart : null}>
<h2>Your Cart</h2>
{cartCtx.items.length === 0 ? (
<p className="cart-empty">Your cart is empty</p>
) : (
<ul>
{cartCtx.items.map((item) => (
<CartItem
key={item.id}
name={item.name}
quantity={item.quantity}
price={item.price}
increase={() => cartCtx.addItem(item)}
decrease={() => cartCtx.removeItem(item.id)}
/>
))}
</ul>
)}
<p className="cart-total">{currencyFormatter.format(cartTotal)}</p>
<p className="modal-actions">
<Button textOnly onClick={handleCloseCart}>
Close
</Button>
{cartCtx.items.length > 0 && (
<Button type="button" onClick={handleCheckout}>
Go to Checkout
</Button>
)}
</p>
</Modal>
);
}
모달 페이지에서는 CreatePortal을 이용하여 모달이 html의 원하는 위치로 옮겨갈 수 있도록 하였고, useRef를 이용해서
모델의 현재 상태(open, close) 를 쉐어 할 수 있게 했고 *이 부분의 이해가 좀 부족함 //
스타일링을 할 수 있게 className도 probs로 제공하고 있다.
아래는 예시코드이다.
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
export default function Modal({ children, open, onClose, className = "" }) {
const dialog = useRef();
useEffect(() => {
const modal = dialog.current;
if (open) {
modal.showModal();
}
return () => modal.close();
}, [open]);
return createPortal(
<dialog ref={dialog} className={`modal ${className}`} onClose={onClose}>
{children}
</dialog>,
document.getElementById("modal")
);
}
위의 포탈을 사용하는 방법은 html 코드로 직접가서 모달의 위치를 정할 수 있다.
아래와 같이 모달의 위치를 정해놓고 위의 코드에서 documen.getElementById를 통해서 원하는 위치로 설정 할 수있다.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.jpg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React FoodOrder</title>
</head>
<body>
<div id="modal"></div>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
'Web & App > Frontend Study' 카테고리의 다른 글
| React _ 미니프로젝트#3 마무리 (0) | 2025.01.05 |
|---|---|
| React _미니프로젝트 기록2 (0) | 2025.01.04 |
| React _ Clean up 처리 (1) | 2025.01.03 |
| React #14 _ 다시 한번 useContext (0) | 2025.01.03 |
| React #13 _ 유효성 검사 (1) | 2025.01.03 |