처음에는, 상태 변경되어야 하는 이벤트가 발생할 때 상태에 대한 정보가 담긴
Action객체가 생성됩니다.
👇
이 Action객체는 Dispatch함수의 인자로 전달됩니다.
👇
Dispatch함수는 Action객체를 다시 Reducer함수로 전달해줍니다.
👇
Reducer함수는 Action객체의 값을 확인하고 그 값에 따라서 전역 상태 저장소 Store의 상태를 변경합니다.
👇
위의 순서가 모두 끝나고 상태가 변경된다면, React는 화면을 다시 렌더링합니다.
export const addToCart = (itemId) => {
return {
type: ADD_TO_CART,
payload: {
quantity: 1,
itemId,
},
};
};
정말정말 단순하게 생각하기 위해서,
위 코드를 해체해봅시다.
itemId를 인자로 받는 addToCart는
{
type: ADD_TO_CART,
payload: {
quantity: 1,
itemId,
}
를 리턴합니다.
이걸 action 객체라고 볼 수 있겠죠!
addToCart는 객체를 생성하는 함수로, 액션생성자
라고 불러줍니다.
payload에는 왜 quantity, itemId를 담았느냐! 라고 한다면,
우리가 상태를 변경할 때 어떤 값들이 필요할까
를 생각해보고 그에 맞는 값을 넣어주면 됩니다.
export default function ShoppingCart() {
const state = useSelector((state) => state.itemReducer);
const { cartItems, items } = state;
const dispatch = useDispatch();
const [checkedItems, setCheckedItems] = useState(
cartItems.map((el) => el.itemId)
);
const handleClick = (item) => {
if (cartItems.map((el) => el.itemId).includes(item.id)) {
dispatch(notify("이미 추가된 상품입니다."));
} else {
dispatch(addToCart(item.id));
dispatch(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`));
}
};
handleClick이라는 함수에 item을 인자로 주고,
조건문으로 원래 있던 카트아이템과 itemId를 비교후 같은 itemId가 있으면,
(장바구니에 내가 새로 추가하려는 아이템이 있으면)
dispatch(notify("~"))로 이미 장바구니에 있으므로 추가할수 없다는 설명을 붙여주고,
없다면 dispatch로 addToCart를 인자로 받고 notify로 추가했다는 설명을 덧붙입니다.
(참고로 notify 함수는 따로 있습니다)
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:
return Object.assign({}, state, {
//Object.assign을 쓰는 이유 : redux의 불변성때문
cartItems: [...state.cartItems, action.payload], //원래있던 아이템목록인 ...state.cartItems에 action.payload를 더한다.
});
break;
//...이하생략
💁🏻♂️ redux의 속성중 하나인 불변성을 지키려면 state를 수정해서는 안되는데,
이를 보통 spread 연산자를 사용하거나 Object.assign을 통해 새로운 객체를 만들어 리턴합니다.
🙆🏻♂️ shallow copy가 아닌 deep copy를 통해서 값 자체의 복사를 나타내 다른 주소값을 지니게 되면서, 불변성을 지키는겁니다!
다시 reducer 함수에 대해 살펴보면, 결국 reducer가 리턴을 해주는 값이 => 새로운 값이 될겁니다.
원래 상태를 state라는 변수로 가져오고, 원래 있던 cartItems에 새로운 값을 덮어씌워주기 위해서 action.payload를 붙여줄겁니다!😤
return Object.assign({}, state, {
cartItems: [...state.cartItems, action.payload],
});
짜잔~ 잠깐! payload는 갑자기 여기서 왜 튀어나왔냐!
{
type: ADD_TO_CART,
payload: {
quantity: 1,
itemId,
}
액션생성자인 addToCart에서 받은 객체 안의 key인 payload를 보면 알 수 있습니다. 우리가 추가하려는 모습을 그대로 간직하고 있는 모습이죠! action객체이니 action.payload값으로 가져오는겁니다.
그렇다면 이제 빠르게
1. 카트 아이템을 삭제하기
2. 카트 수량 조정하기
의 코드도 복기해봅시다!🐳
//index.js
export const removeFromCart = (itemId) => {
return {
type: REMOVE_FROM_CART,
payload: {
itemId,
},
};
};
//ShoppingCart.js
export default function ShoppingCart() {
const state = useSelector((state) => state.itemReducer);
const { cartItems, items } = state;
const dispatch = useDispatch();
const [checkedItems, setCheckedItems] = useState(
cartItems.map((el) => el.itemId)
);
const handleDelete = (itemId) => {
setCheckedItems(checkedItems.filter((el) => el !== itemId));
dispatch(removeFromCart(itemId));
};
}
//itemReducer.js
//switch문은 addToCart에 있는 코드와 동일합니다.
case REMOVE_FROM_CART:
return Object.assign({}, state, {
cartItems: state.cartItems.filter(
(el) => el.itemId !== action.payload.itemId
),
});
break;
//index.js
export const setQuantity = (itemId, quantity) => {
return {
//TODO
type: SET_QUANTITY,
payload: {
quantity,
itemId,
},
};
};
//ShoppingCart.js
export default function ShoppingCart() {
const state = useSelector((state) => state.itemReducer);
const { cartItems, items } = state;
const dispatch = useDispatch();
const [checkedItems, setCheckedItems] = useState(
cartItems.map((el) => el.itemId)
);
const handleQuantityChange = (quantity, itemId) => {
dispatch(setQuantity(itemId, quantity));
};
}
//itemReducer.js
//switch문은 addToCart에 있는 코드와 동일합니다.
case SET_QUANTITY:
let idx = state.cartItems.findIndex(
(el) => el.itemId === action.payload.itemId //여기서 itemId가 같은 인덱스를 찾아놨으니 밑에서 인덱스를 사용해서 map으로 새 배열 만들면 되겠다.
);
return Object.assign({}, state, {
cartItems: state.cartItems.map((el, elidx) => {
return idx === elidx ? action.payload : el;
}),
});
break;
default:
return state;