React 쇼핑몰 클론코딩 : 3. 옵션 선택 시 인터랙티브 반응, 로컬스토리지 장바구니 저장

Gom·2021년 4월 10일
1

Project

목록 보기
6/15
post-thumbnail

기능적 요소

📝 상품 옵션 선택, 장바구니

로직

상세 페이지에서 상품 옵션 선택 : 상품 옵션을 선택할 때마다 선택한 옵션을 화면에 추가로 보여주기 위해 옵션을 useState로 두고 값이 업데이트 될 때마다 화면에 반영시켰다.
장바구니 : 장바구니에 정보는 변동이 잦고 서버에 저장해야 할 정도로 의미있는 데이터는 아니라는 판단 하에 로컬스토리지에 저장하는 방식을 택했다.

상세코드

1. 상세페이지에서 옵션 선택 시 화면에 추가시키기

1.1 옵션 정보 업데이트

//Detail.js
...
  const [options, setOption] = React.useState([]);
  const [sum, setSum] = React.useState(0);

  //상품 옵션을 선택하면 화면에 elements 추가 렌더링
  const selectOption = (e) => {
    const _options = options.filter((o, idx) => {
      if (o !== e.target.value) {
        return o;
      }
    });
    let price = Number(e.target.value.slice(-7, -1).split(",").join("").trim());
    setOption([
      ..._options,
      { text: e.target.value, num: 1, prd: prd, sum: price },
    ]);
    setSum(sum + price);
  };
  • selectbox의 옵션을 선택하면 useState로 지정해둔 options가 업데이트 된다.
  • 조건문을 둔 이유는 동일한 옵션의 증감은 수량 조절 기능을 이용하면 되므로 화면에 중복으로 추가되지 않도록 방지하기 위함이다.
  • 옵션 데이터는 가격을 포함하여 하나의 string으로 주어졌기 때문에 가격 정보를 추출하기 위해 문자열을 가공하여 price라는 변수에 저장했다.
  • 장바구니 컴포넌트에서 이용해야 하는 정보들을 key:value 형태로 추가하여 상품 옵션별로 하나의 딕셔너리로 저장하였다. 이는 여러 문제점을 마주하며 수정 끝에 정해진 데이터 형태인데 발생했던 문제들은 아래와 같다.
    문제1) num값을 저장하는 이유 : 수량 조절 시마다 합계 금액이 변경되어야 한다는 것에만 초점을 맞추어 num값을 useState로 지정한 적이 있다. 그러자 수량 조절 버튼을 클릭할 때 다른 옵션의 num값도 다 같이 변동됐다. 하나의 상품 상세페이지이지만 옵션별 수량은 달리 관리해야 한단 것을 깨닫고 num값을 옵션 정보에 포함시켜 저장하게 됐다.
    문제2) prd, 상품 정보를 option 내에 저장해두는 이유 : 처음에는 전체 배열에 상품 리스트가 존재하고 각 상품 내부에 상품 옵션별 딕셔너리들이 저장된 형태로 저장하였다. 이런 식으로 말이다. [상품{상품옵션1{text, num}, 상품옵션{text, num}}, 상품2{상품2의옵션1{text, num}, 상품2의옵션2{text, num}}]
    그러자 장바구니 컴포넌트에서 데이터를 꺼내 렌더링할 때 옵션 정보에 접근하기 위해서는 이중 map을 이용해야 한다는 점이 불편하고 오류도 많이 발생했다. 구글링해보니 이중으로 map을 사용하는 사례도 많지 않았다. 또한 상품 이미지나 이름과 같은 상품 정보는 그 외부에서 가져와야 했다. 참조해야 할 위치가 제각각이라 결국 옵션 정보를 저장할 때 옵션이 속해있는 상품에 대한 정보도 함께 저장해둠으로써 이용하기 편하게 했다.
    문제3) sum값을 저장하는 이유 : 상세 페이지에서는 해당 상품의 옵션 정보만 보이지만 장바구니로 가면 여러 상품에서 선택한 옵션들이 모두 보여지고 전체 합계를 구해야 한다. 장바구니 컴포넌트에서 재계산을 하려면 JSON.parse, 문자열 가공하여 금액 추출, 숫자형태로 변환 절차를 거쳐야 하므로 이미 계산해둔 값을 함께 넘겨줌으로써 중복되는 절차를 줄이고자 했다.

