이 글을 쓰는 날이 오고야 말았다.
잘 할 수 있을지 겁이 나지만.... 뭘 알고 뭘 모르는지 파악하는 차원에서 써보겠다.
음식 주문 앱 페이지인데.. 아마 이걸로 포스팅 세개는 쓸 것 같다. useRef까지 정리해야 하니까.
useReducer로 할 일은 메뉴판에서 add 버튼을 눌렀을 때 오른쪽 상단 카트 아이콘에 음식 수량이 추가되게 하는 것이다.
상당히 간단해 보이지만... 머리를 세번은 뽀개야 겨우 만들 수 있다🥲
useReducer 왜 쓰나?
useReducer 훅은 복잡한 상태 관리를 보다 쉽게 만들어준다.
useState만으로는 상태 관리가 쉽지 않을 때 useReducer를 사용할 수 있다.
특히 Context API와 함께 사용하면 강력한 효과를 낼 수 있다.
useReducer 알아보기
reducer를 사용할 때는 액션 - 디스패치 - reducer - 업데이트 이 네 단계를 기억하면 된다.
일단 useReducer의 생김새를 뜯어보자.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
function reducer(state, action) {
// ...
}
useReducer
useReducer는 세개의 인자를 받을 수 있고, 두개의 요소가 담긴 배열을 리턴한다.
useReducer의 첫번째 인자는 상태를 어떻게 업데이트할지를 결정하는 reducer 함수다.
두번째 인자는 상태의 초기값이고, 세번째 인자는 초기화 함수로서 상태의 초기값을 설정해 준다.
세번째 인자는 없어도 되고, 있으면 init(intialArg)로, 없으면 initialArg로 초기값이 지정된다.
useReducer가 반환한 배열에는 현재 상태와 dispatch 함수가 담겨 있다.
reducer함수
reducer함수는 다시 두 개의 인자를 갖는데, 첫번째는 현재의 상태이고 두번째는 액션이다.
'액션'은 사용자가 어떤 조작을 했는지에 관한 내용이 담겨 있다.
액션은 보통 사용자의 조작 내용을 담은 type이라는 프로퍼티가 담긴 객체로 만들어진다.
예컨대 카운터 애플리케이션에서 더하기 버튼을 누르면 {type: "ADD"}로 액션을 만들어줄 수 있다.
이렇게 만들어진 액션은 dispatch 함수를 통해서 reducer 함수에 전달된다.
그러면 reducer 함수 내에서 액션의 type에 따라 상태를 업데이트하는 방식을 지정해 주고,
상태가 업데이트 되면 리렌더링이 발생한다.
액션-디스패치-reducer-업데이트를 다시 한번 기억하자!!
useReducer 써보기
import CartContext from "./cart-context"; //컨텍스트 임포트
import { useReducer } from "react"; //useReducer 임포트
//상태 초기값을 미리 지정해 준다.
const defaultCartState = {
items: [],
totalAmout: 0,
};
//reducer 함수: 액션 타입에 따라서 상태를 업데이트해준다.
//remove 코드도 들어가야 하지만 아직 미완성이다.
const cartReducer = (state, action) => {
if (action.type === "ADD") {
const updatedItems = state.items.concat(action.itemToAdd);
const updatedAmount =
state.items.length + action.itemToAdd.price * action.itemToAdd.amount;
return { items: updatedItems, amount: updatedAmount }; //새로운 상태 객체를 리턴
}
};
//CartProvider 컴포넌트
const CartProvider = (props) => {
//useReducer의 인자로 reducer 함수와 초기값을 받았고, 상태와 dispatch 함수가 담긴 배열을 리턴
const [cartState, dispatchCartAction] = useReducer(
cartReducer,
defaultCartState
);
//컨텍스트에 넣어 줄 함수를 미리 선언.
//이 컨텍스트를 사용하는 컴포넌트에서 context.addItem으로 dispatch 함수를 호출할 수 있다.
//dispatch 함수에 type과 업데이트할 아이템의 내용을 적어서 보내주면 그에 맞게 상태가 업데이트
const addItemToCartHandler = (item) => {
dispatchCartAction({ type: "ADD", itemToAdd: item });
};
const removeItemFromCartHandler = (id) => {};
//useReducer로 업데이트된 상태를 컨텍스트에 넣는다.
const cartContext = {
items: cartState.items,
totalAmout: cartState.totalAmout,
addItem: addItemToCartHandler,
removeItem: removeItemFromCartHandler,
};
return (
<CartContext.Provider value={cartContext}>
{props.children}
</CartContext.Provider>
);
};
export default CartProvider;
너무나 길고 복잡하지만 하나씩 뜯어보면 재미있다!!(자기최면)
이 코드는 지난번에 만들었던 카트의 상태를 공급해주는 cartProvider에 useReducer를 끼얹어준 것이다.
코드 해석은 주석으로 달았다.
컨텍스트를 사용하는 곳으로 가서 어떻게 상태를 업데이트 시켜주고 있는지도 확인해 보자.
import { useContext } from "react"; //useContext 임포트
import CartContext from "../../../store/cart-context"; //컨텍스트 임포트
const MealItem = (props) => {
const cartCtx = useContext(CartContext); //useContext로 컨텍스트 불러오기
const price = `$${props.price.toFixed(2)}`;
//MealItemForm 컴포넌트에 prop으로 전달해 줄 함수.
//현재의 컴포넌트도 상위 컴포넌트로부터 아이템의 id, 이름, 수량, 가격을 props로 받는다.
const addToCartHandler = (amount) => {
//위 코드에서 addItem에 담았던 dispatch 호출 함수를 여기서 호출한다.
cartCtx.addItem({
item: props.id,
name: props.name,
amount: amount,
price: props.price,
});
};
return (
<div>
//(중략)
<MealItemForm onAddToCart={addToCartHandler} id={props.id} />
</div>
</li>
);
};
export default MealItem;
+)추가
카트에 아이템이 있으면 수량을 늘리고 아이템이 없으면 아이템을 추가하는 Reducer 작업.
너무 어려웠어서 적어 두지 않으면 다 까먹을 것 같다.
const cartReducer = (state, action) => {
if (action.type === "ADD") {
//이미 존재하는 카트 아이템인 경우를 업데이트 하기 위해 먼저 인덱스를 찾는다
const existingCartItemIndex = state.items.findIndex(
(item) => item.id === action.itemToAdd.id
);
//인덱스를 기준으로 아이템도 찾아서 선언해준다.
const existingCartItem = state.items[existingCartItemIndex];
//조건에 따라서 업데이트 아이템을 다르게 만들어 주기 위해 변수 먼저 선언
let updatedItem;
let updatedItems;
if (existingCartItem) {
//아이템이 이미 있다면 업데이트될 아이템은 이미 있는 아이템에 amount만 추가
updatedItem = {
...existingCartItem,
amount: existingCartItem.amount + action.itemToAdd.amount,
};
//이미 있는 state.items 배열을 변경하면 안되므로 복사해주고
updatedItems = [...state.items];
//여기에서 새로운 항목으로 교체(이 부분이 가장 어려웠다!!)
updatedItems[existingCartItemIndex] = updatedItem;
} else updatedItems = state.items.concat(action.itemToAdd);
const updatedAmount =
state.totalAmount + action.itemToAdd.price * action.itemToAdd.amount;
return { items: updatedItems, totalAmount: updatedAmount };
}
};
이렇게 useReducer와 Context API의 콜라보로 즐거운 머리 뽀개기 시간을 가져보았다.
다음 포스팅에서는 useRef로 또 머리를 뽀개 보자🥲❤️ 리액트 사랑혀..!!!
'React' 카테고리의 다른 글
[Redux] 리덕스 기초 (1) | 2023.04.13 |
---|---|
[React] useRef (0) | 2023.04.12 |
[React] Context API (0) | 2023.04.06 |
[React] useEffect 사용하기 (0) | 2023.03.31 |
[React] state, props (3) | 2023.03.26 |