[Lululemon 클론 프로젝트] 장바구니 페이지

목표

상품 추가 / 삭제 / 수량 변경이 가능한 장바구니 페이지 구현


과정

1. 상품 데이터 받아오기
1) 사용자가 장바구니에 넣은 상품들의 목록을 렌더링하기위해 fetch 함수로 서버에서 데이터를 받아온 후 'itemList' 변수에 저장해두었다 .
2) 사용자를 식별한 다음 그에 맞는 데이터를 불러와야하기때문에 request header에 로그인시 발급했던 토큰 인가 과정을 추가했다.


코드🔻

const Bag = () => {
  const [itemList, setItemList] = useState([]);

  useEffect(() => {
    fetch('http://10.58.3.71:8000/carts', {
      method: 'GET',
      headers: {
        Authorization: localStorage.getItem('token'),
      },
    })
      .then(res => res.json())
      .then(data => {
        setItemList(data.results);
      });
  }, []);
};




2. 상품 수량 변경하기
1) 원래는 lululemon의 사이트처럼 드랍다운 형식으로 수량 선택 버튼을 만드려고했지만 팀원분들과 이야기 후에 +, - 버튼으로 수량을 하나씩 조절할 수 있도록 구현했다.
2) 사용자가 장바구니 페이지에서 수량을 변경했을경우 DB의 데이터도 변경되어야하기때문에 fetch 함수와 PATCH 메서드를 사용했다.


코드🔻

const Item = ({ item, itemList, setItemList, onRemove }) => {
    const { image, price, cart_id, quantity, name, color, size } = item;

    const plusQuantity = () => {
      fetch(`http://10.58.3.71:8000/carts/${cart_id}`, {
        method: 'PATCH',
        headers: {
          Authorization: localStorage.getItem('token'),
        },
        body: JSON.stringify({
          quantity: quantity + 1,
        }),
      }).then(res => {
        if (res.ok) {
          fetch('http://10.58.3.71:8000/carts', {
            method: 'GET',
            headers: {
              Authorization: localStorage.getItem('token'),
            },
          })
            .then(res => res.json())
            .then(data => {
              setItemList(data.results);
            });
        }
      });
    };

    const minusQuantity = () => {
      if (quantity === 1) {
        alert('YOU CANNOT CHOOSE LESS THAN ONE');
      } else {
        fetch(`http://10.58.3.71:8000/carts/${cart_id}`, {
          method: 'PATCH',
          headers: {
            Authorization: localStorage.getItem('token'),
          },
          body: JSON.stringify({
            quantity: quantity - 1,
          }),
        }).then(res => {
          if (res.ok) {
            fetch('http://10.58.3.71:8000/carts', {
              method: 'GET',
              headers: {
                Authorization: localStorage.getItem('token'),
              },
            })
              .then(res => res.json())
              .then(data => {
                setItemList(data.results);
              });
          }
        });
      }
    };
  //(...)
    return (
     //(...)
        <button onClick={minusQuantity}>-</button>
        <div className="quantityInput">{quantity}</div>
        <button onClick={plusQuantity}>+</button>
    //(...)    
    );
  };




3. 상품 합산 가격 계산하기
상품들의 총 가격 합계를 구하기 위해서 사용자가 장바구니에 담은 상품 각각의 수량과 가격을 곱한 다음 모든 상품들의 합산 가격을 다시 더하는 함수를 작성하려고했는데, 백엔드 팀에서 각 상품마다 그 상품의 가격과 구매 수량을 곱한 total_price 데이터를 만들어 보내주셔서 장바구니에 담긴 모든 상품들의 total_price 데이터를 더하는 함수로 간단하게 구현할 수 있었다.


코드🔻

const OrderSummary = ({ itemList }) => {
  const getTotalPrice = itemList => {
    let sum = 0;
    for (let i = 0; i < itemList.length; i++) {
      sum += itemList[0].total_price;
    }
    return sum;
  };

  return (
      //(...)
        <div className="summaryValue">
            <p>$ {getTotalPrice(itemList)}</p>
            <p>FREE</p>
            <p>Calculated at checkout</p>
            <p>USD $ {getTotalPrice(itemList)}</p>
        </div>
      //(...)  
  );
};




