codestates/section3

과제2 - Cmarket redux

나아눙 2023. 2. 24. 00:17

action -> dispatch -> reducers -> store 순으로 간다.

actions폴더

.index.js

export const removeFromCart = (itemId) => {
  return {
    //TODO
    type: REMOVE_FROM_CART,
    payload: {
      itemId,
    },
  }
}
//removeFromCart 함수는 itemId를 인자로 받아 액션 객체를 반환
//반환된 액션 객체는 type 속성이 REMOVE_FROM_CART로 설정
//payload 속성은 { itemId }로 설정

export const setQuantity = (itemId, quantity) => {
  return {
    //TODO
    type: SET_QUANTITY,
    payload: {
      itemId,
      quantity,
    },
  }
}
//setQuantity 함수는 itemId와 quantity를 인자로 받아 액션 객체를 반환
//반환된 액션 객체는 type 속성이 SET_QUANTITY로 설정되며, 
//payload 속성은 { itemId, quantity }로 설정

액션 타입 상수와 액션 생성자 함수는 Redux 애플리케이션에서 액션을 디스패치하거나 상태를 업데이트하는 데 사용

 

payload란?

payload는 액션 객체에서 특정한 데이터를 전달하기 위한 속성이다.

payload 속성은 액션 생성자 함수에서 반환된 액션 객체의 중요한 부분으로,

해당 액션이 수행되는 동안 전달해야 하는 데이터를 포함

 

page폴더 (dispatch있음)

