코드작성

Cmarket Hooks - 작성

Heemok 2023. 6. 21. 22:34

2023-06-21(수)

 

 

✅ useState 를 이용해 상태를 사용하는 방법을 학습합니다.
✅ [장바구니 담기] 버튼을 이용해 장바구니에 해당 상품이 추가되도록 구현하세요.
✅ 장바구니 내 [삭제] 버튼을 이용해 장바구니의 상품이 제거되도록 구현하세요.
✅ 장바구니의 상품 갯수의 변동이 생길 때마다, 상단 내비게이션 바에 상품 갯수가 업데이트되도록 구현하세요.

 

이번 과제는 상품리스트와 장바구니를 이동하며 장바구니 담기 버튼을 클릭 시 장바구니안에 해당 아이템이 담기는

작동을 하는 것과, 장바구니 안에서 해당 카트아이템을 삭제하고, 상품 갯수를 업데이트 하는 작동을 구현하는 것이었다.

 

제일 상위 폴더인 app.js에 이미 작성되어 있는 정보들을 각각의 컴포넌트에 내려줘서  문제를 해결하는 방식이다.

 

  const [items, setItems] = useState(initialState.items);
  const [cartItems, setCartItems] = useState(initialState.cartItems);

아이템상태를 담고있는 items와 카트아이템 상태를 담고있는 cartitems를 props로 내려주어 상태를 바꿔주는 동작을 구현하면 된다.

 

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

// initialState는 import 를 통해서 state에 있는 값을 받아온 것이다.
function App() {
  const [items, setItems] = useState(initialState.items);
  const [cartItems, setCartItems] = useState(initialState.cartItems);

  return (
    <Router>
      <Nav cartItems={cartItems}/>
      <Routes>
        <Route path="/" element={<ItemListContainer items={items} cartItems={cartItems} setCartItems={setCartItems } />} />
        <Route
          path="/shoppingcart"
          element={<ShoppingCart cartItems={cartItems} items={items} setCartItems={setCartItems } />}
        />
      </Routes>
      <img
        id="logo_foot"
        src={`${process.env.PUBLIC_URL}/codestates-logo.png`}
        alt="logo_foot"
      />
    </Router>
  );
}

export default App;

 

app js에서는 react-router-dom을 이용해 Client Side Routhing을 해주고, 각각의 상태를 사용할 수 있도록

하위컴포넌트에 상태를 보내주는 코드를 작성하면 된다.

      <Routes>
        <Route path="/" element={<ItemListContainer items={items} cartItems={cartItems} setCartItems={setCartItems } />} />
        <Route
          path="/shoppingcart"
          element={<ShoppingCart cartItems={cartItems} items={items} setCartItems={setCartItems } />}
        />

 

 

장바구니 추가 기능

ItemListContainer의 handleClick을 item 컴포넌트와 id를 받아서 사용해 장바구니에 추가해주면 된다.

import React from 'react';
import Item from '../components/Item';

