[React] Cmarket Hooks 상태관리

HIHI JIN·2023년 2월 23일
0

react

목록 보기
8/19
post-thumbnail

[React] 상태 관리

쇼핑몰 애플리케이션인 Cmarket에서 Hooks를 이용해 상태 관리 애플리케이션을 만듭니다.
Redux를 배우기 전, React Hooks를 사용한 상태 관리 입니다.

  • Bare minimum requirements
    react-router-dom을 이용해 Client Side Routing하는 방법을 학습합니다.
    useState 를 이용해 상태를 사용하는 방법을 학습합니다.
    쇼핑몰 애플리케이션의 주요 기능을 구현하세요.
    [장바구니 담기] 버튼을 이용해 장바구니에 해당 상품이 추가되도록 구현하세요.
    장바구니 내 [삭제] 버튼을 이용해 장바구니의 상품이 제거되도록 구현하세요.
    장바구니 내에서 각 아이템 개수를 변경할 수 있도록 구현하세요.
    장바구니의 상품 개수의 변동이 생길 때마다, 상단 내비게이션 바에 상품 개수가 업데이트되도록 구현하세요.
  • TODO: 장바구니에 추가 및 상품 개수 업데이트
    다음과 같이 작동해야 합니다.
    메인 화면에서 [장바구니 담기] 버튼을 누른 후, 장바구니 페이지로 이동하면 상품이 담겨있어야 합니다.
    장바구니 페이지에서 장바구니에 담긴 각 아이템의 개수를 변경할 수 있어야 합니다.
    내비게이션 바에 상품 개수가 즉시 표시되어야 합니다.
  • TODO: 장바구니로부터 제거
    장바구니 페이지에서 [삭제] 버튼을 누른 후, 해당 상품이 목록에서 삭제되어야 합니다.
    내비게이션 바에 상품 개수가 즉시 표시되어야 합니다.

시작하기 전에...
애플리케이션에서 사용하는 주요 상태는 다음과 같습니다. 시작하기 전에 반드시 컴포넌트 구조와, 데이터 흐름을 먼저 그림으로 그려보세요. 일을 보다 단순하게 만들 수 있습니다.

<컴포넌트 구조와, 데이터 흐름 Figma>


App.js

props로 상태변경함수 내려주기!
1. Nav 컴포넌트에 props로 cartItems 내려주기
2. ItemListContainer props로 items, cartItems, setCartItems 내려주기
3.ShoppingCart props로 items, cartItems, setCartItems 내려주기

import './App.css';
import './variables.css';

import React, { useState } from 'react';

import {
  BrowserRouter as Router,
  Route,
  Routes,
} from 'react-router-dom';

import { initialState } from './assets/state';
import Nav from './components/Nav';
import ItemListContainer from './pages/ItemListContainer';
import ShoppingCart from './pages/ShoppingCart';

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;

ItemListContainer.js

  1. props로 받아온 {items, cartItems, setCartItems} 넣어주기
  2. item.js에서 handleClick(e, item.id)으로 전달인자를 넣어줬으므로, handleClick(e,id) 매개변수 넣기
    id는 상품목록페이지에서 장바구니담기를 클릭한 item의 id
<button className="item-button" onClick={(e) => handleClick(e, item.id)}>장바구니 담기</button>
  1. cartItem의 요소인 객체형태로 새로 추가된 cartitem 요소를 변수(itemObj)에 담는다.
//장바구니 목록 = cartItem의 코드
[{
  "itemId": 1,
  "quantity": 1
}]

//itemObj
const itemObj = {"itemId":id, "quantity":1};

