본문 바로가기
개발 학습/프론트엔드

React 상태관리 제대로 해보기 (useState ↔ Redux)

by StelthPark 2022. 4. 19.

Hook - useState

우선 useState로 최상위 컴포넌트에서 장바구니에 담은 아이템들을 관리하고 있으며 장바구니에서 담긴 아이템은 삭제하거나 수량을 변경하면 최상위 컴포넌트에 있는 장바구니 상태인 cartItems가 변경되며 상품리스트에 있는 새 아이템들을 장바구니에 담으면 기존 장바구니에 있는 itemId가 존재하는, 담겨있던 상품이라면 갯수만 추가해주고 담겨 있지 않던 상품이라면 아이템을 새롭게 추가하게 된다.

최상위 컴포넌트 App

function App() {
  const [items, setItems] = useState(initialState.items); //판매중인 itemList
  const [cartItems, setCartItems] = useState(initialState.cartItems); //현재 장바구니 itemList

  return (
    <Router>
      <Nav cartCount={cartItems.length} />
      <Switch>
        <Route exact={true} path="/">
          <ItemListContainer items={items} pushCartItems={setCartItems} cartItems={cartItems} />
        </Route>
        <Route path="/shoppingcart">
          <ShoppingCart cartItems={cartItems} pushCartItems={setCartItems} items={items} />
        </Route>
      </Switch>
    </Router>
  );
}

export default App;


자식 컴포넌트1 상품리스트 itemListContainer

function ItemListContainer({ items, pushCartItems, cartItems }) {
  const handleClick = (e, itemId) => {
    let cartItemId = cartItems.map((el) => {
      return el.itemId;
    });
    if (cartItemId.includes(itemId)) {
      pushCartItems(
        cartItems.map((el) => {
          if (el.itemId === itemId) {
            el.quantity++;
          }
          return el;
        })
      );
    } else {
      pushCartItems([...cartItems, { itemId: itemId, quantity: 1 }]);
    }
  };
  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} />
        ))}
      </div>
    </div>
  );
}

export default ItemListContainer;


자식 컴포넌트2 장바구니리스트 ShoppingCart

export default function ShoppingCart({ items, pushCartItems, cartItems }) {
  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) => {
  // 장바구니 아이템 수량 연산
    pushCartItems(
      cartItems.map((el) => {
        if (el.itemId === itemId) {
          el.quantity = quantity;
        }
        return el;
      })
    );
  };

  const handleDelete = (itemId) => {
    // 장바구니 아이템 삭제
    pushCartItems(
      cartItems.filter((el) => {
        if (el.itemId !== itemId) return el;
      })
    );
  };

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


useState로 동작


최상위 컴포넌트에서 상태를 관리하고 하위 컴포넌트와의 깊이가 깊어지면 상태를 관리하고 전달하기가 매우 어려워진다. 그래서 상태를 컴포넌트에 종속시키지 않고 외부에서 관리할 수 있도록 redux를 사용해보자


Redux

Redux(리덕스)란 JavaScript(자바스트립트) 상태관리 라이브러리이다.

Action 객체가 디스패치에 전달 되고 디스패치는 리듀서를 호출해서 새로운 state를 생성한다.

1. Action 생성


Action은 말 그대로 어떤 액션을 취할 것인지 정의해 놓은 객체입니다.

{ type: ‘ADD_TO_CART’, payload: request }

보통 다음과 같은 모양으로 구성됩니다. 여기서 type은 필수로 지정을 해 주어야 하며, 그 외의 것들은 선택적으로 사용할 수 있습니다. 이렇게 모든 변화를 action을 통해 취하는 것은 우리가 만드는 앱에서 무슨 일이 일어나고 있는지 직관적으로 알기 쉽게 하는 역할을 합니다.

export const addToCart = (itemId) => {
  return {
    type: ADD_TO_CART,
    payload: {
      quantity: 1,
      itemId: 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,
  };
};


2. Reducer 정의
Reducer 는 현재의 state와 Action을 이용해서 새로운 state를 만들어 내는 pure function 입니다. 또한 보이는 코드는 쇼핑몰에서 크게 볼 수 있는 장바구니 추가 액션에 대한 코드입니다.

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

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

    case SET_QUANTITY:
      let idx = state.cartItems.findIndex((el) => el.itemId === action.payload.itemId);
      state.cartItems[idx].quantity = action.payload.quantity;
      return Object.assign({}, state, {
        cartItems: state.cartItems,
      });

    default:
      return state;
  }
};

actions에 정의 했던 액션명들을 가져와서 정의해주게 되며 어떤 액션명마다 어떤행동을 하게 될 것인지 정해주게된다.
Object.assign을 사용한 이유는 리듀서의 불변성때문이다. 리액트에서 State를 변경 할 때 직접 바꾸지않고 setState를 사용하는 것처럼 Reducer 함수를 작성할 때 주의해야 할 점이 있습니다. 바로 Redux의 state 업데이트는 immutable한 방식으로 변경해야 한다는 것이다. Redux의 장점 중 하나인 변경된 state를 로그로 남기기 위해서 꼭 필요한 작업입니다.

3. Dispatch 사용

  const dispatch = useDispatch(); //선언 하여 디스패치를 사용 할 수 있게 된다.
  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("이미 추가된 상품입니다."));
    }
  };

해당 로직은 상품리스트에서 어떤 상품을 "장바구니 담기" 버튼을 누르면 장바구니에 담기위해 이벤트핸들러가 작동되는 함수로 내부에서 dispatch를 사용해 addTocart라는 액션을 불러 내부에 인자를 전달하게 된다.

4. Storage 접근

  const state = useSelector((state) => state.itemReducer);
  const { items, cartItems } = state;



5. 복습


6. 전체적인 순서 흐름



직접 구현을 해봐야 이해하기가 쉽다. action을 정의하고 액션이름과 내부에서 쓸 payload를 정의하며 itemReducer라는 리듀서에서 액션망마다 해당하는 payload로 어떤 행동을(더하고 빼고 연산등등) 할지 정의하고 이후 사용자 행위에 따라 움직일 이벤트핸들러 함수내부에 dispatch를 사용해 액션명을 부르며 인자도 같이 넘겨주는 것이다.

GitHub - Parkstelth/React-State-Management-ShoppingCartSystem

Contribute to Parkstelth/React-State-Management-ShoppingCartSystem development by creating an account on GitHub.

github.com

'개발 학습 > 프론트엔드' 카테고리의 다른 글

개발간에 썼던 React use Hook - 2  (0) 2024.08.22
개발간에 썼던 React use Hook - 1  (0) 2024.08.22
Node.js에서 Puppeteer로 동적웹 크롤링하기  (0) 2022.02.08
S2: Redux  (0) 2021.10.18
S2: Event.stopPropagation()  (0) 2021.10.15

댓글