본문 바로가기

E-commerce Vanila JS [6편] Cart 제작

by Recstasy 2021. 12. 8.

 


1 Cart기능 구현하기

1) index.js 추가하기

- 카트 페이지를 랜더링하는 라우트와 랜더링파일을 추가한다.

 


  import {parseRequestUrl} from './utils.js';
  import HomeScreen from './screens/HomeScreen.js';
  import ProductScreen from './screens/ProductScreen.js';
  import Error404Screen from './screens/Error404Screen.js';
  import CartScreen from './screens/CartScreen.js';
 
  const routes = {
      '/' : HomeScreen,
      '/product/:id':ProductScreen,
      '/cart/:id': CartScreen,
      '/cart': CartScreen,
  }
 
  const router = async() => {
      const request = parseRequestUrl();
      const parseUrl = (request.resource ? `/${request.resource}` : '/') +
                       (request.id ? '/:id' : '' ) +
                       (request.verb ? `/${request.verb}` : '' );
 
      const screen = routes[parseUrl] ? routes[parseUrl] : Error404Screen;
      const mainContainer = document.getElementById('main-container');
     
      mainContainer.innerHTML = await screen.render();
      if(screen.after_render) await screen.after_render();
  }
 
  window.addEventListener('load', router);
  window.addEventListener('hashchange', router);

[ root\frontend\src\ index.js ]

 

 

2) LocalStorage.js 생성하기

- 카트에 담는 아이템은 클라이언트의 로컬스토러지에 저장한다.

 


  export const getCartItems = () => {
      const cartItems = localStorage.getItem('cartItems')
                        ? JSON.parse(localStorage.getItem('cartItems'))
                        : [];
      return cartItems;
  }
 
  export const setCartItems = (cartItems) => {
      localStorage.setItem('cartItems', JSON.stringify(cartItems));
  }

[ root\frontend\src\localStorage.js ]

 

 

3) CartScreen.js 생성하기

- CartScreen.js는 카트 페이지를 랜더링한다.

 

- 일단 서버에서 데이터를 받는 테스트를 진행한다. addToCart 함수는 localStorage에 저장된 데이터와 사용자가 선택한 객체를 비교하여 장바구니에 존재하는 상품인지 체크한 뒤, 사용자가 요청한 데이터를 로컬스토러지에 저장한다. 이와 관련하여 '.find( )' 메서드를 사용하는데, DB에 존재하는 단 하나의 값을 배열로 추출하는 상황에서는 find( )메서드가 제격이다. 만일 여러개의 배열값을 반환하기를 원한다면, .filter( )메서드를 사용하는 편이 적합하다.

 


 
import { parseRequestUrl } from "../utils";
  import { getProduct } from "../api";
  import { getCartItems, setCartItems } from "../localStorage";
 
  const addToCart = (item, forceUpdate=false) => {
      let cartItems = getCartItems();
      const existItem = cartItems.find((x) => x.product === item.product);
      if(existItem){
          cartItems = cartItems.map((x) => {
              x.product === existItem.product ? item : x
          });
      }else{
          cartItems = [...cartItems, item];
      }
      setCartItems(cartItems);
  }
 
  const CartScreen = {
      after_render: () => {
          return `CartScreen after_render`
      },
      render: async() => {
          const request = parseRequestUrl();
          if(request.id){
              const product = await getProduct(request.id);
              addToCart({
                  product: product._id,
                  name: product.name,
                  image: product.image,
                  price: product.price,
                  format: product.format,
                  qty:1,
              })
          }
          return `
              <div> Cart Screen Rendering </div>
              <div> ${getCartItems()} </div>
          `;
      },
  };
 
  export default CartScreen;

[ root\frontend\src\screens\CartScreen.js ]

 

 

- forceUpdate속성에는 boolean값을 넣어서 로컬스토러지의 저장유무를 판단하는 지표로 사용한다.