4. 상품 숨기기
lululemon 사이트에는 장바구니에 담긴 상품을 나중에 다시 구매할 수 있도록 일시적으로 숨겨주는 버튼이 있는데, 데이터를 아예 지워버리면 다시 불러오기가 힘들것같아 filter 메서드를 사용해 'save for later' 버튼을 누른 상품들을 제외하고 렌더링해주는 onRemove 함수를 작성해 구현했다.


코드🔻

const Bag = () => {
  //(...)
  const onRemove = useCallback(
    id => {
      setItemList(itemList.filter(item => item.cart_id !== id));
    },
    [itemList]
  );

  return (
      //(...)
        <Item
            itemList={itemList}
            setItemList={setItemList}
            item={item}
            key={item.cart_id}
            onRemove={onRemove}
        />
     //(...)   
  );
};
const Item = ({ item, itemList, setItemList, onRemove }) => {
//(...)
  return (
      //(...)
        <div className="productFooter">
          <p>Free Shipping + Free Returns</p>
          <div className="footerBtns">
            <button
              onClick={() => {
                onRemove(cart_id);
              }}
            >
              Save for Later
            </button>
      //(...)
  );
};




5. 상품 삭제하기
장바구니에서 상품을 삭제할 경우 DB에서도 지워질 수 있도록 fetch 함수와 DELETE 메서드를 사용해 구현했다.


코드🔻

const deleteItem = () => {
    fetch(`http://10.58.3.71:8000/carts/${cart_id}`, {
      method: 'DELETE',
      headers: {
        Authorization: localStorage.getItem('token'),
      },
    });
  };




6. 빈 장바구니 페이지 렌더링하기
1) 장바구니 페이지에서 모든 상품을 삭제하면 구매를 유도하는 문구를 띄워주어야했는데, 조건부 렌더링을 활용해서 장바구니에 담긴 itemList의 길이가 0일때는 상품 목록과 결제정보를 숨기고 만들어둔 emptyBag 컴포넌트를 보여줄 수 있도록 구현했다.
2) itemList 데이터를 useEffect를 사용해서 받아왔는데, 이 경우 처음 mount가 될 때 useEffect가 실행되지 않아 useState의 초기값을 반환하기때문에 빈 배열이 들어와버려서 itemList의 길이에 따른 조건부 렌더링이 내 의도대로 되지 않았다. 이 에러는 itemList의 길이 조건을 설정해주는 부분에 itemList가 falsy값이 아닌 경우에만 렌더링이 실행될 수 있도록 && 연산자를 활용한 조건부 렌더링을 한번 더 사용해서 해결할 수 있었다.


코드🔻

const Bag = () => {
//(...)
  return (
    //(...)
      {itemList && itemList.length !== 0 ? (
        <div className="bagContainer">
          <div className="leftContainer">
            <h1>
              My Bag <span>({itemList.length} Items)</span>
            </h1>
            { itemList?.map(item => {
                return (
                  <Item
                    itemList={itemList}
                    setItemList={setItemList}
                    item={item}
                    key={item.cart_id}
                    onRemove={onRemove}
                  />
                );
              })}
          </div>
          <OrderSummary itemList={itemList} />
        </div>
      ) : (
        <EmptyBag goToMain={goToMain} />
      )}
    //(...)  
  );
};




시연 영상


회고

처음엔 어디서부터 시작을 해야할지 감조차 오지 않아서 장바구니 페이지를 구현하기에는 아직 내 실력이 많이 부족하다고 느껴졌고 자책도 많이 했었는데, 2주 후에 돌아보니 이 힘들었던 시간들 덕분에 오히려 내가 더 성장할 수 있었던 것 같다. 조건부 렌더링, fetch 함수 등 이전에는 개념으로만 공부했던 것들을 실제 웹페이지 제작에 활용해보면서 좀 더 확실히 이해할 수 있게 되었고, 막히는 부분이 생기면 스스로 공부도 하고 고민도 하다가 끝내 해결해냈던 모든 과정들이 앞으로 내가 더 나아갈 수 있도록 자신감을 심어주고 갔다. 사실 처음 해보는 프로젝트였던 만큼 개인 작업에서도, 협업에서도 서툰 부분들이 많았지만 늘 웃는 얼굴로 대해주셨던 팀원 분들께 정말 감사했다는 말씀을 드리고 싶다🥺