먼저 장바구니에서 구현하고자 하는 기능은 이렇다.
- 아이템 항목에서 추가 기능
- 장바구니에서 아이템 수량 조절
- 장바구니 토글
- 장바구니 실시간 저장 + 불러오기
아이템 항목에서 추가 기능
const cartSlice = createSlice({
name: "cart",
initialState: { item: [], totalQuantity: 0, cartChange: false },
reducers: {
addCartItem: (state, action) => {
const newItem = action.payload;
const existingItem = state.item.find((item) => item.id === newItem.id);
if (!existingItem) {
state.item.push({
id: newItem.id,
title: newItem.title,
price: newItem.price,
description: newItem.description,
totalPrice: newItem.price,
quantity: 1,
});
} else {
existingItem.totalPrice = existingItem.totalPrice + existingItem.price;
existingItem.quantity++;
}
state.totalQuantity++;
state.cartChange = true;
},
},
});
export const cartActions = cartSlice.actions;
export default cartSlice;
먼저 store를 만들어서 cart 이름의 slice를 만든다.
초기 상태로는
item 빈 배열
총 수량 0개
cartChange(이따 data fetch할 때 사용)
의 값을 넣어준다.
addCartItem의 구조는 이렇다.
항목 추가를 하는 아이템을 action.payload로 받는다. 그리고 그 데이터를 find를 통해 겹치는 게 있는지 확인하고, 만약 겹치는 항목이 없다면 새로운 object를 item 배열에 push를 해준다. 겹치는 항목이 있다면 해당하는 항목의 수량과 가격을 올린다.
cart 안에 있는 애들을 useSelector로 컴포넌트에 넣어주고 useDispatch로 addCartItem을 실행하면 아래와 같이 실행된다.
장바구니에서 아이템 수량 조절
수량 추가하는 건 위에서 했으니 수량을 줄여주는 reducer만 만들면 된다.
removeCartItem: (state, action) => {
const currentItem = action.payload;
const targetItem = state.item.find((item) => item.id === currentItem.id);
if (targetItem.quantity === 1) {
targetItem.quantity--;
state.item = state.item.filter((item) => item.quantity !== 0);
}
state.totalQuantity--;
targetItem.quantity--;
targetItem.totalPrice -= targetItem.price;
state.cartChange = true;
},
사실상 거의 똑같은 구조다.
action.payload로 수량을 조절할 애를 골라주고 이미 있으면 수량을 줄여준다.
다만 수량이 1개일 때 또 마이너스를 하면 0이 된다. 그 때는 조금 다르게 처리를 해주었다.
먼저 수량을 0으로 만들어 준 다음,
전체 item 배열을 filter를 돌려서 0이 아닌 애들만 남긴다.
결국, 수량이 0인 항목은 삭제되는 것이다.
마찬가지로 useDispatch로 removeCartItem을 불러오면 아래와 같이 잘 실행되는 것을 확인할 수 있다.
-장바구니 토글
거두절미하고 바로 코드로 가보자.
const uiSlice = createSlice({
name: "UI",
initialState: { show: false, notification: null },
reducers: {
toggle(state) {
state.show = !state.show;
},
},
});
export const uiActions = uiSlice.actions;
export default uiSlice;
uiSlice라는 새로운 slice를 생성해주었고 show라는 변수에 따라 cart를 보여줄지 말지 정해줄 것이다.
toggle은 그냥 show의 상태만 반대로 해주는 걸로 reducer를 생성했다.
다른 컴포넌트에서 toggle을 실행해주고
이렇게 cartShow를 받아서 true일 때만 보이게 해주었다.
const cartShow = useSelector((state) => state.ui.show);
<Layout>
{cartShow && <Cart />}
<Products />
</Layout>
구현한 모습은 아래와 같다.
-장바구니 실시간 저장 + 불러오기
이 부분이 가장 좀 애먹었던 부분이다.
먼저 저장하는 부분부터 보자.
일단 redux 데이터를 가져와야 하는데 slice 안에서는 절대 그 데이터를 직접 조작하면 안 된다.
그래서 데이터를 조작하는 함수들을 담은 파일을 새로 만들어주었다.
cart-fetch.js (fetch하는 함수들 따로 정리)
export const updateCart = (cart) => {
return async (dispatch) => {
const sendRequest = async () => {
const response = await fetch(
// firebase 주소
{
method: "PUT",
body: JSON.stringify({
item: cart.item,
totalQuantity: cart.totalQuantity,
}),
}
);
if (!response.ok) {
alert("Error occured");
}
};
try {
await sendRequest();
} catch (error) {
console.log(error);
}
};
};
fetch를 비동기로 처리하기 위해 updateCart를 만들었다.
여기서 보면 return 값이 async 함수다.
이 개념을 redux-thunk라고 한다.
redux의 값을 조작하는 방법에는 reducer를 통해 값을 변화시켜주는 것이 있다.
하지만 reducer는 반드시 새로운 state를 결과로 내야 한다.
그렇기에 redux의 값을 더 다이나믹하게 조작하기 위해서는 reducer로는 한계가 있다.
예를 들어 reducer에서는 setTimeout과 같은 함수를 할 수 없는 것이다. return 값이 어떤 state가 아니기 때문.
그래서 아예 다른 파일로 함수를 return 해주는 애들을 모아놓고 컴포넌트 파일에 가서 redux 값을 더 다이나믹하게 다루는 방식을 redux-thunk라고 한다.
그럼 여기서 어떻게 redux-thunk를 활용했는지 한 번 보자.
App.js
let isInitial = true;
function App() {
const cart = useSelector((state) => state.cart);
useEffect(() => {
if (isInitial) {
isInitial = false;
return;
}
if (cartChange) {
dispatch(updateCart(cart));
}
}, [cart, dispatch, cartChange]);
}
export default App;
cart 전체를 불러온 다음 (item, totalQuantity 포함)
useEffect로 cart가 바뀌면 updateCart를 실행한다.
예를 들어 addCartItem을 실행하면 cart 변수에 item이 추가된다.
그러면 cart가 바뀌었기 때문에 updateCart를 추가된 cart에 대해서 실행한다.
그렇게 새로운 cart는 firebase로 올라가게 된다.
아래와 같이 Harry Potter 항목을 5개로 만들어주고 firebase로 가보면
이렇게 DB에 cart가 업데이트 되어있는 것을 확인할 수 있다.
이와 같은 방식으로 장바구니 불러오는 기능도 만들었다.
export const readCart = () => {
return async (dispatch) => {
const fetchCart = async () => {
const response = await fetch(
//firebase 주소
);
const existingCart = await response.json();
dispatch(
cartActions.replaceCartItem({
item: existingCart.item || [],
totalQuantity: existingCart.totalQuantity,
})
);
};
try {
await fetchCart();
} catch (error) {
console.log(error);
}
};
};
//Cart Slice
replaceCartItem: (state, action) => {
state.item = action.payload.item;
state.totalQuantity = action.payload.totalQuantity;
},
//App.js
useEffect(() => {
dispatch(readCart());
}, [dispatch]);
App.js가 렌더링되자마자 firebase에 있는 cart를 fetch해서 replaceCartItem을 실행해준다.
cart slice에서는 initialstate로 다 비워줬지만 페이지를 처음 렌더링할 때 바로 fetch를 해도록 해준 것이다.
그리고 item과 totalQuantity를 replace해주고 없으면 빈 배열 할당.
이렇게 cart에 아이템을 추가해주고 새로고침을 해도 그 전에 저장되어있던 값이 그대로 출력된다.
배운 점
- redux의 값과 reducer를 불러오는 방법 복습
- reducer에서 state에 따라 좀 더 구체적인 조건을 설정하는 로직
- redux의 값을 가지고 다른 함수를 실행하는 redux-thunk의 구조
- firebase REST API 조금 더 익숙해짐
- REST API에서 async await의 플로우 이해도 조금 더 올라감
'웹 > Redux' 카테고리의 다른 글
Redux - store 만들어서 react에 적용시켜보기 (0) | 2022.08.01 |
---|