[ React ] 상태관리(recoil) & Portals & ReactNode & modal

CJY00N·2023년 7월 12일
0

react

목록 보기
3/10
post-thumbnail

⚡️ 장바구니 상태관리(recoil)

선택된 상품에 대한 상태관리를 하기 위해 checkedCartState를 recoil로 관리한다.
▼ src/recoils/cart.ts

export const checkedCartState = atom<Cart[]>({
  key: "cartState",
  default: [],
});
});

⚡️ 결제창 연결하기

Pick

  • 기존 타입에서 원하는 속성만 선택하여 새로운 타입을 생성하는 역할을 한다.
  • Pick을 사용하면 기존 타입에서 필요한 일부 속성만 선택하여 새로운 타입을 만들 수 있다.
Pick<Cart, "imageUrl" | "title" | "price">

itemData 컴포넌트 분리하기

▼ src/components/cart/itemData.tsx

const ItemData = ({
  imageUrl,
  title,
  price,
}: Pick<Cart, "imageUrl" | "title" | "price">) => {
  return (
    <div>
      <img className="cart-item_img" src={imageUrl} />
      <p className="cart-item_title">{title}</p>
      <p className="cart-item_price">{price}</p>
    </div>
  );
};

결제예정 컴포넌트(WillPay) 추가하기

  • 장바구니 창에서 상품을 선택하면(체크박스 체크) 결제예정 컴포넌트에 추가된다. (map)
  • 선택된 상품들은 checkedCartState(recoil)에서 받아온다.
  • 결제할 상품이 있으면 payment창으로 이동한다.
  • 장바구니 페이지에 WillPay 컴포넌트를 추가한다.

▼ src/components/cart/willPay.tsx

const WillPay = () => {
  const navigate = useNavigate();
  const checkedItems = useRecoilValue(checkedCartState);
  const totalPrice = checkedItems.reduce((res, { price, amount }) => {
    res += price * amount;
    return res;
  }, 0);

  const handleSubmit = () => {
    if (checkedItems.length) {
      navigate("/payment");
    } else {
      alert("결제 할 상품이 없어요.");
    }
  };
  return (
    <div className="cart-willpay">
      <ul>
        {checkedItems.map(({ imageUrl, price, title, amount, id }) => (
          <li key={id}>
            <ItemData
              imageUrl={imageUrl}
              price={price}
              title={title}
              key={id}
            />
            <p>수량 :{amount}</p>
            <p>금액 :{price * amount} </p>
          </li>
        ))}
      </ul>
      <span>총 예상결제액 : {totalPrice}</span>
      <button onClick={handleSubmit}>결제하기</button>
    </div>
  );
};

⚡️ 상품 선택 핸들링하기

  • 상품의 수량을 변경하거나, 상품을 체크하거나, 삭제할 때 아래 WillPay 컴포넌트에 즉각 반영되지 않는 이슈가 발생한다.
  • items가 변경되거나 현재 선택된 상품(formData)가 변경되면 다시 로드하기 위해 빈 배열에 체크된 상품들을 push하여 checkedCartData를 업데이트(setCheckedCartData)한다.

▼ src/components/cart/index.tsx

  const formRef = useRef<HTMLFormElement>(null);

  useEffect(() => {
    const checkedItems = checkboxRefs.reduce<Cart[]>((res, ref, i) => {
      if (ref.current!.checked) res.push(items[i]);
      return res;
    }, []);
    setCheckedCartData(checkedItems);
  }, [items, formData]);
  • 장바구니 페이지에서 상품을 선택한 후 다른 페이지에 이동했다가 다시 장바구니로 가면 선택된 체크박스들이 체크해제되는 이슈가 발생한다. (체크박스의 속성)
  • setAllcheckedFromItems 함수는 개별 항목이 선택되어있을 때 모두 선택이 되어있다면 상단의 전체선택 체크박스가 체크되도록하는 함수이다. (기존 코드에서 리팩토링)
  const [formData, setFormData] = useState<FormData>();

  const setAllcheckedFromItems = () => {
    if (!formRef.current) return;
    const data = new FormData(formRef.current);
    const selectedCount = data.getAll("select-item").length;
    const allchecked = selectedCount === items.length;
    formRef.current.querySelector<HTMLInputElement>(
      ".cart_select-all"
    )!.checked = allchecked;
  };
  • 장바구니 페이지가 새로 렌더링될 때 기존에 체크되었던 항목들의 체크가 유지도록되기 위해 useEffect를 사용한다.
  • checkedCartData로 부터 어떤 상품들이 선택되어있는지 확인하여 실제 체크박스에 체크가 반영되도록 한다.
  • 체크박스가 모두 선택되었을 때 전체선택 체크박스도 체크되게 하기 위해 setAllcheckedFromItems() 함수를 호출한다.
  useEffect(() => {
    checkedCartData.forEach((item) => {
      const itemRef = checkboxRefs.find(
        (ref) => ref.current!.dataset.id === item.id
      );
      if (itemRef) itemRef.current!.checked = true;
    });
    setAllcheckedFromItems();
  }, []);
  • 기존의 코드에서 setItemsCheckedFromAll 함수를 분리하였다.
  • 전체선택 체크박스가 체크되었을 경우 모든 체크박스를 선택(체크)한다.
  const setItemsCheckedFromAll = (targetInput: HTMLInputElement) => {
    const allchecked = targetInput.checked;
    checkboxRefs.forEach((inputElem) => {
      inputElem.current!.checked = allchecked;
    });
  };
  • 함수들을 분리하여 리팩토링한 후 handleCheckboxChanged 함수도 수정해주었다.
  const handleCheckboxChanged = (e?: SyntheticEvent) => {
    if (!formRef.current) return;
    const targetInput = e?.target as HTMLInputElement;
    if (targetInput && targetInput.classList.contains("cart_select-all")) {
      setItemsCheckedFromAll(targetInput);
    } else {
      setAllcheckedFromItems();
    }
    const data = new FormData(formRef.current);
    setFormData(data);
  };

