[React] Cmarket Redux

jungmin Lee·2023년 9월 18일
0

Cmarket Redux

Cmarket의 컴포넌트 구조

CMARKET-REDUX

README.md

▶ public

▽ src

    ▽ actions          # 장바구니를 구현하기 위해 필요한 Action들을 구성

         index.js

     ▽ components 

         CartItem.js                   # 장바구니로 선택된 상품 항목에 대한 컴포넌트 

          item.js                      # 상품 항목에 대한 컴포넌트     

          Nav.js                       # 상단의 네비게이션 컴포넌트

          NotificationCenter.js    

          OrderSummary.js       # 장바구니의 총 합계에 대한 컴포넌트

          Toast.js                       

      ▽ pages            # Action객체를 Reducer로 전달하는 useDispatch, 컴포넌트와 state를 연결하는 useSelector가 있음

           ItemListContainer.js         # 상품리스트 페이지

           ShoppingCart.js               # 장바구니 페이지

      ▽ reducers

           index.js                      # 상품리스트 페이지

           initialState.js               # Store의 전체 아이템 목록(items), 장바구니 목록(cartItems)

           itemReducer.js                # 현재의 state와 Action을 이용해서 새로운 state를 만들어내는 순수함수

           notificationReducer.js      

      ▽ store

           store.js                      # createStore 메소드 rootReducer를 연결

        App.js

        App.css

        index.js


action > index.js

장바구니 구현을 위해 필요한 액션들을 정의해 놓은 객체이다. Action에서 type은 필수로 지정해야하며, payload를 작성하여 구체적인 값을 전달할 수 있다. Action 객체는 Dispatch 함수를 통해 Reducer 함수 두번째 인자로 전달된다. 액션에서 지정한 type에 따라 Reducer 함수에서 새로운 state를 리턴하게 된다. Action Creator(액션 생성자)함수로 addTocart, removeFromCart, setQuantity, enqueueNotification, dequeueNotification을 확인할 수 있다.

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";

export const addToCart = (itemId) => {
  return {
    type: ADD_TO_CART,
    payload: {
      quantity: 1,
      itemId,
    },
  };
};

export const removeFromCart = (itemId) => {
  return {
    type: REMOVE_FROM_CART,
    payload: {
      itemId,
    },
  };
};

export const setQuantity = (itemId, quantity) => {
  return {
    type: SET_QUANTITY,
    payload: {
      quantity,
      itemId,
    },
  };
};

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,
  };
};

reducers > itemReducer.js

현재의 state와 Action을 이용해서 새로운 state를 만들어 내는 순수함수인 Reducer를 작성한다. 외부 요인으로 생각하지 못한 값으로 변경되는 일이 없어야하므로 순수함수로 작성한다. Action 객체의 type 값에 따라서 분기하는 switch 조건문으로 작성되었으며 장바구니 추가, 장바구니 삭제, 수량 변경을 확인할 수 있다.
Reducer의 state 업데이트는 immutable한 방식으로 변경해야하므로 주의해서 작성해야 하며 Reducer가 처음 호출될 때, state 값은 undefined가 되고 오류가 발생하므로 초기값을 지정해줘야한다.
object.assign을 사용하여 새로운 객체를 만들어 리턴할 수 있으며 스프레드 연산자를 사용하여 작성할 수도 있다.

import { REMOVE_FROM_CART, ADD_TO_CART, SET_QUANTITY } from "../actions/index";
import { initialState } from "./initialState";

const itemReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TO_CART:
      return Object.assign({}, state, {
        cartItems: [...state.cartItems, action.payload],
      });

      break;
    case REMOVE_FROM_CART:
      let removeItem = state.cartItems.filter(
        (el) => el.itemId !== action.payload.itemId
      );
      return Object.assign({}, state, { cartItems: removeItem });

      break;
    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),
        ],
      });

      break;
    default:
      return state;
  }
};

export default itemReducer;
import { REMOVE_FROM_CART, ADD_TO_CART, SET_QUANTITY } from "../actions/index";
import { initialState } from "./initialState";

const itemReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TO_CART:
      return { ...state, cartItems: [...state.cartItems, action.payload] };

      break;
    case REMOVE_FROM_CART:
      return {
        ...state,
        cartItems: state.cartItems.filter(
          (el) => el.itemId !== action.payload.itemId
        ),
      };

      break;
    case SET_QUANTITY:
      let target = state.cartItems.findIndex(
        (el) => el.itemId === action.payload.itemId
      );
      return {
        ...state,
        cartItems: state.cartItems.map((el, idx) => {
          if (target === idx) return action.payload;
          else return el;
        }),
      };
      break;
    default:
      return state;
  }
};

export default itemReducer;

pages > itemListContainer.js

위에 있는 파일들에서 Store, Reducer, Action, Dispatch를 코드로 구성하였으며 이 개념들을 연결해주는 Redux Hooks를 이용하여 작성하고 있다. useSelector 메서드로 Store의 state에 접근하고 있으며 전달인자로는 콜백 함수를 받고 콜백함수의 전달인자로는 state 값이 들어온다. useDispatch는 Action객체를 Reducer로 전달해주고 있다. Dispatch의 전달인자로 Action 객체가 전달되며 Action 객체를 전달받은 Dispatch 함수는 Reducer를 호출한다.

import React from "react";
import { addToCart, notify } from "../actions/index";
import { useSelector, useDispatch } from "react-redux";
import Item from "../components/Item";

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}이(가) 추가되었습니다.`));
    } else {
      dispatch(notify("이미 추가된 상품입니다."));
    }
  };

  return (
    <div id="item-list-container">
      <div id="item-list-body">
        <div id="item-list-title">쓸모없는 선물 모음</div>
        {items.map((item, idx) => (
          <Item
            item={item}
            key={idx}
            handleClick={() => {
              handleClick(item);
            }}
          />
        ))}
      </div>
    </div>
  );
}

export default ItemListContainer;

pages > ShoppingCart.js

import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { removeFromCart, setQuantity } from "../actions";
import CartItem from "../components/CartItem";
import OrderSummary from "../components/OrderSummary";

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 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) => {
    dispatch(setQuantity(itemId, quantity));
  };

  const handleDelete = (itemId) => {
    setCheckedItems(checkedItems.filter((el) => el !== itemId));
    dispatch(removeFromCart(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 (
    <div id="item-list-container">
      <div id="item-list-body">
        <div id="item-list-title">장바구니</div>
        <span id="shopping-cart-select-all">
          <input
            type="checkbox"
            checked={checkedItems.length === cartItems.length ? true : false}
            onChange={(e) => handleAllCheck(e.target.checked)}
          ></input>
          <label>전체선택</label>
        </span>
        <div id="shopping-cart-container">
          {!cartItems.length ? (
            <div id="item-list-text">장바구니에 아이템이 없습니다.</div>
          ) : (
            <div id="cart-item-list">
              {renderItems.map((item, idx) => {
                const quantity = cartItems.filter(
                  (el) => el.itemId === item.id
                )[0].quantity;
                return (
                  <CartItem
                    key={idx}
                    handleCheckChange={handleCheckChange}
                    handleQuantityChange={handleQuantityChange}
                    handleDelete={handleDelete}
                    item={item}
                    checkedItems={checkedItems}
                    quantity={quantity}
                  />
                );
              })}
            </div>
          )}
          <OrderSummary total={total.price} totalQty={total.quantity} />
        </div>
      </div>
    </div>
  );
}
profile
Leejungmin

0개의 댓글