본문 바로가기
프로그래밍/웹 개발

[React] useState Hook과 Props로 쇼핑몰 상태관리

by 제이콥J 2021. 8. 22.

코드스테이츠의 과제로 쇼핑몰 애플리케이션의 주요 기능을 구현했다.

그 과정에서 app.js, ItemListContainer.js, ShoppingCart.js 파일의 코드를 자세하게 정리해보려고 한다.

 

먼저 전체 진행 과정은 아래와 같다.

 

1. 장바구니의 기능 구현하기

  - [장바구니 담기] 버튼을 이용해 장바구니에 해당 상품이 추가되도록 구현

  - 장바구니 내 [삭제] 버튼을 이용해 장바구니의 상품이 제거되도록 구현

  - 장바구니의 상품 갯수의 변동이 생길 때마다, 상단 내비게이션 바에 상품 갯수가 업데이트되도록 구현

 

2. app.js 파일에서 state와 메소드를 생성하고 웹 페이지인 ItemListContainer와 ShoppingCart에 props으로 전달

 

3. itemListContainer와 ShoppingCart에서는 cartItem, Item, Nav, OrderSummary 컴포넌트를 사용

 

4. 상품 목록(items)과 장바구니 목록(cartItems) 상태로 관리하기 위해 React의 Hook을 사용

// 상품 목록 (items)
{
  "id": 1,
  "name": "노른자 분리기",
  "img": "../images/egg.png",
  "price": 9900
}

// 장바구니 목록 (cartItems)
{
  "itemId": 1,
  "quantity": 1
}

 

5. 상품 목록과 장바구니 목록의 초기값은 initialState 객체에 저장됨

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": [
    {
      "itemId": 1,
      "quantity": 1
    },
    {
      "itemId": 5,
      "quantity": 7
    },
    {
      "itemId": 2,
      "quantity": 3
    }
  ]
}

 


app.js에서 상태 관리

1. 상태 관리 : 상품 목록(items)과 장바구니 목록(cartItems)을 app.js에서 관리

2. 장바구니 기능을 구현하여 상태를 변경하기 위한 메소드 제작

3. 상태와 메소드를 props로 하위 컴포넌트에 전달하기

 

코드 작성

- 중첩 구조 분해를 활용하여 itemId와 quantity의 키와 값을 간단하게 전달함 { itemId, quantity }

- 상태(State)뿐 아니라 상태를 변경하는 함수(setState)를 담은 메소드를 props로 함께 전달하기

- 덕분에 다른 컴포넌트에서 상태(State)를 변경할 수 있음

 

import React, { useState } from 'react';
import Nav from './components/Nav';
import ItemListContainer from './pages/ItemListContainer';
import './App.css';
import {
  BrowserRouter as Router,
  Switch,
  Route,
} from "react-router-dom";
import ShoppingCart from './pages/ShoppingCart';
import { initialState } from './assets/state';

function App() {
  
  // items와 cartItems을 state로 관리
  const [items, setItems] = useState(initialState.items);
  const [cartItems, setCartItems] = useState(initialState.cartItems);
  
  // 장바구니에 아이템을 추가하여 cartItems에 넣는 메소드
  const addToCart = (itemId) => {
    
    const found = cartItems.filter((el) => el.itemId === itemId)[0]
    
    // 이미 장바구니에 있는 상품을 추가하는 경우, 해당 요소의 quantity의 숫자를 1 올려주기
    if (found) {
      setQuantity(itemId, found.quantity + 1)
    }
    else { // 장바구니에 없는 상품을 추가할 경우, cartItems에 새로운 엘리먼트로 추가하기
      setCartItems([...cartItems, {
        itemId,
        quantity: 1
      }])
    }
  }
  
  // 이미 장바구니에 있는 상품의 cartItems의 quantity를 변경하는 메소드
  const setQuantity = (itemId, quantity) => {
    
    // itemId로 배열에서 해당 상품을 찾고, 그것의 인덱스를 구하기
    const found = cartItems.filter((el) => el.itemId === itemId)[0]
    const idx = cartItems.indexOf(found)
    
    // 배열에 삽입할 객체 형태의 엘리먼트 선언하기
    const cartItem = {
      itemId,
      quantity
    }
    
    // quantity값이 변경되었으므로 기존의 엘리먼트를 삭제하고 새로운 엘리먼트 삽입
    setCartItems([
      ...cartItems.slice(0, idx),
      cartItem,
      ...cartItems.slice(idx + 1)
    ])
  }
  
  // 상품을 장바구니에서 삭제하는 메소드
  const handleDelete = (itemId) => {
    setCartItems(cartItems.filter((ele)=>{
      return ele.itemId !== itemId
    }))
  }


  return (
    <Router>
      <Nav cartItems={cartItems} />
      <Switch>
        <Route exact={true} path="/">
          <ItemListContainer handleAdd={addToCart} items={items} />
        </Route>
        <Route path="/shoppingcart">
          <ShoppingCart 
          cartItems={cartItems} 
          items={items} 
          handleDelete={handleDelete}
          handleQuantityChange={setQuantity}  />
        </Route>
      </Switch>
    </Router>
  );
}

export default App;

 


ItemListContainer 페이지 구현

1. map 함수, props으로 전달받은 item 배열, item 컴포넌트를 사용하여 상품 목록 구현

2. map 함수의 두 번째 전달인자를 이용하여 item 컴포넌트의 key를 인덱스로 설정하기

3. 이벤트 핸들러와 handleAdd 메소드를 활용하여 app.js의 상태(state)를 변경하기

 

코드 작성

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

function ItemListContainer({ items, handleAdd  }) {
  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={() => {
          handleAdd(item.id)
        }} />)}
      </div>
    </div>
  );
}

export default ItemListContainer;

 


ShoppingCart 페이지 구현

1. 객체 구조 분해 할당의 콜론 패턴을 사용하여 props를 전달 받기 (필수는 아님)

2. props로 받은 메소드를 다시 props로 전달할 때 : 새로운 함수를 만들어 props로 전달 그 안에서 메소드 실행

 

코드 작성

import React, { useState } from 'react'
import CartItem from '../components/CartItem'
import OrderSummary from '../components/OrderSummary'

// handleDelete는 onDelete로, handleQuantityChange는 onQuantityChange로 받기
export default function ShoppingCart({ items, cartItems, handleDelete: onDelete, 
  handleQuantityChange : onQuantityChange }) {
  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([]);
    }
  };

  // props로 받은 onQuantityChange를 함수 안에서 실행하기
  const handleQuantityChange = (quantity, itemId) => {
    onQuantityChange(itemId, quantity)
  }

  // props로 받은 onDelete를 함수 안에서 실행하기
  const handleDelete = (itemId) => {
    setCheckedItems(checkedItems.filter((el) => el !== itemId))
    onDelete(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>
  )
}

 

반응형

댓글