⚡️ 결제페이지 모달 추가

Portals - createProtal

https://ko.legacy.reactjs.org/docs/portals.html

ReactDOM.createPortal(child, container)
  • React의 createPortal은 React 애플리케이션에서 DOM의 다른 부분에 컴포넌트를 렌더링하는 기능을 제공하는 메소드이다.
  • createPortal은 이러한 문제를 해결하기 위해 도입된 메소드이다. 이 메소드를 사용하면 컴포넌트를 부모 컴포넌트의 DOM 트리의 외부로 렌더링할 수 있다.

ReactNode

  • ReactNode는 React에서 사용되는 타입 중 하나로, 컴포넌트가 렌더링할 수 있는 모든 종류의 데이터를 나타냅니다. 이는 JSX 구문에서 컴포넌트의 자식 요소로 전달되는 모든 값을 포함한다.

최상단 index.html의 body에 id가 modal인 div를 추가한다. 이 곳에 Portal을 통해 모달 컴포넌트가 렌더링된다.
▼ index.html

  <body>
    <div id="root"></div>
    <div id="modal"></div>
  </body>

modal 컴포넌트에 childrun이 ReactNode이고, id가 modal인(위에서 정의) 요소를 createPortal하여 리턴하는 컴포넌트 ModalPortal을 정의한다.
▼ src/components/payment/modal.tsx

const ModalPortal = ({ children }: { children: ReactNode }) => {
  return createPortal(children, document.getElementById("modal")!);
};
  • 모달의 show 여부를 props로 전달받아 true일 때만 모달 화면이 보이게 한다.
  • 위에서 정의한 ModalPortal 컴포넌트를 호출하고, 해당 컴포넌트로 감싼 부분이 modal 화면에 들어가게 된다.
  • 예/ 아니오 버튼을 각각 클릭했을 때 수행될 함수도 props로 전달받는다.
const PaymentModal = ({  show,  proceed,  cancel}: {  show: boolean;  proceed: () => void;  cancel: () => void;
}) => {
  return (
    show && (
      <ModalPortal>
        <div className={`modal ${show ? "show" : ""}`}>
          <div className="modal_inner">
            <p>정말 결제하시겠습니까?</p>
            <div>
              <button onClick={proceed}></button>
              <button onClick={cancel}>아니오</button>
            </div>
          </div>
        </div>
      </ModalPortal>
    )
  );
};

결제 쿼리 작성

  • 결제할 상품들의 아이디를 전달한다.

▼ src/graphql/payment.ts

export const EXECUTE_PAY = gql`
  mutation EXCUTE_PAY($info: [string]) {
    payInfo(info: $info)
  }
  • 결제할 상품들은 현재 장바구니에서 삭제된다.

결제 쿼리 핸들러함수 작성

▼ src/mocks/handlers.ts

  graphql.mutation(EXECUTE_PAY, ({ variables: ids }, res, ctx) => {
    ids.forEach((id: string) => {
      delete cartData[id];
    });
    return res(ctx.data(ids));
  }),

결제 컴포넌트 작성(모달 호출)

  • 결제창에서는 현재 결제할 상품들 목록을 보여주고 결제하기 버튼을 클릭하면 결제 모달창을 띄운다.
  • 모달창에서 예를 클릭하면 수행될 함수 proceed는 현재 체크된 상품들(결제할 상품들)의 id값들을 payInfos에 저장하고, 위에서 정의한 쿼리 EXECUTE_PAY에 전달한다.
  • 결제가 완료되었다는 알림창을 띄우고, 상품 목록 페이지로 이동한다.
type PaymentInfos = string[];

const Payment = () => {
  const { mutate: executePay } = useMutation((payInfos: PaymentInfos) =>
    graphqlFetcher(EXECUTE_PAY, payInfos)
  );

  const proceed = () => {
    const payInfos = checkedCartData.map(({ id }) => id);
    executePay(payInfos);
    setCheckedCartData([]);
    alert("결제가 완료되었습니다.");
    navigate("/products", { replace: true });
  };




profile
COMPUTER SCIENCE ENGINEERING / Web Front End

0개의 댓글