ItemListContainer.js
  const state = useSelector(state => state.itemReducer);
  //itemReducer 상태를 가져와서 items와 cartItems 변수에 할당
  const { items, cartItems } = state;
  const dispatch = useDispatch();

  const handleClick = (item) => {
    //handleClick 함수는 사용자가 상품을 클릭할 때 호출되며,
    // 클릭한 상품이 이미 카트에 추가되었는지 여부를 확인

    if (!cartItems.map((el) => el.itemId).includes(item.id)) {
      //TODO: dispatch 함수를 호출하여 아이템 추가에 대한 액션을 전달하세요.
      dispatch(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`))
      dispatch(addToCart(item.id));
    }
    else {
      dispatch(notify('이미 추가된 상품입니다.'))
    }
  }
  //카트에 추가되지 않은 상품이면, notify 함수를 호출하여
  // 상품 추가 메시지를 표시하고 addToCart 액션을 디스패치

  // 이미 추가된 상품이면, 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>
  );
}
//items 배열을 매핑하여 Item 컴포넌트를 렌더링한다.
// handleClick 함수를 handleClick props로 전달하여 
// 상품이 클릭되면 handleClick 함수가 호출

export default ItemListContainer;

ShoppingCart.js

export default function ShoppingCart() {

  const state = useSelector(state => state.itemReducer);
  const { cartItems, items } = state
  //useSelector를 사용하여 Redux store에서 상태 값을 가져와서 state 변수에 저장
  // state 변수에서는 장바구니에 담긴 아이템 목록과 상품 목록을 가져온다.
  :
  :
  const handleQuantityChange = (quantity, itemId) => {
    //TODO: dispatch 함수를 호출하여 액션을 전달하세요.
    dispatch(setQuantity(itemId, quantity));
  }
  //handleQuantityChange 함수는 수량 조절 버튼을 클릭하면  setQuantity 액션을 dispatch하여 상태 값을 업데이트합


  const handleDelete = (itemId) => {
    setCheckedItems(checkedItems.filter((el) => el !== itemId))
    //TODO: dispatch 함수를 호출하여 액션을 전달하세요.
    dispatch(removeFromCart(itemId));
  }
  //handleDelete 함수는 삭제 버튼을 클릭하면
  // removeFromCart 액션을 dispatch하여 상태 값을 업데이트

reducer

reducer 함수는 이전 상태와 액션을 받아서 새로운 상태를 반환하는 순수 함수

educer는 immutable해야한다.

=>Object.assign()이나 ...obj같이 얕은 복사하여 원본 수정이 안되게 한다.

itemReducer.ks

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

const itemReducer = (state = initialState, action) => {
//이 reducer 함수는 ADD_TO_CART, REMOVE_FROM_CART, SET_QUANTITY 세 가지 액션을 처리
//각 액션에 따라 다른 새로운 상태를 반환
  switch (action.type) {
    case ADD_TO_CART:
    //카트에 새로운 상템을 추가
      //TODO
      return Object.assign({}, state, {
        cartItems: [...state.cartItems, action.payload],
      });
      //action.payload 객체는 { itemId, quantity } 형식으로 들어오며, 
      //itemId는 추가할 아이템의 고유 ID, quantity는 추가할 아이템의 수량
      //.state.cartItems를 통해 현재 카트 아이템 배열의 내용을 그대로 가져오고, 
      //action.payload를 추가한 새로운 배열을 반환
      
    case REMOVE_FROM_CART:
      //TODO
      return {
        ...state,
        cartItems: state.cartItems.filter((el) => el.itemId !== action.payload.itemId),
      };
      //카트에서 특정 아이템을 삭제
      //action.payload 객체는 { itemId } 형식으로 들어오며, itemId는 삭제할 아이템의 고유 ID
      //state.cartItems.filter()를 통해 
      //현재 카트 아이템 배열에서 삭제할 아이템을 제외한 새로운 배열을 반환

    case SET_QUANTITY:
      let idx = state.cartItems.findIndex(el => el.itemId === action.payload.itemId)
      //TODO
      const reviseCart = state.cartItems.map((el, elIdx) => (elIdx === idx ? action.payload : el));
      return Object.assign({}, state, {
        cartItems: reviseCart,
      });
      //SET_QUANTITY: 카트에서 특정 아이템의 수량을 변경
      //action.payload 객체는 { itemId, quantity } 형식으로 들어오며, itemId는 변경할 아이템의 고유 ID, quantity는 변경할 아이템의 수량
      //state.cartItems.map()을 통해 카트 아이템 배열을 새로운 배열로 변환
      //action.payload로 변경한 새로운 아이템 객체를 반환하고, 그 외의 아이템은 그대로 반환
    default:
      return state;
  }
}

export default itemReducer;

 

리덕스에서 상태를 변경하는 작업은 기존 상태(state)를 변경하지 않고 새로운 상태를 반환하는 것이 원칙이다.

따라서 이 리듀서에서도 마찬가지로, 기존 state를 변경하지 않고 새로운 객체를 반환하도록 한다.

switch문에서 SET_QUANTITY 액션을 처리하는 부분이다.

SET_QUANTITY 액션이 dispatch되면,

우선 해당 상품의 itemId를 기준으로 기존의 cartItems 배열에서 해당 상품의 index를 찾는다.

이렇게 찾은 index를 이용해서, 새로운 배열 reviseCart를 만듭니다.

 

reviseCart는 기존의 cartItems 배열에서 map 메소드를 사용해서 새로운 배열을 만들어낸다.

이때, 수정할 상품의 index와 같은 index를 가진 요소는,

action.payload로 받은 수정된 상품 정보로 대체하고 그 외에는 기존 요소를 그대로 사용합니다.

 

마지막으로, Object.assign 메소드를 사용해서, state 객체를 복제하고, cartItems 속성을 새롭게 만든 reviseCart 배열로 업데이트한 객체를 반환한다.

이렇게 반환된 객체가 새로운 상태가 됩니다.

 

예시1)
 
case ADD_TO_CART: // 카트에 새로운 상템을 추가 return Object.assign({}, state, { cartItems: [...state.cartItems, action.payload], });

예를 들어, 현재 상태(state)는 다음과 같다고 가정

 
{ cartItems: [ { itemId: 1, quantity: 2 }, { itemId: 2, quantity: 1 }, ] }

그리고, ADD_TO_CART 액션이 다음과 같은 payload 객체를 가지고 발생했다고 가정합니다.

 
{ type: ADD_TO_CART, payload: { itemId: 3, quantity: 3 } }

이때, ADD_TO_CART 액션을 처리하는 reducer 함수는 다음과 같이 동작

  1. 먼저, Object.assign() 메서드를 이용하여 빈 객체({})를 새로운 객체로 만듭니다.
 
{}
  1. 두 번째 인자로 현재 상태(state) 객체를 전달하고, 이를 통해 현재 상태 객체의 속성들을 모두 새로운 객체에 복제합니다.
 
{ cartItems: [ { itemId: 1, quantity: 2 }, { itemId: 2, quantity: 1 }, ] }
   
    2. cartItems 속성을 추가하여, 기존의 state.cartItems 배열에 새로운 아이템(payload)을 추가한 새로운 배열을 만들어 할당한다.

이때, 기존의 state.cartItems 배열을 그대로 가져와서, 배열 전개 연산자([...])를 이용하여 새로운 배열의 끝에 새로운 아이템을 추가한다.

 
{ cartItems: [ { itemId: 1, quantity: 2 }, { itemId: 2, quantity: 1 }, { itemId: 3, quantity: 3 }, ] }
   
   3. 이를 새로운 상태로 반환합니다.
 
{ cartItems: [ { itemId: 1, quantity: 2 }, { itemId: 2, quantity: 1 }, { itemId: 3, quantity: 3 }, ] }

이렇게, ADD_TO_CART 액션에 대한 reducer 함수는, 현재 상태를 변경하지 않고, 새로운 상태를 만들어 반환하는 방식으로 동작한다.

 
예시2)
 
{ cartItems: [ { itemId: 1, quantity: 2 }, { itemId: 2, quantity: 3 }, { itemId: 3, quantity: 1 } ] }

이때, REMOVE_FROM_CART 액션이 dispatch되어 payload로 { itemId: 2 }가 전달된다고 가정

그러면, ...state 구문을 통해 현재의 상태를 복사한 후, cartItems 프로퍼티를 새로운 배열로 업데이트한다.

이때, filter 메소드를 사용하여 삭제할 아이템을 제외한 나머지 아이템들로 이루어진 새로운 배열을 만든다.

위의 예시에서는 itemId가 2인 아이템이 삭제되므로, 다음과 같은 새로운 상태가 반환된다.

 
{ cartItems: [ { itemId: 1, quantity: 2 }, { itemId: 3, quantity: 1 } ] }
 

예시3)

예를 들어, 현재 카트 아이템 배열이 다음과 같다고 가정해본다.

 
[ { itemId: 1, quantity: 2 }, { itemId: 2, quantity: 3 }, { itemId: 3, quantity: 1 } ]

이 상태에서 SET_QUANTITY 액션을 디스패치하면, 예를 들어 { itemId: 2, quantity: 5 } 라는 payload가 전달되었다고 가정해본다.

그러면 먼저 idx 변수에는 현재 카트 아이템 배열에서 itemId가 2인 아이템의 인덱스인 1이 할당된다.

이후 map() 함수를 사용하여 카트 아이템 배열을 새로운 배열로 변환한다.

elIdx === idx 조건에 해당하는 아이템은 새로운 payload 값으로 교체되고, 그 외의 아이템은 그대로 유지된다.

그렇게 변환된 새로운 카트 아이템 배열은 다음과 같다.

yamlCopy code
[ { itemId: 1, quantity: 2 }, { itemId: 2, quantity: 5 }, { itemId: 3, quantity: 1 } ]

마지막으로 이 새로운 배열을 Object.assign() 함수를 사용하여 기존 상태 객체에 덮어씌워서 반환한다.

 

import { ENQUEUE_NOTIFICATION, DEQUEUE_NOTIFICATION } from "../actions/index";
import { initialState } from "./initialState";

const notificationReducer = (state = {notifications:[]}, action) => {

  switch (action.type) {
    case ENQUEUE_NOTIFICATION: //알림을 큐에 추가
      return Object.assign({}, state, {
        notifications: [...state.notifications, action.payload]
      })
      //기존 상태 객체를 변경하지 않고 새로운 상태 객체를 만들어서 반환
      // 상태 객체의 notifications 배열을 새 배열로 업데이트
    case DEQUEUE_NOTIFICATION://큐에서 알림을 제거
      return Object.assign({}, state, {
        notifications: state.notifications.slice(1)
      })
      // 기존 상태 객체를 변경하지 않고 새로운 상태 객체를 만들어서 반환
      // notifications 배열의 첫 번째 요소를 제외한 나머지 요소로
      // 새 배열을 만들어서 업데이트
    default:
      return state;
  }
}

export default notificationReducer;

위 코드는 ENQUEUE_NOTIFICATION 액션 타입에 대한 리듀서이다.

Object.assign() 메소드는 첫 번째 인자로 빈 객체 {}를 받고,

두 번째 인자로는 현재 상태 객체 state를 받는다.

마지막 인자는 새로운 상태를 정의할 객체이다.

그리고 새로운 상태 객체를 만들어 notifications 배열 속성을 갱신한다.이때, [...state.notifications, action.payload] 문법을 사용해 기존의 notifications 배열과 새로운 알림 객체 action.payload를 합쳐서 새로운 배열을 생성한다.

그리고 그 배열을 Object.assign() 메소드를 사용해 {} 객체와 합쳐서 새로운 상태 객체를 생성한다.

이 리듀서는 ENQUEUE_NOTIFICATION 액션이 디스패치될 때마다, notifications 배열에 새로운 알림 객체를 추가해 상태를 갱신한다.

 

import { compose, createStore, applyMiddleware } from "redux";
import rootReducer from '../reducers/index';
import thunk from "redux-thunk";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
  : compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));

export default store;
//createStore() 함수는 Redux에서 제공하는 Store 객체를 생성
//두 개의 인자를 받습니다. 첫 번째 인자로는 모든 reducer를 합친 rootReducer를 전달하고, 
//두 번째 인자로는 middleware를 적용

//compose() 함수는 여러 개의 함수를 조합하여 하나의 함수로 만들어 주는 함수
//composeEnhancers()는 window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__이 있다면 그것을 사용하고,
// 없다면 compose()를 사용하여 기본적인 compose 함수를 반환

//applyMiddleware() 함수는 thunk 미들웨어를 적용하여
// store에 액션을 디스패치할 때 추가적인 로직을 처리

//마지막으로 createStore() 함수와 함께 composeEnhancers() 함수를 호출하여, Redux Store 객체를 생성
// 그리고 생성된 store 객체를 내보내어, 다른 파일에서 사용