- existItem조건문의 경우, 기존의 데이터셋에서 사용자가 요청한 상품을 .map( )메서드로 찾은 뒤, 'true'일 경우에는 해당 객체에 덮어쓰기를 한다. 이와 같이 데이터의 값을 변경하는 경우에는 .map( )메서드를 사용하는 편이 좋다.

 

 

 

 


2 장바구니 페이지 구현

1) cartScreen.js 수정하기(랜더링)

- express템플릿(pug, ejs, handlebar)을 사용하지 않다보니, 코드가 다소 긴 것 같지만 내용은 간단하다. 로컬스토러지의 데이터를 받아서 프론트 페이지를 생성하는 마크업태그가 전체코드의 반 이상을 차지하고 있다. 핵심은 장바구니 페이지를 랜더링하는 return 이후의 코드에 있다.

 


  import { parseRequestUrl, rerender } from "../utils";
  import { getProduct } from "../api";
  import { getCartItems, setCartItems } from "../localStorage";

  // 장바구니 물품추가
  const addToCart = (item, forceUpdate=false) => {
      let cartItems = getCartItems();
     
      const existItem = cartItems.find((x) => x.product === item.product);
      if(existItem){
          if(forceUpdate){
              cartItems = cartItems.map((x) =>
                  x.product === existItem.product ? item : x
              );
          }
      }else{
          cartItems = [...cartItems, item];
      }

      setCartItems(cartItems);
    
  }
 
  const CartScreen = {
      after_render: () => {
   
      },
      render: async() => {
          const request = parseRequestUrl();
          if(request.id){
              const product = await getProduct(request.id);
              addToCart({
                  product: product._id,
                  name: product.name,
                  image: product.image,
                  price: product.price,
                  format: product.format,
                  countInStock: product.countInStock,
                  qty:1,
              })
          }
              const cartItems = getCartItems();
          return `
          <div class="content cart">
        <div class="cart-list">
          <ul class="cart-list-container">
            <li>
              <h3>Shopping Cart</h3>
              <div>Price</div>
            </li>
            ${
              cartItems.length === 0
                ? '<div>Cart is empty. <a href="/#/">Go Shopping</a>'
                : cartItems
                    .map(
                      (item) => `
              <li>
                <div class="cart-image">
                  <img src="${item.image}" alt="${item.name}" />
                </div>
                <div class="cart-name">
                  <div>
                    <a href="/#/product/${item.product}">
                      ${item.name}
                    </a>
                  </div>
                  <div class="qty-area">
                    <div class="qty-board">
                      Qty: ${ item.qty }
                    </div>
                    <div class="qty-btn" id="${item.product}">
                      <i class="ri-arrow-up-s-line qty-select" id="cart-up"></i>
                      <i class="ri-arrow-down-s-line qty-select" id="cart-down"></i>
                    </div>
                    <button type="button" class="delete-button" id="${item.product}">
                      Delete
                    </button>
                  </div>
                </div>
                <div class="cart-price">
                  $${item.price}
                </div>
              </li>
              `
                    )
                    .join('\n')
            }
          </ul>
        </div>
        <div class="cart-action">
            <h3>
              Subtotal (${cartItems.reduce((a, c) => a + c.qty, 0)} items)
              :
              $${cartItems.reduce((a, c) => a + c.price * c.qty, 0)}
            </h3>
            <button id="checkout-button" class="primary fw">
              Proceed to Checkout
            </button>
        </div>
      </div>
          `;
      },
  };
 
  export default CartScreen;


[ root\frontend\src\screens\CartScreen.js ]

 

 

 

2) style.css 추가하기

- 카트와 관련된 css를 추가한다.

- 장바구니 'up&down' 아이콘의 경우에는 오픈소스, 'Remix ICON'을 사용한다. 리믹스 아이콘의 CDN은 index.html파일상단의 <head></head>부분에 붙여넣는다. 