4.cartitems가 빈 배열이라면, setcartitems에 원래 cartitems에 새로운 객체itemObj를 추가하여 상태변경한다.
5. cartitems 요소의 id와 상품클릭한 id가 다르다면 => 현재 장바구니에 없는 item
setCartitems에 원래 cartitems에 itemObj를 추가로 넣어 상태변경한다.
cartitems 요소의 id와 상품클릭한 id가 같다면 => 현재 장바구니에 있는 item
장바구니에 있는 cartitems의 요소에 수량(quantity)만 더해주고,
setCartItems에 원래 cartitems를 넣어 업데이트 한다.
⭐그런데 수량만 업데이트 될 줄 알았는데, 새롭게 itemObj가 또 추가로 들어갔다.
앞서 itemid:2,quantity:1이 들어가고, 같은 item을 또 클릭하니까
itemid:2,quantity:2가 들어가서 앞의 객체를 삭제해주기로 했다.

const cartIndex = [...cartItems].indexOf([...cartItems][i].itemId);
[...cartItems] = [...cartItems].splice(cartIndex,1);

6.마지막에 setCartItems에 [...cartItems]를 담아 상태변경해준다.

import React from 'react';

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

function ItemListContainer({ items, cartItems, setCartItems}) {
  const handleClick = (e, id) => { 
    //장바구니 담기를 클릭하면 클릭한 item이 cartItems에 담긴다.
    const itemObj = {"itemId":id, "quantity":1};

    //cartItem[i].itemId===id 클릭한 아이템아이디=장바구니에 있는 itemid가 같다면
    //이미 장바구니에 담겼으므로 수량만 추가
    //클릭한 아이템아이디=장바구니순서id가 다르다면 없는 것이므로
    //장바구니에 클릭한 아이템을 담아주기
    if(cartItems.length===0) setCartItems([...cartItems, itemObj]);

    for(let i=0; i<[...cartItems].length; i++){
      if(id!==[...cartItems][i].itemId){
        setCartItems([...cartItems, itemObj]);
      }else{
        [...cartItems][i].quantity++;
        const cartIndex = [...cartItems].indexOf([...cartItems][i].itemId);
        [...cartItems] = [...cartItems].splice(cartIndex,1);
        setCartItems([...cartItems]);
      }
    }
    console.log([...cartItems]);
  }

  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;

ShoppingCart.js

  1. props로 받아온 요소들을 적어준다.
ShoppingCart({ items, cartItems, setCartItems })
  1. 장바구니에서 cartitem의 수량을 조절할 수 있는 input의 이벤트 함수인
    handleQuantityChange의 코드를 써야 한다.
    수량변경 가능한 input박스에서 올라가는 모양을 클릭하면 수량+1,
    내려가는 모양을 클릭하면 수량-1이 바로 눈에 보여야 한다.
//CartItem.js의 input(type="number")에 들어있던 코드
//handleQuantityChange(Number(e.target.value), item.id) 

const handleQuantityChange = (quantity, itemId) => {
    const newCartItem = cartItems.filter(v => v.itemId===itemId ? v.quantity=quantity:v);
    setCartItems(newCartItem);
  }
//수량변경 작성
// itemId와 일치하는 아이템일 경우 해당 아이템 수량 변경, 일치하지 않은 아이템은 수량 그대로 두기
//else인 경우를 써주지 않는다면 itemId와 일치하는 아이템빼고 전부 사라진다.
  1. 장바구니의 체크한 cartitem의 삭제버튼을 클릭하면 이벤트 함수 handleDelete가 동작한다.
    체크한 요소들의 id와 다른 itemid를 가진 것만 setCheckedItems에 넣는다.
    cartitem들의 id와 클릭한 cartitem의 id가 다른 것만 setCartItem에 넣으므로, 삭제클릭한 cartitem은 제외된다.
 const handleDelete = (itemId) => {
    setCheckedItems(checkedItems.filter((el) => el !== itemId));
    setCartItems(cartItems.filter((el) => el.itemId !== itemId));
  }
import React, { useState } from 'react';

import CartItem from '../components/CartItem';
import OrderSummary from '../components/OrderSummary';

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) => {
    //수량변경 작성
    //handleQuantityChange(Number(e.target.value), item.id) 
    const newCartItem = cartItems.filter(v => v.itemId===itemId ? v.quantity=quantity:v);
    /*filter삼항연산자로 쓰기 전 처음 코드
    const newCartItem = cartItems.filter((el) => {
      // itemId와 일치하는 아이템일 경우 해당 아이템 수량 변경
      if (el.itemId === itemId) {
        el.quantity = quantity;
        return true;
        // filter메서드에서 주어진 반환값이 true인 요소들로 이루어진 새로운 배열을 반환함.
        // 따라서 해당 함수에서 true를 반환하는 경우 해당 아이템을 새로운 배열에 포함시키겠다는 의미.
      } else {
        return el;
      }
    });
    */
    setCartItems(newCartItem);
  }

  const handleDelete = (itemId) => {
    setCheckedItems(checkedItems.filter((el) => el !== 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>
  )
}
  1. props로 받아온 컴포넌트 적어주기
Nav({cartItems})
  1. item의 장바구니 클릭버튼을 클릭하면 장바구니에 cartitem이 담길것이고, 그러면 cartitems의 length만큼 Nav의 장바구니 옆 숫자가 바뀌어야 한다.
 <Link to="/shoppingcart">
          장바구니<span id="nav-item-counter">{cartItems.length}</span>
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;

나머지 js파일은 처음 그대로 이다.

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

CartItem.js

import React from 'react';

export default function CartItem({
  item,
  checkedItems,
  handleCheckChange,
  handleQuantityChange,
  handleDelete,
  quantity
}) {
  return (
    <li className="cart-item-body">
      <input
        type="checkbox"
        className="cart-item-checkbox"
        onChange={(e) => {
          handleCheckChange(e.target.checked, item.id)
        }}
        checked={checkedItems.includes(item.id) ? true : false} >
      </input>
      <div className="cart-item-thumbnail">
        <img src={item.img} alt={item.name} />
      </div>
      <div className="cart-item-info">
        <div className="cart-item-title" data-testid={item.name}>{item.name}</div>
        <div className="cart-item-price">{item.price}</div>
      </div>

      <input
        type="number"
        min={1}
        className="cart-item-quantity"
        value={quantity}
        onChange={(e) => {
          handleQuantityChange(Number(e.target.value), item.id)
        }}>
      </input>
      <button className="cart-item-delete" onClick={() => { handleDelete(item.id) }}>삭제</button>
    </li >
  )
}

OtherSummary.js

import React from 'react'

export default function OrderSummary({ totalQty, total }) {
  return (
    <div id="order-summary-container">
      <h4>주문 합계</h4>
      <div id="order-summary">
        총 아이템 개수 : <span className="order-summary-text">{totalQty}</span>
        <hr></hr>
        <div id="order-summary-total">
          합계 : <span className="order-summary-text">{total}</span>
        </div>
      </div>
    </div >
  )
}

state.js

export const initialState =
{
  "items": [
    {
      "id": 1,
      "name": "노른자 분리기",
      "img": "../images/egg.png",
      "price": 9900
    },
    {
      "id": 2,
      "name": "2020년 달력",
      "img": "../images/2020.jpg",
      "price": 12000
    },
    {
      "id": 3,
      "name": "개구리 안대",
      "img": "../images/frog.jpg",
      "price": 2900
    },
    {
      "id": 4,
      "name": "뜯어온 보도블럭",
      "img": "../images/block.jpg",
      "price": 4900
    },
    {
      "id": 5,
      "name": "칼라 립스틱",
      "img": "../images/lip.jpg",
      "price": 2900
    },
    {
      "id": 6,
      "name": "잉어 슈즈",
      "img": "../images/fish.jpg",
      "price": 3900
    },
    {
      "id": 7,
      "name": "웰컴 매트",
      "img": "../images/welcome.jpg",
      "price": 6900
    },
    {
      "id": 8,
      "name": "강시 모자",
      "img": "../images/hat.jpg",
      "price": 9900
    }
  ],
  
  "cartItems": [
    
  ] 
}
profile
신입 프론트엔드 웹 개발자입니다.

0개의 댓글