App.js
import React, { useState } from 'react'; import Nav from './components/Nav'; import ItemListContainer from './pages/ItemListContainer'; import './App.css'; import './variables.css'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import ShoppingCart from './pages/ShoppingCart'; import { initialState } from './assets/state'; function App() { const [items, setItems] = useState(initialState.items); //데이터를 담은 배열 const [cartItems, setCartItems] = useState(initialState.cartItems); //장바구니 리스트를 담은 배열 const addCartList = (itemId) => { const foundId = cartItems.filter(el => el.itemId === itemId)[0]; // 중복되는 요소가 있는지 확인 (값은 최대 1개이니 바로 요소를 꺼내준다) if (foundId) { setQuantity(itemId, foundId.quantity + 1) } else { setCartItems([...cartItems, {itemId:itemId, quantity: 1}]) } } const setQuantity = (itemId, quantity) => { // 수량을 증가시켜준 후, 그 요소를 찾아 카트아이템에 집어넣어주는 함수 const foundId = cartItems.filter((el) => el.itemId === itemId)[0]; const idx = cartItems.indexOf(foundId); const cartItem = { // quantity가 증가하여 다시 재정의 해준다. {itemId: itemId , quantity: quantity} 받아온 인자와 이름이 같기떄문에 아래와 같이 작성 itemId, quantity, }; setCartItems([...cartItems.slice(0, idx), cartItem, ...cartItems.slice(idx + 1)]); // 덮어씌우기 console.log(foundId); }; ... return 생략
ShoppingCart.js
import React, { useState } from 'react' import CartItem from '../components/CartItem' import OrderSummary from '../components/OrderSummary' import { Link } from 'react-router-dom'; export default function ShoppingCart({ items, cartItems, setCartItems }) { const [checkedItems, setCheckedItems] = useState( cartItems.map((el) => el.itemId) ); //카트아이템의 id값을 요소로하는 배열 const handleCheckChange = (checked, id) => { //체크된 리스트를 추가, 제거하는 함수 if (checked) { setCheckedItems([...checkedItems, id]); } else { setCheckedItems(checkedItems.filter((el) => el !== id)); } }; const handleAllCheck = (checked) => { //모두선택 체크박스를 선택하면 모두선택되고, 다시누르면 모두 해체 if (checked) { setCheckedItems(cartItems.map((el) => el.itemId)); } else { setCheckedItems([]); } }; const handleQuantityChange = (quantity, itemId) => { let obj = cartItems.filter(el => el.itemId === itemId)[0]; let idx = cartItems.indexOf(obj); obj = { itemId, quantity, } setCartItems([...cartItems.slice(0, idx), obj, ...cartItems.slice(idx + 1)]); }; const handleDelete = (itemId) => { //지우는 함수 setCheckedItems(checkedItems.filter((el) => el !== itemId)); //체크박스 상태를 false로 함 setCartItems(cartItems.filter(el => el.itemId !== itemId)); // 상품을 제거한다. }; const getTotal = () => { let cartIdArr = cartItems.map((el) => el.itemId); let total = { price: 0, quantity: 0, }; for (let i = 0; i < cartIdArr.length; i++) { if (checkedItems.indexOf(cartIdArr[i]) > -1) { let quantity = cartItems[i].quantity; let price = items.filter((el) => el.id === cartItems[i].itemId)[0] .price; total.price = total.price + quantity * price; total.quantity = total.quantity + quantity; } } return total; }; const renderItems = items.filter( (el) => cartItems.map((el) => el.itemId).indexOf(el.id) > -1 ); const total = getTotal(); ... return 생략
1. 장바구니에 추가 및 상품 개수 업데이트
메인 화면에서 [장바구니 담기] 버튼을 누른 후, 장바구니 페이지로 이동하면 상품이 담겨있어야 합니다.
장바구니 페이지에서 장바구니에 담긴 각 아이템의 개수를 변경할 수 있어야 합니다.
내비게이션 바에 상품 개수가 즉시 표시되어야 합니다.
2. 장바구니로부터 제거
장바구니 페이지에서 [삭제] 버튼을 누른 후, 해당 상품이 목록에서 삭제되어야 합니다.
내비게이션 바에 상품 개수가 즉시 표시되어야 합니다.
{
"id": 1,
"name": "노른자 분리기",
"img": "../images/egg.png",
"price": 9900
}
...
{
"itemId": 1,
"quantity": 1
}
...
먼저 장바구니에 상품을 추가하려면 장바구니 목록 데이터를 이용해야한다.
아래의 함수를 이용하여 선택한 상품의 id를 인자로 전달하여 함수를 실행시킨다.
ItemListContainer
function ItemListContainer({ items, addCartList }) { const handleClick = (e, id) => { addCartList(id); // App.js에서 props로 전달 받은 함수 };
이후 전달받은 id를 이용하여
cartItems
상태에 값을 추가해야하지만,
이미 있는 상품이라면 상품을 추가하지않고 수량만 증가시켜야한다.
foundId
라는 변수를 선언하여 중복되는 요소를 확인하는 용도로 사용하였다.
addCartList
const addCartList = (itemId) => { const foundId = cartItems.filter(el => el.itemId === itemId)[0]; // 중복되는 요소가 있는지 확인 (값은 최대 1개이니 바로 요소를 꺼내준다) if (foundId) { setQuantity(itemId, foundId.quantity + 1) // 수량을 하나 증가시킨다 } else { setCartItems([...cartItems, {itemId:itemId, quantity: 1}]) } }
데이터의 수량만 증가시키고 싶을 때, index를 찾아서 아래와 같이 slice 메서드를 이용해
cartItems
를 덮어씌우면 된다.
setQuantity
const setQuantity = (itemId, quantity) => { // 수량을 증가시켜준 후, 그 요소를 찾아 카트아이템에 집어넣어주는 함수 const foundId = cartItems.filter((el) => el.itemId === itemId)[0]; const idx = cartItems.indexOf(foundId); const cartItem = { // quantity가 증가하여 다시 재정의 해준다. {itemId: itemId , quantity: quantity} 받아온 인자와 이름이 같기떄문에 아래와 같이 작성 itemId, quantity, }; setCartItems([...cartItems.slice(0, idx), cartItem, ...cartItems.slice(idx + 1)]); // 덮어씌우기 console.log(foundId); };
내비게이션 바에 상품개수를 표시하는 방법은 cartItems
의 길이를 이용하면 된다.
<span id="nav-item-counter">{cartItems.length}</span>
장바구니 페이지에서 수량을 증가 / 감소 시키는 것은 위의 setQuantity
와 똑같은 방법으로 하면 된다.
위의 Cmarket-Hooks를 Redux를 이용하여 리팩토링 하는 과제이다.
itemReducer
: 현재의 state와 Action을 이용해서 새로운 state를 만들어 내는 순수함수이다.
import { REMOVE_FROM_CART, ADD_TO_CART, SET_QUANTITY } from "../actions/index"; import { initialState } from "./initialState"; // 객체이며 cartItems: [...], items:[...] 을 가지고 있다. const itemReducer = (state = initialState, action) => { switch (action.type) { case ADD_TO_CART: return Object.assign({}, state, { // 불변성을 지키기 위해 빈 객체에 값들을 집어넣어 새로운 객체를 반환한다. cartItems: [...state.cartItems, action.payload], // 기존의 cartItems을 덮어씌운다. }); case REMOVE_FROM_CART: console.log(action.payload); return Object.assign({}, state, { cartItems: state.cartItems.filter(el => el.itemId !== action.payload.itemId) //선택한 리스트만 제거된 배열로 재정의해줌 }) case SET_QUANTITY: let idx = state.cartItems.findIndex(el => el.itemId === action.payload.itemId) return Object.assign({}, state, { cartItems: [...state.cartItems.slice(0, idx), action.payload, ...state.cartItems.slice(idx + 1)] }) default: return state; } } export default itemReducer;
action / index.js ( 액션을 만드는 함수들 )
// action types export const ADD_TO_CART = "ADD_TO_CART"; export const REMOVE_FROM_CART = "REMOVE_FROM_CART"; export const SET_QUANTITY = "SET_QUANTITY"; export const NOTIFY = "NOTIFY"; export const ENQUEUE_NOTIFICATION = "ENQUEUE_NOTIFICATION"; export const DEQUEUE_NOTIFICATION = "DEQUEUE_NOTIFICATION"; // actions creator functions export const addToCart = (itemId) => { return { type: ADD_TO_CART, payload: { itemId, quantity: 1, }, }; }; export const removeFromCart = (itemId) => { return { type: REMOVE_FROM_CART, payload: { itemId }, //reducer는 기존 상태를 보존해야하기 때문에 객체형태로 작성 }; }; export const setQuantity = (itemId, quantity) => { return { type: SET_QUANTITY, payload: { itemId, quantity, }, }; }; export const notify = function (message, dismissTime = 5000) { return function (dispatch) { const uuid = Math.random(); dispatch(enqueueNotification(message, dismissTime, uuid)); setTimeout(() => { dispatch(dequeueNotification()); }, dismissTime); }; }; //위의 코드는 이런 방식으로도 표현 가능 // export const notify = (message, dismissTime = 5000) => dispatch => { //함수 중첩문 // const uuid = Math.random() // dispatch(enqueueNotification(message, dismissTime, uuid)) // setTimeout(() => { // dispatch(dequeueNotification()) // }, dismissTime) // } export const enqueueNotification = (message, dismissTime, uuid) => { return { type: ENQUEUE_NOTIFICATION, payload: { message, dismissTime, uuid, }, }; }; export const dequeueNotification = () => { return { type: DEQUEUE_NOTIFICATION, }; };
장바구니에 추가 및 상품 개수 업데이트
위의 이미지에는
state
, items
, cartItems
변수의 값이 순서대로 나와있다.
리스트를 클릭했을 때, 중복이 아니라면
addToCart
함수에 id를 인자로 주어 실행시킨다.function ItemListContainer() { const state = useSelector(state => state.itemReducer); const { items, cartItems } = state; const dispatch = useDispatch(); const handleClick = (item) => { if (!cartItems.map((el) => el.itemId).includes(item.id)) { dispatch(addToCart(item.id)) dispatch(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`)) //message를 인자로 전달 } else { dispatch(notify('이미 추가된 상품입니다.')) } } ...
Action 생성 함수
export const addToCart = (itemId) => { return { type: ADD_TO_CART, payload: { itemId, quantity: 1, }, };
위의 Cmarket-Hooks와는 다르게 actions.payload
를 이용하여 Action 생성함수에서 객체를 가져와서 사용한다.
나머지 기능구현은 Cmarket-Hook과 JavaScript코드는 거의 똑같기 때문에, action, reducer, dispatch, store 가 어떻게 유기적으로 연결되어 있는지만 이해하면 좋을듯하다.