(https://github.com/Remix-Design/remixicon#usage)

 

 
/* Cart */
  .cart {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
  }
 
  .cart-list {
    flex: 3 1 60rem;
  }
  .cart-action {
    flex: 1 1 20rem;
    background-color: #f0f0f0;
    border-radius: 0.5rem;
    padding: 1rem;
  }
  .cart-list-container {
    padding: 1rem;
    list-style-type: none;
  }
 
  .cart-list-container li {
    display: flex;
    justify-content: space-between;
    padding-bottom: 1rem;
    margin-bottom: 1rem;
    border-bottom: 0.1rem #c0c0c0 solid;
  }
  .cart-list-container img {
    max-width: 10rem;
    max-height: 10rem;
  }
  .cart-list-container li:first-child {
    align-items: flex-end;
  }
  .cart-image {
    flex: 1 1;
  }
  .cart-name {
    flex: 8 1;
  }
  .cart-price {
    flex: 1 1;
    text-align: right;
  }
  .cart-name > div {
    padding: 1rem;
  }
  .cart-list h3 {
    margin: 0;
  }
  .cart-list button,
  .cart-list select {
    font-size: 1.3rem;
    padding: 0.5rem;
  }

  .qty-area{
      display:flex;
  }

  .qty-board{
      display:flex;
      align-items:center;
      justify-content:center;
  }

  .qty-btn{
      display:flex;
      flex-direction: column;
      padding:.1rem 1.3rem;
      cursor: pointer;
  }

[ root\frontend\style.css ]

 

 

 

 

 

 


3 장바구니 업데이트 구현

1) cartScreen.js 수정하기

- 사용자가 장바구니를 수정함에 따라 로컬스토러지의 내용도 변경되어야만 한다. 사용자의 반응이 발생한 이후의 단계이므로  after_render: 메서드를 사용해야한다. 장바구니 물품에 관한 이벤트는 'up', 'down'이며, 해당 클래스의 product ID값을 받아서 로컬스토러지의 물품개수(item.qty)를 수정한다.

