본문 바로가기

React

20210615 React14 : JS Array Cheat sheet, 쇼핑몰 프로젝트(ContextAPI, Hooks중심 : Providers, Contexts, Hooks)

React 14



JS Array Cheat Sheet


by TomekSulkowsi




React Project : 쇼핑몰


프로젝트 개요


  • Context API로 상품 리스트와 상품주문의 상태를 공유하고 이를 Hooks를 이용해서 변경하고 사용하는 개발 형태
  • 장바구니에 담기 기능이 있는 쇼핑몰 만들기
  • 기본적인 디자인을 보는 것이 아닌 기능과, 코드 관리에 중점을 둠



기본 컴포넌트 구성


  • Header : 쇼핑몰 안내 관련 기본적인 Header 형태
  • Prototypes : 상품이 진열 되는 곳
    • 개별 상품 : video, title, price, description, addButton
  • Order : 장바구니 역할
    • Prototypes 컴포넌트에서 addButton 클릭시 장바구니로 1개의 수량 씩 추가됨
    • 같은 품목을 중복해서 누르는 경우 장바구니에 들어있는 품목의 수량만 늘어남
    • 다른 품목을 누르는 경우 장바구니에 새로운 품목이 추가됨 (품목 기준의 장바구니 임)
  • Footer : 기본적인 footer



ContextAPI를 통한 제품 정보 및 사용 함수 관리


  • 컴포넌트 끼리 States, Props를 통해서 정보를 전달하고 활용하게 되면 점점 State, Props의 연결이 복잡하게 됨으로써 관리가 까다로워 지고 서로 영향을 주게 됨
  • ContextAPI를 통해서 전달하는 정보, 함수(Actions)를 관리하게 되면, 독립적으로 사용되어 깔끔하게 사용 할 수 있음



context 만들기


  • 기본적으로 Context Provider를 만들어 정보를 나누기전에 Context 객체가 필요함
// AppStateContext.js
import React from "react";

// context 객체 생성, defaultValue 설정
const AppStateContext = React.createContext();

export default AppStateContext;



Custom Context.Provider Component (with Children)


  • 사실은 context를 만들면 context.provider에 바로 접근하여 데이터를 공유할 컴포넌트를 묶어주면 사용준비가 끝난 상태이다.
  • 하지만, 위처럼 App.js에서 바로 context.provider로 묶고 App.js에서 공유할 정보를 작성하면 App.js가 복잡하게 느껴짐
  • 그래서, 따로 Custom context.provider이 Children을 받아들이도록 만들어서 provider를 깔끔하게 App.js에서 사용할 수 있고 공유할 정보를 독립적으로 관리할 수 있다.
  • 모든 state, method들은 이곳에서 만들어 지기 때문에 어떤 컴포넌트에서 setState를 사용하는 경우 setState를 전달할 필요도 없이 그냥 이곳에서 setState를 활용하는 해당 method를 넣어 구현하고 method를 전달하면 됨
// AppStateProvider.jsx
import { useCallback, useState } from "react";

// Context 가져오기
import AppStateContext from "../contexts/AppStateContext";

const AppStateProvider = ({ children }) => {
  // Datas : useState
  const [orders, setOrders] = useState([]);
  const [prototypes] = useState([
    {
      id: "pp-01",
      title: "Kids-story",
      artist: "Thomas Buisson",
      desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
      thumbnail:
        "https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Kids-story_1.mp4",
      price: 10,
      pieUrl: "https://cloud.protopie.io/p/8a6461ad85",
    },
    {
      id: "pp-02",
      title: "mockyapp",
      artist: "Ahmed Amr",
      desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
      thumbnail:
        "https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/mockyapp.mp4",
      price: 20,
      pieUrl: "https://cloud.protopie.io/p/27631ac9d5",
    },
    {
      id: "pp-03",
      title: "macOS Folder Concept",
      artist: "Dominik Kandravý",
      desc: "Folder concept prototype by Dominik Kandravý.",
      thumbnail:
        "https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/macOS_Folder_Concept_-_Folder_concept.mp4",
      price: 30,
      pieUrl: "https://cloud.protopie.io/p/acde5ccdf9",
    },
    {
      id: "pp-04",
      title: "Translator",
      artist: "Tony Kim",
      desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
      thumbnail:
        "https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Translator.mp4",
      price: 40,
      pieUrl: "https://cloud.protopie.io/p/b91edba11d",
    },
    {
      id: "pp-05",
      title: "In-car voice control",
      artist: "Tony Kim",
      desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
      thumbnail:
        "https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/In-car_voice_control.mp4",
      price: 50,
      pieUrl: "https://cloud.protopie.io/p/6ec7e70d1a",
    },
  ]);

  // Actions : useCallback
  const addToOrder = useCallback(() => {});
  const remove = useCallback(() => {});
  const removeAll = useCallback(() => {});

  // Children에 context로 전달하는 값 연결
  return (
    <AppStateContext.Provider
      value={{
        orders,
        prototypes,
        addToOrder,
        remove,
        removeAll,
      }}
    >
      {children}
    </AppStateContext.Provider>
  );
};