function ItemListContainer({ items,cartItems,setCartItems }) {
  const handleClick = ( e, Id ) => { 
    let original= cartItems
    let findNumber = cartItems.findIndex((e)=>e.itemId===Id)
     if(findNumber!==-1){
      original[findNumber].quantity+=1
      setCartItems(original)
     }else {
       setCartItems ([...original, 
         {"itemId": Id,"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;
function ItemListContainer({ items, cartItems, setCartItems }) {
  const handleClick = ( e, id ) => {
  let newCartItem = {};
  newCartItem.itemId = id;
  newCartItem.quantity = 1;
  for(let i = 0; i < cartItems.length; i++){
  if(cartItems[i].itemId === id){
  setCartItems([...cartItems])
  cartItems[i].quantity++
  }else{
  setCartItems([...cartItems, newCartItem])
  }
  }
  }

위의 전체코드에서와 아래 haedleClick이 다른걸 알 수 있다.

내가 이번과제를 진행하면서 생긴 문제들을 해결한 코드가 처음 코드고 , 두 번째 코드가 문제가 발생하는 코드였다.

 

우선 2번 코드로 작성한 뒤 실행을 해보면 실제 장바구니가 다 비워져있는 상태에서는 상품이 장바구니에 추가되지 않는

문제점이 생겼고, 또 장바구니에 없는 상품을 추가한 후, 그다음 바로 장바구니에 있는 상품을 추가하면 2개씩

품목이 증가하는 오류가 생기고, 품목이 8개라면 장바구니는 최대 8개까지만 쌓여야 하지만, 그 이상까지 증가하는것을

확인할 수 있었습니다.

 

하지만 1번 코드로 hadleClick 함수를 수정해주자. 해당 문제점들이 사라진것을 확인할 수 있었다.

 

이유는 우선 장바구니가 비워져 있을때 추가되지 않는 것은 for문을 돌지 않기 때문인데요.

여기서 cartItems에 length만큼 반복하는대 만약 아무것도 들어있지 않다면, for문이 동작하지 않기 때문에 작동되지

않았습니다. 이를 해결하기 위해, for문을 돌기전 if문으로 비워져있는 상태라면 즉각적으로 들어가는 코드를 작성해주면

잘 들어가는 것을 확인할 수 있습니다.

 

2번째 오류는 2개씩 품목이 증가하는 오류인데, 이는 품목이 8개이상으로 증가하는 오류와 같은 이유입니다.

그 이유는 Batching 때문인데요. batch는 일괄이라는 의미입니다. React에서는 여러개의 state 업데이트를

하나의 리렌더링으로 묶는 것을 의미합니다.

 

예를들자면

function App() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount((count) => count + 1);
    setCount((count) => count + 1);
    setCount((count) => count + 1);
  };

  useEffect(() => {
    console.log("count", count);
  }, [count]);

  return <button onClick={handleClick}>+</button>;
}

2.

자바스크립트에 기반하여 생각하면, 1번이 정답일 것이다.

setState가 3번 발생했고, count가 하나 증가할 때마다, 콘솔을 찍어줘야할 것 같다.

 

하지만, 실제로는 2번이 찍힙니다.

 

3번의 상태변화가 3번의 렌더링을 발생시키지 않고, 한 번의 렌더링을 발생시켰다.

즉, 배칭이란 여러개의 state 업데이트를 하나의 리렌더링으로 묶는 것을 의미한다.

 

이렇게 여러 상태변화를 통해 렌더링이 발생하면 가장 마지막 상태를 렌더링 해주는데

이러한 문제를 통해서  (3) setCartItems([...cartItems, newCartItem]) 의 호출만 적용되게 됩니다.

 

그런데 처음에 if문에 들어가면서 cartItems[0].quantity++라는 코드가 한 번 실행 되었죠?

만약 기존 cartItems의 값이 [ { 아이템1번  1개} , { 아이템 2번  1개 }, { 아이템 3번  1개 } ] 이렇게 있었고, 1번 아이템을 

선택했다면 기존의 cartItems의 값은 [ {아이템1 2개}, {아이템2 1개}, {아이템3 1개} ] 이렇게 바뀌었을거에요.

 

그래서 (3)에 전달된 인자는 […cartItems(여기에서 qunatity가 +1 된 상태가 붙여 넣어지고), newCartItem(여기서 똑같은 아이템이 한 번 더 추가되어버려요!)] 가 되고, 최종적으로 변경된 상태 값은

[ {아이템1 2개}, {아이템2 2개}, {아이템3 3개}, {아이템1 1개} ]가 되어버리는거죠..

 

결국 장바구니에 이것저것 추가할수록 cartItems 상태는 엉망이 되어버립니다.

 

 

item.js

import React from 'react'

export default function Item({ item, handleClick }) {

  return (
    <div key={item.id} className="item">
      <img className="item-img" src={item.img} alt={item.name}></img>
      <span className="item-name">{item.name}</span>
      <span className="item-price">{item.price}</span>
      <button className="item-button" onClick={(e) => handleClick(e, item.id)}>장바구니 담기</button>
    </div>
  )
}

 

 

nav.js

import React from 'react';
import { Link } from 'react-router-dom';

function Nav({cartItems}) {

  return (
    <div id="nav-body">
      <span id="title">
        <img id="logo" src="../logo.png" alt="logo" />
        <span id="name">CMarket</span>
      </span>
      <div id="menu">
        <Link to="/">상품리스트</Link>
        <Link to="/shoppingcart">
          장바구니<span id="nav-item-counter">{cartItems.length}</span>
        </Link>
      </div>
    </div>
  );
}

export default Nav;

nav에서는 장바구니에 몇개의 상품이 들어있는지 확인하기 위해 0에서 cartItems.length로 변경되게 해주고,

마찬가지로 상태를 가져와야하기 때문에 , cartItems의 상태를 넣어줘야 한다.

 

 

ShoppingCart.js

import React, { useState } from 'react'
import CartItem from '../components/CartItem'
import OrderSummary from '../components/OrderSummary'
import { render } from '@testing-library/react';

export default function ShoppingCart({ items, cartItems,setCartItems }) {
  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) => {
    const cartitemslist = [...cartItems];
    let findid = cartItems.findIndex((item) => item.itemId === itemId)
    cartitemslist[findid].quantity = quantity
    setCartItems(cartitemslist)

  }

  const handleDelete = (itemId) => {
    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 (
    <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>
  )
}

handleDelete함수를 수정해서 사용하면 된다
setCartItems(cartItems.filter((el)=> el.itemId !== itemId))
cartItems 에 필터를 돌려서 cartItems의 el은 객체형태로 되어있으므로

el.itemId와 클릭된 itemId값을 비교해서 같은 값만 삭제되도록 하면 된다

 

수량변경은

  const handleQuantityChange = (quantity, itemId) => {
    const newCartItems = [...cartItems]
    let findIdx = cartItems.findIndex((item) => item.itemId === itemId)
    newCartItems[findIdx].quantity = quantity
    setCartItems(newCartItems)
  }

findIndex를 사용해서 인덱스 번호를 찾아준 뒤
newCartItems[인덱스번호]의 수량 = 받아온 수량 으로 바꿔주면 된다
이렇게 하면 장바구니에서 위 아래 버튼을 클릭해서도 수량 변경이 가능하다