1.2 렌더링

              {options && //선택한 옵션이 존재할 때 화면 렌더링
                options.map((option, idx) => {
                  return (
                    <Grid padding="16px" key={idx}>
                      <Grid
                        bg="rgb(248, 248, 248)"
                        height="98px"
                        padding="16px"
                      >
                        <Grid is_flex padding="0 0 14px 0">
                          <Text margin="0" size="13px">
                            {option.text.includes("[") ? (
                              <H>[{option.text.split("[")[1].split("]")[0]}]</H>
                            ) : null}
                            {option.text.split("[")[0].split("(")[0]}
                          </Text>
                          <svg
                            onClick={() => {
                              deleteOption(option);
                            }}
                            class="css-1jd8bf1"
                            xmlns="http://www.w3.org/2000/svg"
                            width="12"
                            height="12"
                            viewBox="0 0 12 12"
                          >
                            <g
                              fill="none"
                              fill-rule="evenodd"
                              stroke="#C4C4C4"
                              stroke-linecap="square"
                            >
                              <path
                                d="M0 0L10.5 10.5"
                                transform="translate(.75 .75)"
                              ></path>
                              <path
                                d="M0 0L10.5 10.5"
                                transform="translate(.75 .75) matrix(-1 0 0 1 10.5 0)"
                              ></path>
                            </g>
                          </svg>
                        </Grid>
                        <Grid height="28px" is_flex center>
                          <Grid is_flex width="116px">
                            <Grid
                              _onClick={() => {
                                minusQuantity(option);
                              }}
                              width="28px"
                              height="28px"
                              bg="rgb(255, 255, 255)"
                            >
                              -
                            </Grid>
                            <Grid
                              width="50px"
                              height="28px"
                              bg="rgb(255, 255, 255)"
                              margin="0 5px"
                            >
                              {option.num ? option.num : 1}
                            </Grid>
                            <Grid
                              _onClick={() => {
                                plusQuantity(option);
                              }}
                              width="28px"
                              height="28px"
                              bg="rgb(255, 255, 255)"
                            >
                              +
                            </Grid>
                          </Grid>
                          <Text margin="0" size="14px">
                            {Number(
                              option.text
                                .slice(-7, -1)
                                .split(",")
                                .join("")
                                .trim()
                            ) * option.num}
                          </Text>
                        </Grid>
                      </Grid>
                    </Grid>
                  );
                })}

2. 선택한 옵션의 수량 조절과 삭제

  //선택한 옵션의 수량 변경 시 수량 및 가격 반영
  const plusQuantity = (option) => {
    let price = Number(option.text.slice(-7, -1).split(",").join("").trim());
    setSum(sum + price);
    option.num += 1;
    option.sum += price;
  };
  const minusQuantity = (option) => {
    if (sum > 0 && option.num > 1) {
      let price = Number(option.text.slice(-7, -1).split(",").join("").trim());
      setSum(sum - price);
      option.num -= 1;
      option.sum -= price;
    }
  };  //옵션 삭제
  const deleteOption = (target) => {
    const _options = options.filter((option, idx) => {
      if (option.text !== target.text) {
        return option;
      }
      setSum(
        sum -
          Number(target.text.slice(-7, -1).split(",").join("").trim()) *
            option.num
      );
    });
    setOption(_options);
  };
  • 수량 조절 버튼을 클릭하면 해당 옵션의 수량과 합계 금액을 변동시킨다.
  • 옵션을 삭제할 때는 기존에 저장되어있던 옵션의 수량을 고려한 금액을 전부 차감해준다. num값을 곱해주는 것을 누락하는 바람에 옵션이 삭제될 때 옵션 1개만큼의 가격만 차감되고 나머지 금액이 합계 금액에 남아있는 문제가 발생했었다.

3. 장바구니 담기를 클릭하면 현재까지의 옵션 정보가 로컬스토리지에 저장된다.

  //장바구니 담기 시 로컬 스토리지에 정보를 저장
  const setLocalStorage = () => {
    const _cart = localStorage.getItem("cart");
    if (_cart) {
      const parseCart = JSON.parse(_cart);
      localStorage.setItem("cart", JSON.stringify([...parseCart, ...options]));
    } else {
      localStorage.setItem("cart", JSON.stringify(options));
    }
    //저장 여부를 알리고 페이지 이동 의사를 묻는 알림창
    swal({
      title: "장바구니에 잘 담겼어요!",
      icon: "success",
      buttons: {
        showCart: { text: "장바구니 이동", value: "showCart" },
        cancel: "쇼핑 계속하기",
      },
    }).then((value) => {
      switch (value) {
        case "showCart":
          history.push("/cart");
          break;
      }
    });
  • 값이 하나일 때는 그 값이 boolean이든 string, number 관계 없지만 나처럼 배열로 된 여러 값을 로컬스토리지에 저장하려면 JSON 형태로 변환해야 한다. JSON.stringify를 이용하여 로컬스토리지에 저장될 수 있는 데이터로 변환하였다.
  • 조건문이 없다면 다른 상품 페이지에서 장바구니 담기가 이루어질때마다 동일한 key값인 "cart"의 값은 덮어씌워질 뿐 이전에 저장한 상품 정보를 누적시키지 못한다. 그래서 로컬스토리지에 이미 저장되어 있는 장바구니 정보가 있는 경우는 그 값을 가져와 새로운 값을 추가한 배열로 만든 뒤 로컬스토리지에 저장한다.
    문제1) localStorage.getItem("cart") 의 문제점 : 원하는 자료 형태로 저장이 되지 않아 애를 먹었다. 로컬스토리지에서 가져온 기존에 저장되어 있던 상품 정보를 스프레드 문법으로 나누니 배열 안에 딕셔너리대로 나뉘는 것이 아니라 문자열 하나하나로 나누어 지는 것이 아니겠는가. 알고보니 JSON.stringify를 통해 문자열화되어 로컬스토리지에 저장되었기 때문에 getItem을 해왔을 때도 문자열 형태였던 것이다. 원래의 형태인 배열의 특성을 이용하여 가공하기 위해서는 JSON.parse 절차를 반드시 거쳐야 했다! 덕분에 로컬스토리지의 특성을 잘 알게 되었다.

profile
안 되는 이유보다 가능한 방법을 찾을래요

0개의 댓글