export default AppStateProvider;

  • 최종적으로 AppStateProvider 컴포넌트 자체에는 어떤 값을 줄지는 모두 설정 되어 있고, 어느 컴포넌트에 값을 공유 해줄 것인지 정하여 children으로 들어가게 해주면 됨
  • 즉, 컴포넌트 표현 형태로 공유해주고 싶은 컴포넌트들을 묶어 주어 사용하면 됨
// App.js
import Header from "./components/Header";
import Prototypes from "./components/Prototypes";
import Orders from "./components/Orders";
import Footer from "./components/Footer";
import AppStateProvider from "./providers/AppStateProvider";

function App() {
  return (
    <AppStateProvider>
      <Header />
      <div className="container">
        <Prototypes />
        <Orders />
        <Footer />
      </div>
    </AppStateProvider>
  );
}

export default App;



Context CustomHooks로 관리하기 (Hooks 폴더)

  • context에서 제공하고 있는 state 인, Prototypes, Orders를 좀더 가독성 좋게 사용하고 정리하기 위해 Hooks로 만들어 관리
    • useActions.js -> addToOrder, remove, removeAll
    • useOrders.js -> orders
    • usePrototypes.js -> Prototypes
  • useContext() 함수를 활용하여 해당 Context를 인자로 넣으면 value 형태로 값을 모두 가져올 수 있는데, 구조 분해 할당을 활용하여 사용할 값만 가져와 사용 할 수 있음
import { useContext } from "react";
import AppStateContext from "../contexts/AppStateContext";

export default function usePrototypes() {
  // useContext를 통해서 AppstateContext에서 제공하고 있는 정보 중에서 prototypes를 받음
  const { prototypes } = useContext(AppStateContext);

  return prototypes;
}
// 사용하고 싶은 곳에서 usePrototypes() 만 사용하면 쉽게 값을 가져올 수 있음
import { useContext } from "react";
import AppStateContext from "../contexts/AppStateContext";

export default function useOrders() {
  const { orders } = useContext(AppStateContext);

  return orders;
}
import { useContext } from "react";
import AppStateContext from "../contexts/AppStateContext";

export default function useActions() {
  const { addToOrder, remove, removeAll } = useContext(AppStateContext);

  return { addToOrder, remove, removeAll };
}



상품 목록 만들기 (Prototypes Component)


  • usePrototypes를 사용해서, 상품 목록 정보 가져오기 (prototypes)
  • useActions를 사용해서, 상품 추가 mothod 가져오기 (addToOrder)

  • 상품목록에서 상품을 꺼내서 각 상품별 정보 담아 오기
    • prototypes.map() -> 각 상품 prototype 구조 분해 할당으로 정보 담아옴 -> 각 상품 별 JSX 반복
    • button에 달려있는 Click 함수 정의 -> addToOrder() 에 해당 상품 id 넣어 추가
import useActions from "../hooks/useActions";
import usePrototypes from "../hooks/usePrototypes";

export default function Prototypes() {
  // 사용할 값, method 가져오기
  const prototypes = usePrototypes();
  const { addToOrder } = useActions();

  return (
    <main>
      <div className="prototypes">
        {
          // 상품 리스트에서 상품 가져와 각 상품에 대한 JSX 반복
          prototypes.map((prototype) => {
            const { id, thumbnail, title, price, desc, pieUrl } = prototype;
            const click = () => {
              addToOrder(id);
            };
            // 상품 1개에 대한 출력 모양
            return (
              <div className="prototype" key={id} /*id 추가 주의*/>
                {/*Thumnail 표현 부분*/}
                <a href={pieUrl} target="_BLANK" rel="noreferrer">
                  <div
                    style={{
                      padding: "25px 0 33px 0",
                    }}
                  >
                    <video
                      autoPlay
                      loop
                      playsInline
                      className="prototype__artwork prototype__edit"
                      src={thumbnail}
                      style={{
                        objectFit: "contain",
                      }}
                    ></video>
                  </div>
                </a>
                <div className="prototype__body">
                  {/* title, button 표현 부분*/}
                  <div className="prototype__title">
                    <div
                      className="btn btn--primary float--right"
                      onClick={click}
                    >
                      <i className="icon icon--plus" />
                    </div>
                    {title}
                  </div>
                  {/* Price, Desc 표현 부분*/}
                  <p className="prototype__price">$ {price}</p>
                  <p className="prototype__desc">{desc}</p>
                </div>
              </div>
            );
          })
        }
      </div>
    </main>
  );
}



주문 목록 만들기 (Orders Component)

import { useMemo } from "react";
import useActions from "../hooks/useActions";
import useOrders from "../hooks/useOrders";
import usePrototypes from "../hooks/usePrototypes";