(//장바구니 물품개수 조절 이벤트 & 기능 부분 )

 


  
import { parseRequestUrl, rerender } from "../utils"// rerender 해체할당 추가
  import { getProduct } from "../api";
  import { getCartItems, setCartItems } from "../localStorage";

  // 장바구니 물품추가
  const addToCart = (item, forceUpdate=false) => {
      let cartItems = getCartItems();
     
      const existItem = cartItems.find((x) => x.product === item.product);
      if(existItem){
          if(forceUpdate){
              cartItems = cartItems.map((x) =>
                  x.product === existItem.product ? item : x
              );
          }
      }else{
          cartItems = [...cartItems, item];
      }
      setCartItems(cartItems);

   // 장바구니 업데이트:: forceUpdate값이 true일때,
      if(forceUpdate){
          rerender(CartScreen);
      }
  }
 
  const CartScreen = {
      after_render: () => {
       
    // 장바구니 물품개수 조절 이벤트 & 기능
          const qtySelects = document.getElementsByClassName('qty-select');
         
          Array.from(qtySelects).forEach( (qtySelect) => {
 
              qtySelect.addEventListener('click', (e) => {
                  let itemQty
                  const btnID = e.target.id;
                  const item = getCartItems().find((x) => x.product === e.target.parentNode.id);
                 
                  itemQty = (btnID === 'cart-up') ? item.qty+=1 : item.qty-=1;
                  itemQty = (itemQty === 0) ? 1 : itemQty;
               
                  addToCart({...item, qty:itemQty }, true);        
              })
          })

      },
      render: async() => {

     // 중략...

[ root\frontend\src\screens\CartScreen.js ]

 

 

2) utils.js 추가하기

- 사용자의 변경요청이 있을 경우, CartScreen.js의 재귀가 이뤄져야 한다.(옵저버 패턴) 이와 관련된 옵저퍼 패턴을 utils.js에 구현해준다.

 


 
export const parseRequestUrl = () => {
      const url = document.location.hash.toLowerCase();
     
      const request = url.split('/');
      return {
          resource: request[1],
          id: request[2],
          action : request[3]
      }
  }
 
  export const rerender = async(component) => {
      document.getElementById('main-container')
      .innerHTML = await component.render();
      await component.after_render();
  }

[ root\frontend\src\utils.js ]

 

 

 

 

 

 

 

3 장바구니 삭제

1) cartScreen.js 삭제버튼 추가

- 장바구니의 상품을 삭제할 수 있는 삭제버튼을 추가한다.

 


  
import { parseRequestUrl, rerender } from "../utils";
  import { getProduct } from "../api";
  import { getCartItems, setCartItems } from "../localStorage";

  // 중략...
 
  const CartScreen = {
      after_render: () => {
          const qtySelects = document.getElementsByClassName('qty-select');
          Array.from(qtySelects).forEach( (qtySelect) => {
 
              qtySelect.addEventListener('click', (e) => {
                  let itemQty
                  const btnID = e.target.id;
                  const item = getCartItems().find((x) => x.product === e.target.parentNode.id);
                 
                  itemQty = (btnID === 'cart-up') ? item.qty+=1 : item.qty-=1;
                  itemQty = (itemQty === 0) ? 1 : itemQty;
               
                  addToCart({...item, qty:itemQty }, true);        
              })
          })

  // 장바구니 물품삭제 버튼 추가
          const deleteButtons = document.getElementsByClassName('delete-button');
          Array.from(deleteButtons).forEach((deleteButton) => {
              deleteButton.addEventListener('click', () => {
                  removeFromCart(deleteButton.id);
              });
          });
          document.getElementById('checkout-button').addEventListener('click', () => {
              document.location.hash = '/signin';
          });
      },

      render: async() => {

  // 중략...

[ root\frontend\src\screens\CartScrenn.js ]

 

 

 

2) cartScreen.js 삭제기능추가

- 장바구니의 상품을 삭제할 수 있는 기능, 'removeFromCart 함수를 추가한다. removeFromCart함수는 인자값으로 id를 받은 뒤, 해당 id를 제외한 데이터(로컬스토러지)를 filter()메서드로 저장한다.( setCartItems() ) 

 


  
import { parseRequestUrl, rerender } from "../utils";
  import { getProduct } from "../api";
  import { getCartItems, setCartItems } from "../localStorage";

  const addToCart = (item, forceUpdate=false) => {
      let cartItems = getCartItems();
     
      const existItem = cartItems.find((x) => x.product === item.product);
      if(existItem){
          if(forceUpdate){
              cartItems = cartItems.map((x) =>
                  x.product === existItem.product ? item : x
              );
          }
      }else{
          cartItems = [...cartItems, item];
      }
      setCartItems(cartItems);
      if(forceUpdate){
          rerender(CartScreen);
      }
  }
 
  // 장바구니 물품 삭제 기능 추가
  const removeFromCart = (id) => {
      setCartItems(getCartItems().filter((x) => x.product !== id));
      if (id === parseRequestUrl().id) {
        document.location.hash = '/cart';
      } else {
        rerender(CartScreen);
      }
  };

  // 중략...

[ root\frontend\src\screens\CartScrenn.js ]

 

 

- CartScreen.js의 최종코드는 아래와 같다.

다음 포스팅에서는 몽고DB를 통해 사용자를 등록하고, 계정을 생성해보자. 

 


  
import { parseRequestUrl, rerender } from "../utils";
  import { getProduct } from "../api";
  import { getCartItems, setCartItems } from "../localStorage";

  // 장바구니 물품추가
  const addToCart = (item, forceUpdate=false) => {
      let cartItems = getCartItems();
     
      const existItem = cartItems.find((x) => x.product === item.product);
      if(existItem){
          if(forceUpdate){
              cartItems = cartItems.map((x) =>
                  x.product === existItem.product ? item : x
              );
          }
      }else{
          cartItems = [...cartItems, item];
      }
      setCartItems(cartItems);
      if(forceUpdate){
          rerender(CartScreen);
      }
  }
 
  // 장바구니 물품 삭제 기능
  const removeFromCart = (id) => {
      setCartItems(getCartItems().filter((x) => x.product !== id));
      if (id === parseRequestUrl().id) {
        document.location.hash = '/cart';
      } else {
        rerender(CartScreen);
      }
  };
 
  const CartScreen = {
      after_render: () => {
   // 장바구니 물품개수 조절 이벤트 & 기능
          const qtySelects = document.getElementsByClassName('qty-select');
         
          Array.from(qtySelects).forEach( (qtySelect) => {
 
              qtySelect.addEventListener('click', (e) => {
                  let itemQty
                  const btnID = e.target.id;
                  const item = getCartItems().find((x) => x.product === e.target.parentNode.id);
                 
                  itemQty = (btnID === 'cart-up') ? item.qty+=1 : item.qty-=1;
                  itemQty = (itemQty === 0) ? 1 : itemQty;
               
                  addToCart({...item, qty:itemQty }, true);        
              })
          })

  // 장바구니 물품삭제 버튼 이벤트
          const deleteButtons = document.getElementsByClassName('delete-button');
          Array.from(deleteButtons).forEach((deleteButton) => {
              deleteButton.addEventListener('click', () => {
                  removeFromCart(deleteButton.id);
              });
          });
          document.getElementById('checkout-button').addEventListener('click', () => {
              document.location.hash = '/signin';
          });
      },
      render: async() => {
          const request = parseRequestUrl();
          if(request.id){
              const product = await getProduct(request.id);
              addToCart({
                  product: product._id,
                  name: product.name,
                  image: product.image,
                  price: product.price,
                  format: product.format,
                  countInStock: product.countInStock,
                  qty:1,
              })
          }
              const cartItems = getCartItems();
          return `
          <div class="content cart">
        <div class="cart-list">
          <ul class="cart-list-container">
            <li>
              <h3>Shopping Cart</h3>
              <div>Price</div>
            </li>
            ${
              cartItems.length === 0
                ? '<div>Cart is empty. <a href="/#/">Go Shopping</a>'
                : cartItems
                    .map(
                      (item) => `
              <li>
                <div class="cart-image">
                  <img src="${item.image}" alt="${item.name}" />
                </div>
                <div class="cart-name">
                  <div>
                    <a href="/#/product/${item.product}">
                      ${item.name}
                    </a>
                  </div>
                  <div class="qty-area">
                    <div class="qty-board">
                      Qty: ${ item.qty }
                    </div>
                    <div class="qty-btn" id="${item.product}">
                      <i class="ri-arrow-up-s-line qty-select" id="cart-up"></i>
                      <i class="ri-arrow-down-s-line qty-select" id="cart-down"></i>
                    </div>
                    <button type="button" class="delete-button" id="${item.product}">
                      Delete
                    </button>
                  </div>
                </div>
                <div class="cart-price">
                  $${item.price}
                </div>
              </li>
              `
                    )
                    .join('\n')
            }
          </ul>
        </div>
        <div class="cart-action">
            <h3>
              Subtotal (${cartItems.reduce((a, c) => a + c.qty, 0)} items)
              :
              $${cartItems.reduce((a, c) => a + c.price * c.qty, 0)}
            </h3>
            <button id="checkout-button" class="primary fw">
              Proceed to Checkout
            </button>
        </div>
      </div>
          `;
      },
  };
 
  export default CartScreen;

[  root\frontend\src\screens\CartScreen.js  ]

 

댓글

최신글 전체

이미지
제목
글쓴이
등록일