npm install @reduxjs/toolkit
npm install redux react-redux
설치가 끝난 후 store 폴더를 만들고 폴더 안에 index.js와
장바구니 관리용 슬라이스 장바구니 토글링 슬라이스 같은 사용자 인터페이스 로직용 슬라이스 두개로 나누겠습니다!
toggle버튼을 만들어 Shpping Cart 창을 toggle 해보겠습니다.
import { createSlice } from "@reduxjs/toolkit";
const uiSlice = createSlice({
name: "ui",
initialState: { cartIsVisible: false },
reducers: {
toggle(state) {
state.cartIsVisible = !state.cartIsVisible;
},
},
});
export const uiActions = uiSlice.actions;
export default uiSlice;
다음 저장소인 index.js를 만들어보겠습니다.
import { configureStore } from "@reduxjs/toolkit";
import uiSlice from "./ui-slice";
const store = configureStore({
reducer: { ui: uiSlice.reducer },
});
export default store;
전체 애플리케이션에 리덕스 스토어를 제공하는 방법은
루트 컴포넌트인 index.js 에서 설정할 수 있습니다.
빨강색 네모 박스 부분을 설정해줘야합니다.
자 이제 cart버튼이 있는 cartButton.js 컴포넌트로 가보면
onClick 메서드를 만들어주고 이것을 실행하기위해 dispatch를 해줘야합니다. 그리고 ui-slice에 있는 toggle메서드를 불러와서 실행해줍니다.
이것을 실행하기 위해서는 App.js 컴포넌트에서 데이터를 추출해야 합니다.
데이터 추출을 위해 useSelector를 이요하고 state.ui.cartIsVisible은
index.js에 있는 ui이름과 ui-slice.js에 있는 관심있는 프로퍼티 이름인 cartIsVisible을 사용해야한다.
toggle 버튼이 잘 되는 것을 볼 수 있다.
이제 장바구니의 안의 내용과 Add to Cart 버튼 그리고 MyCart버튼의 개수가 달라지는 작업을 수행해야합니다.
cart-slice.js를 살펴보겠습니다.
import { createSlice } from "@reduxjs/toolkit";
createSlice({
name: "cart",
initialState: {
items: [],
totaslQuantity: 0, //수량의 합
},
reducers: {
addItemToCart(state, action) {
// dispatch될 떄 추가정보를 dispatch해야하기 때문에 actions이 필요하다
const newItem = action.payload;
const existingItem = state.items.find((item) => item.id === newItem.id); // 이미 항목이 존재하는지 파악
if (!existingItem) {
state.items.push({
itemId: newItem.id,
price: newItem.price,
quantity: 1,
totalPrice: newItem.price,
name: newItem.title,
}); //리더스 툴킷이 자동으로 내부적으로 조작하지 않게 해서 push 사용 가능
} else {
//항목이 존재하는 경우
existingItem.quantity++;
existingItem.totalPrice = totalPrice + newItem.price;
}
},
removeItemFromCart() {},
},
});
이상으로 addItemToCart를 알아보았습니다.
이제 장바구니의 항목을 제거하려면 removeItemFormCart 상태와 작업이 필요합니다.
import { createSlice } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: {
items: [],
totaslQuantity: 0, //수량의 합
},
reducers: {
addItemToCart(state, action) {
// dispatch될 떄 추가정보를 dispatch해야하기 때문에 actions이 필요하다
const newItem = action.payload;
const existingItem = state.items.find((item) => item.id === newItem.id); // 이미 항목이 존재하는지 파악
if (!existingItem) {
state.items.push({
itemId: newItem.id,
price: newItem.price,
quantity: 1,
totalPrice: newItem.price,
name: newItem.title,
}); //리더스 툴킷이 자동으로 내부적으로 조작하지 않게 해서 push 사용 가능
} else {
//항목이 존재하는 경우
existingItem.quantity++;
existingItem.totalPrice = totalPrice + newItem.price;
}
},
removeItemFromCart(state, action) {
const id = action.payload;
const existingItem = state.items.find((item) => item.id === id);
if (existingItem.quantity === 1) {
//1과 같으면 배열에서 항목을 완전히 제거한다.
state.items = state.itmes.filter((item) => item.id !== id);
} else {
//1보다 크면 단지 항목의 숫자를 하나 줄이면 된다.
existingItem.quantity--;
existingItem.totalPrice = existingItem.totalPrice - existingItem.price;
}
},
},
});
export const cartActions = cartSlice.actions;
export default cartSlice;
그리고 store/index.js에서 새로운 슬라이스를 전체 리덕스 스토어에 병합해 보겠습니다.
import cartSlice from "./cart-slice";
const store = configureStore({
reducer: { ui: uiSlice.reducer, cart: cartSlice.reducer },
});
cart를 추가해줍니다
자 그리고 이제 Add Cart를 눌렀을 떄 바뀌려면 실제 제품이 필요하기 떄문에 Product.js로 가서 DUMMY_DATA를 만들겠습니다.
const DUMMY_PRODUCTS = [
{
id: "p1",
price: 6,
title: "My First Book",
description: "The first Book I ever wrote",
},
{
id: "p2",
price: 5,
title: "My Second Book",
description: "The secpmd Book I ever wrote",
},
];
더미 데이터를 만들고
<ul>
{DUMMY_PRODUCTS.map((product) => (
<ProductItem
key={product.id}
title={product.title}
price={product.price}
description={product.description}
/>
))}
</ul>
동적으로 보여지게 합니다.
그리고 나서 ProductItem.js에 가서 Add to Cart 버튼을 만들어보겠습니다. 이것은 cart-slice.js의 addItemToCart함수에 연결하고 싶습니다.
이제 데이터를 업데트할 CartButton.js를 보겠습니다.
장바구니의 숫자가 바뀌는 것을 알 수 있습니다.
이제 장바구니를 올바르게 렌더링 해보기 위해 Cart.js로 가보겠습니다.
기존에 있던 항목 추가는 숫자로 잘 나타나는 것을 확인할 수 있습니다.
이제 CartItem의 더하기와 뺴기 버튼 기능을 할 차례입니다!
이를 위해 CartItem.js에서 + - 버튼을 디스패치 해야합니다!
작동이 잘 되는 것을 알 수 있습니다.
이제 장바구니를 보낼 수 있는 서버인
백엔드를 추가해볼까 합니다.
Firebase를 이용할 것인데, 하지만 리덕스는 동기식이기 때문에 비동기식인 fetch()를 사용할 수 없습니다.
이것을 사용하기 위해서는 두 가지 옵션이 있는데 살펴보겠습니다.
컴포넌트 내부 효과인 비동기 코드를 실행하는 것부터 시작하겠습니다.
App.js
Network
Add to Cart를 누를 떄 마다 네트워크에 json형태로 생기는 것을 볼 수 있다.
이제 성공과 여부를 알려주는 알림 컴포넌트를 UI 폴더에 Notification.js를 만들겠습니다.
import { Fragment, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
import { uiActions } from "./store/ui-slice";
import Notification from "./components/UI/Notification";
let isInitial = true;
function App() {
const dispatch = useDispatch();
const showCart = useSelector((state) => state.ui.cartIsVisible);
const cart = useSelector((state) => state.cart);
const notification = useSelector((state) => state.ui.notification);
useEffect(() => {
const sendCartData = async () => {
dispatch(
uiActions.showNotification({
status: "pending",
title: "Sending...",
message: "Sending cart data!",
})
);
const response = await fetch(
"https://react-http-ecb71-default-rtdb.firebaseio.com/cart.json",
{
method: "PUT",
body: JSON.stringify(cart),
}
);
if (!response.ok) {
throw new Error("Sending cart data failed.");
}
dispatch(
uiActions.showNotification({
status: "success",
title: "Success!",
message: "Sent cart data successfully!",
})
);
};
if (isInitial) {
isInitial = false;
return;
}
sendCartData().catch((error) => {
dispatch(
uiActions.showNotification({
status: "error",
title: "Error!",
message: "Sending cart data failed!",
})
);
});
}, [cart, dispatch]);
return (
<Fragment>
{notification && (
<Notification
status={notification.status}
title={notification.title}
message={notification.message}
/>
)}
<Layout>
{showCart && <Cart />}
<Products />
</Layout>
</Fragment>
);
}
export default App;
import { createSlice } from "@reduxjs/toolkit";
const uiSlice = createSlice({
name: "ui",
initialState: { cartIsVisible: false, notification: null },
reducers: {
toggle(state) {
state.cartIsVisible = !state.cartIsVisible;
},
showNotification(state, action) {
state.notification = {
status: action.payload.status,
title: action.payload.title,
message: action.payload.message,
};
},
},
});
export const uiActions = uiSlice.actions;
export default uiSlice;
Add to cart를 누르면 상단에 sucess할 떄 메시지가 잘 나타납니다.
잘못된 fetch 주소를 보내면 error 문구가 뜨는 것을 볼 수 있습니다.
다음으로 데이터를 fetch 하기 전에 모든 부작용 논리를 컴포넌트에 넣는 대안을 살펴보겠습니다.
import { createSlice } from "@reduxjs/toolkit";
import { uiActions } from "./ui-slice";
const cartSlice = createSlice({
name: "cart",
initialState: {
items: [],
totalQuantity: 0,
},
reducers: {
replaceCart(state, action) {
state.totalQuantity = action.payload.totalQuantity;
state.items = action.payload.items;
},
addItemToCart(state, action) {
const newItem = action.payload;
const existingItem = state.items.find((item) => item.id === newItem.id);
state.totalQuantity++;
if (!existingItem) {
state.items.push({
id: newItem.id,
price: newItem.price,
quantity: 1,
totalPrice: newItem.price,
name: newItem.title,
});
} else {
existingItem.quantity++;
existingItem.totalPrice = existingItem.totalPrice + newItem.price;
}
},
removeItemFromCart(state, action) {
const id = action.payload;
const existingItem = state.items.find((item) => item.id === id);
state.totalQuantity--;
if (existingItem.quantity === 1) {
state.items = state.items.filter((item) => item.id !== id);
} else {
existingItem.quantity--;
}
},
},
});
export const sendCartData = (cart) => {
return async (dispatch) => {
dispatch(
uiActions.showNotification({
status: "pending",
title: "Sending...",
message: "Sending cart data!",
})
);
const sendRequest = async () => {
const response = await fetch(
"https://react-http-ecb71-default-rtdb.firebaseio.com/cart.json",
{
method: "PUT",
body: JSON.stringify(cart),
}
);
if (!response.ok) {
throw new Error("Sending cart data failed.");
}
};
try {
await sendRequest();
dispatch(
uiActions.showNotification({
status: "success",
title: "Success!",
message: "Sent cart data successfully!",
})
);
} catch (error) {
dispatch(
uiActions.showNotification({
status: "error",
title: "Error!",
message: "Sending cart data failed!",
})
);
}
};
};
export const cartActions = cartSlice.actions;
export default cartSlice;
import { Fragment, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
import Notification from "./components/UI/Notification";
import { sendCartData } from "./store/cart-slice";
let isInitial = true;
function App() {
const dispatch = useDispatch();
const showCart = useSelector((state) => state.ui.cartIsVisible);
const cart = useSelector((state) => state.cart);
const notification = useSelector((state) => state.ui.notification);
useEffect(() => {
if (isInitial) {
isInitial = false;
return;
}
dispatch(sendCartData(cart));
}, [cart, dispatch]);
return (
<Fragment>
{notification && (
<Notification
status={notification.status}
title={notification.title}
message={notification.message}
/>
)}
<Layout>
{showCart && <Cart />}
<Products />
</Layout>
</Fragment>
);
}
export default App;
이렇게 해도 위와 같은 결과를 가지고올 수 있습니다.