export default function Orders() {
  // 사용할 값, method 가져오기
  const orders = useOrders();
  const prototypes = usePrototypes();
  const { remove, removeAll } = useActions();

  // 주문 총합계 금액 계산
  const totalPrice = useMemo(() => {
    return orders
      .map((order) => {
        const { id, quantity } = order;
        const prototype = prototypes.find((p) => p.id === id);
        return prototype.price * quantity;
      })
      .reduce((l, r) => l + r, 0);
  }, [orders, prototypes]);

  // 주문 목록에 주문이 없는 경우 JSX
  if (orders.length === 0) {
    return (
      <aside>
        <div className="empty">
          <div className="title">You don't have any orders</div>
          <div className="subtitle">Click on a + to add an order</div>
        </div>
      </aside>
    );
  }

  // 주문 목록에 주문이 있는 경우 JSX
  return (
    <aside>
      <div className="order">
        <div className="body">
          {
            // 주문 리스트(orders)에서 주문(order) 가져와 각 주문에 대한 JSX 반복
            orders.map((order) => {
              const { id } = order;

              // 주문 id와 상품 목록에 있는 상품 id가 일치하는 경우, 해당 상품 정보 가져오기
              const prototype = prototypes.find((p) => p.id === id);

              // 해당 주문 제거하는 method
              const click = () => {
                remove(id);
              };

              // 주문 품목 1개에 대한 출력 모양
              return (
                <div className="item" key={id} /*id 추가 주의*/>
                  {/*주문 상품 Thumnail*/}
                  <div className="img">
                    <video src={prototype.thumbnail}></video>
                  </div>
                  {/*주문 상품 Title*/}
                  <div className="content">
                    <p className="title">
                      {prototype.title} X {order.quantity}
                    </p>
                  </div>
                  {/*주문 품목 수량 Total Price & 품목 제거 버튼 */}
                  <div className="action">
                    <p className="price">
                      $ {prototype.price * order.quantity}
                    </p>
                    <button className="btn btn--link">
                      <i className="icon icon--cross" onClick={click}></i>
                    </button>
                  </div>
                </div>
              );
            })
          }
        </div>
        {/* 주문 목록 Total Price 창 */}
        <div className="total">
          <hr />
          <div className="item">
            <div className="content">Total</div>
            {/* Total Price 표시 */}
            <div className="action">
              <div className="price">$ {totalPrice} </div>
            </div>
            {/* 주문 목록 전체 제거 버튼*/}
            <button className="btn btn--link" onClick={removeAll}>
              <i className="icon icon--delete" />
            </button>
          </div>
          {/* 주문 하기 버튼*/}
          <button
            className="btn btn--secondary"
            style={{ width: "100%", marginTop: 10 }}
          >
            CheckOut
          </button>
        </div>
      </div>
    </aside>
  );
}



Provider Methods 로직 작성하기

  • setState에서 callback을 넣으면 해당 callback의 첫번째 매개변수는 state임
  • useCallback, useMemo를 사용하는 이유가 state, props의 변경으로 인해서 컴포넌트 자체가 rerender시 재선언 되는 현상을 제어를 위함임
  • 이화랑 블로그 : useMemo&useCallback
import { useCallback, useState } from 'react';
import AppStateContext from '../contexts/AppStateContext';

const AppStateProvider = ({ children }) => {
    const [prototypes] = useState([...]]);
    const [orders, setOrders] = useState([]);

// Methods

  const addToOrder = useCallback((id) => {
        // 버튼클릭으로 주문요청 id를 받아 orders에 해당 id와 수량 정보가 들어오게 함 ([{id, quantity: 1}] 형태)
    // useCallback으로 로직이 재선언 되는 것을 막음 (최초에 선언 되면 그 로직을 계속 활용하도록 함)
        setOrders((orders) => {
      // 주문 목록(orders)에서 주문요청 id와 주문 목록(orders)의 상품 id와 같은 상품의 정보(finded)를 가져옴
            const finded = orders.find((order) => order.id === id);

      // 주문 목록과 주문요청이 같은 것이 없는 경우
            if (finded === undefined) {
        // 원래 주문 목록에 주문요청 id 항목의 수량 1을 추가
                return [...orders, { id, quantity: 1 }];

      // 주문 목록과 주문요청이 같은 것이 있는 경우
            } else {
        // 주문 목록(orders)을 돌면서,
                return orders.map((order) => {

          // 주문목록의 상품(order)과 주문요청 상품(id)과 같으면,
                    if (order.id === id) {
            // 해당 주문목록의 상품의 경우 주문요청 상품 id에 quantity를 기존 order의 quantity에 1추가
                        return {
                            id,
                            quantity: order.quantity + 1,
                        };

          // 주문목록의 상품과 주문요청 상품이 같지 않으면,
                    } else {
            // 해당 주문목록의 상품은 그대로 둔다.
                        return order;
                    }
                });
            }
        });
    }, []);

    const remove = useCallback((id) => {
    // 주문 목록을 돌면서, 제거하는 품목의 아이디가 아닌 주문 상품만 주문목록에 설정함
        setOrders((orders) => {
            return orders.filter((order) => order.id !== id);
        });
    }, []);

    const removeAll = useCallback(() => {
    // 주문목록을 빈 배열로 설정함
        setOrders([]);
    }, []);

    return (
        <AppStateContext.Provider
            value={{
                orders,
                prototypes,
                addToOrder,
                remove,
                removeAll,
            }}
        >
            {children}
        </AppStateContext.Provider>
    );
};

export default AppStateProvider;