react, redux-toolkit를 이용한
쇼핑몰(장바구니) 사이트 예제입니다.
쇼핑몰(장바구니) 사이트 예제입니다.
예제는 유투버, chaoo charles님의 강의를 들으면서 공부한 내용입니다.
구현 화면
메인 화면
장바구니 페이지
구현 내용
react, hooks
css 파일 코드는 중요하지 않아 불포함
redux-toolkit
react-toastify
화면: 전체 아이템 리스트 화면, 장바구니 상세 화면
설치 패키지
1. npx create-react-app 앱이름(version 5.2.0)
2. npm install redux react-redux @reduxjs/toolkit
3. npm install axios
4. npm install react-router-dom
5. npm install react-toastify
폴더 구조
App.js
import "./App.css";
import "react-toastify/dist/ReactToastify.css";
import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";
//toast 메시지 기능 추가
import { ToastContainer } from "react-toastify";
import NavBar from "./components/NavBar";
import Cart from "./components/Cart";
import Home from "./components/Home";
import NotFound from "./components/NotFound";
function App() {
return (
<div className="App">
<BrowserRouter>
<ToastContainer />
<NavBar />
<Switch>
<Route path="/cart" component={Cart} />
<Route path="/" exact component={Home} />
<Route path="/not-found" component={NotFound} />
<Redirect to="/not-found" component={NotFound} />
</Switch>
</BrowserRouter>
</div>
);
}
export default App;
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { configureStore } from "@reduxjs/toolkit"; //1. configureStore 로 store 생성
import { Provider } from "react-redux"; //2. Provider 감싸주기
import productsReducer, { productsFetch } from "./features/productsSlice";
import { productsApi } from "./features/productsApi";
import cartReducer, { getTotals } from "./features/cartSlice";
//configureStore의 2가지 역할
//a. 각각의 reducer들을 combine
//b. redux devtools 자동 생성
const store = configureStore({
reducer: {
products: productsReducer, //3. productsReducer 적용
cart: cartReducer, //cartSlice에서 import 할때 cartReducer로 이름 변경 - reducer 이므로
[productsApi.reducerPath]: productsApi.reducer, //RTK Query
},
//RTK Query
middleware: (getDefaultMiddleware) => {
return getDefaultMiddleware().concat(productsApi.middleware);
},
});
store.dispatch(productsFetch());
store.dispatch(getTotals());
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
features > productSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
const initialState = {
items: [],
status: null,
};
//productsFetch -> action creator
//createAsyncThunk(action type, payload creator -> function, options)
export const productsFetch = createAsyncThunk(
"products/productsFetch",
async () => {
const response = await axios.get("http://localhost:5000/products"); //get(end point)
return response?.data; //?를 넣으줌으로 data property 없을 경우 에러 대비
}
);
//reducers와 extraReducers의 차이점
//reducers: action creator 생성과 action type을 핸들링 할때 사용
//extraReducers: action type만 핸들링 함
//이미 action creator가 있다면 extraReducer사용
const productsSlice = createSlice({
//createSlice로 action, reducer 세팅
name: "products",
initialState,
reducers: {},
extraReducers: {
[productsFetch.pending]: (state, action) => {
state.status = "pending";
},
[productsFetch.fulfilled]: (state, action) => {
state.status = "success";
state.items = action.payload;
},
[productsFetch.rejected]: (state, action) => {
state.status = "rejected";
},
},
});
export default productsSlice.reducer; //reducer export
features > cartSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { toast } from "react-toastify";
const initialState = {
//cartItems: [],
cartItems: localStorage.getItem("cartItems") //localstorage에 저장된 것이 있으면 사용, 없으면 빈 배열
? JSON.parse(localStorage.getItem("cartItems"))
: [],
cartTotalQuantity: 0,
cartTotalAmount: 0,
};
const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
addToCart(state, action) {
//장바구니 추가하기
//findIndex 만족하는 배열의 첫 번째 요소에 대한 인덱스를 반환, 만족하는 요소가 없으면 -1을 반환합니다.
const itemIndex = state.cartItems.findIndex(
(item) => item.id === action.payload.id
);
if (itemIndex >= 0) {
//이미 장바구니에 있다면
state.cartItems[itemIndex].cartQuantity += 1;
toast.info(`increased ${state.cartItems[itemIndex].name} quantity`, {
position: "bottom-left",
});
} else {
//장바구니에 없다면
const tempProduct = { ...action.payload, cartQuantity: 1 };
state.cartItems.push(tempProduct);
toast.success(`${action.payload.name} added to cart`, {
position: "bottom-left",
});
}
localStorage.setItem("cartItems", JSON.stringify(state.cartItems));
},
removeFromCart(state, action) {
//장바구니 삭제하기
const nextCartItems = state.cartItems.filter(
(cartItem) => cartItem.id !== action.payload.id
);
state.cartItems = nextCartItems;
localStorage.setItem("cartItems", JSON.stringify(state.cartItems));
toast.error(`${action.payload.name} removed from cart`, {
position: "bottom-left",
});
},
decreaseCart(state, action) {
//장바구니 아이템 개수 감소
const itemIndex = state.cartItems.findIndex(
(cartItem) => cartItem.id === action.payload.id
);
if (state.cartItems[itemIndex].cartQuantity > 1) {
state.cartItems[itemIndex].cartQuantity -= 1;
toast.info(`Decreased ${action.payload.name} cart quantity`, {
position: "bottom-left",
});
} else if (state.cartItems[itemIndex].cartQuantity === 1) {
const nextCartItems = state.cartItems.filter(
(cartItem) => cartItem.id !== action.payload.id
);
state.cartItems = nextCartItems;
toast.error(`${action.payload.name} removed from cart`, {
position: "bottom-left",
});
}
localStorage.setItem("cartItems", JSON.stringify(state.cartItems));
},
clearCart(state, action) {
state.cartItems = [];
toast.error(`Cart cleared`, {
position: "bottom-left",
});
localStorage.setItem("cartItems", JSON.stringify(state.cartItems));
},
getTotals(state, action) {
//reduce안의 (callback 함수, {} type의 initial value)
let { total, quantity } = state.cartItems.reduce(
(cartTotal, cartItem) => {
//cardTotal: accumulator, cartItem: currentValue
const { price, cartQuantity } = cartItem;
const itemTotal = price * cartQuantity;
cartTotal.total += itemTotal;
cartTotal.quantity += cartQuantity;
return cartTotal;
},
{
total: 0, //subtotal default
quantity: 0,
}
);
total = parseFloat(total.toFixed(2));
state.cartTotalQuantity = quantity;
state.cartTotalAmount = total;
},
},
});
export const { addToCart, removeFromCart, decreaseCart, clearCart, getTotals } =
cartSlice.actions;
export default cartSlice.reducer;
features > productsApi.js
RTK Query 사용
//RTK Query
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const productsApi = createApi({
reducerPath: "productsApi",
baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:5000" }),
endpoints: (builder) => ({
getAllProducts: builder.query({
query: () => "products",
}),
}),
});
export const { useGetAllProductsQuery } = productsApi; //customs hoooks
components > Home.jsx
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router";
import { addToCart } from "../features/cartSlice";
import { useGetAllProductsQuery } from "../features/productsApi";
const Home = () => {
//const { items, status } = useSelector(state => state.products) //구조분해로 데이터 가져오기
const { items: products, status } = useSelector((state) => state.products);
const dispatch = useDispatch();
const history = useHistory();
//const { data, error, isLoading } = useGetAllProductsQuery(); RTK Query 방법, data = items
const handleAddToCart = (product) => {
dispatch(addToCart(product));
history.push("/cart");
};
return (
<div className="home-container">
{status === "success" ? (
<>
<h2>New Arrivals</h2>
<div className="products">
{products &&
products?.map((product) => (
<div key={product.id} className="product">
<h3>{product.name}</h3>
<img src={product.image} alt={product.name} />
<div className="details">
<span>{product.desc}</span>
<span className="price">${product.price}</span>
</div>
<button onClick={() => handleAddToCart(product)}>
Add To Cart
</button>
</div>
))}
</div>
</>
) : status === "pending" ? (
<p>Loading...</p>
) : (
<p>Unexpected error occured...</p>
)}
</div>
);
}
export default Home;
components > Cart.jsx
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
addToCart,
clearCart,
decreaseCart,
getTotals,
removeFromCart,
} from "../features/cartSlice";
import { Link } from "react-router-dom";
const Cart = () => {
const cart = useSelector((state) => state.cart);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getTotals());
}, [cart, dispatch]);
const handleAddToCart = (product) => {
dispatch(addToCart(product));
};
const handleDecreaseCart = (product) => {
dispatch(decreaseCart(product));
};
const handleRemoveFromCart = (product) => {
dispatch(removeFromCart(product));
};
const handleClearCart = () => {
dispatch(clearCart());
};
return (
<div className="cart-container">
<h2>Shopping Cart</h2>
{cart.cartItems.length === 0 ? (
<div className="cart-empty">
<p>Your cart is currently empty</p>
<div className="start-shopping">
<Link to="/">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
className="bi bi-arrow-left"
viewBox="0 0 16 16"
>
<path
fillRule="evenodd"
d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"
/>
</svg>
<span>Start Shopping</span>
</Link>
</div>
</div>
) : (
<div>
<div className="titles">
<h3 className="product-title">Product</h3>
<h3 className="price">Price</h3>
<h3 className="quantity">Quantity</h3>
<h3 className="total">Total</h3>
</div>
<div className="cart-items">
{cart.cartItems &&
cart.cartItems.map((cartItem) => (
<div className="cart-item" key={cartItem.id}>
<div className="cart-product">
<img src={cartItem.image} alt={cartItem.name} />
<div>
<h3>{cartItem.name}</h3>
<p>{cartItem.desc}</p>
<button onClick={() => handleRemoveFromCart(cartItem)}>
Remove
</button>
</div>
</div>
<div className="cart-product-price">${cartItem.price}</div>
<div className="cart-product-quantity">
<button onClick={() => handleDecreaseCart(cartItem)}>-</button>
<div className="count">{cartItem.cartQuantity}</div>
<button onClick={() => handleAddToCart(cartItem)}>+</button>
</div>
<div className="cart-product-total-price">
${cartItem.price * cartItem.cartQuantity}
</div>
</div>
))}
</div>
<div className="cart-summary">
<button className="clear-btn" onClick={() => handleClearCart()}>
Clear Cart
</button>
<div className="cart-checkout">
<div className="subtotal">
<span>Subtotal</span>
<span className="amount">${cart.cartTotalAmount}</span>
</div>
<p>Taxes and shipping calculated at checkout</p>
<button>Check out</button>
<div className="continue-shopping">
<Link to="/">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
className="bi bi-arrow-left"
viewBox="0 0 16 16"
>
<path
fillRule="evenodd"
d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"
/>
</svg>
<span>Continue Shopping</span>
</Link>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default Cart;
components > NavBar.jsx
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
const NavBar = () => {
const { cartTotalQuantity } = useSelector((state) => state.cart);
return (
<nav className="nav-bar">
<Link to="/">
<h2>OnlineShop</h2>
</Link>
<Link to="/cart">
<div className="nav-bag">
<svg
xmlns="http://www.w3.org/2000/svg"
width="35"
height="35"
fill="currentColor"
className="bi bi-handbag-fill"
viewBox="0 0 16 16"
>
<path d="M8 1a2 2 0 0 0-2 2v2H5V3a3 3 0 1 1 6 0v2h-1V3a2 2 0 0 0-2-2zM5 5H3.36a1.5 1.5 0 0 0-1.483 1.277L.85 13.13A2.5 2.5 0 0 0 3.322 16h9.355a2.5 2.5 0 0 0 2.473-2.87l-1.028-6.853A1.5 1.5 0 0 0 12.64 5H11v1.5a.5.5 0 0 1-1 0V5H6v1.5a.5.5 0 0 1-1 0V5z" />
</svg>
<span className="bag-quantity">
<span>{cartTotalQuantity}</span>
</span>
</div>
</Link>
</nav>
);
};
export default NavBar;
components > NavBar.jsx
const NotFound = () => {
return (
<div className="not-found">
<h2>404</h2>
<p>Page Not Found</p>
</div>
);
}
export default NotFound
반응형
'개발 > React' 카테고리의 다른 글
[react] useEffect 메모리 누수 Can't perform a React state update on an unmounted component... (0) | 2022.01.07 |
---|---|
[react] framer motion 사용법 (0) | 2022.01.06 |
[react] swiper.js으로 timepicker 커스텀 (swipe slide event) (0) | 2022.01.04 |
[react] 데이터(json) html 태그 화면 렌더링 방법 (ft. html-react-parser) (0) | 2021.12.23 |
[react] react에서 inline style 적용 방법 (ft. 인라인 css) (0) | 2021.12.22 |
댓글