PJH's Shopping Mall - 결제

λ°•μ •ν˜ΈΒ·2022λ…„ 12μ›” 27일

Shopping Project

λͺ©λ‘ 보기
5/11
post-thumbnail

πŸš€ Start

이제 κ²°μ œνŽ˜μ΄μ§€λ₯Ό 생성해보렀고 ν•œλ‹€. 그러면 μ•žμ„œ μž₯λ°”κ΅¬λ‹ˆμ—μ„œ μ²΄ν¬ν•œ μƒν’ˆλ°μ΄ν„°λ₯Ό κ·ΈλŒ€λ‘œ κ²°μ œνŽ˜μ΄μ§€λ‘œ κ°€μ Έμ™€μ•Όν•œλ‹€.

βœ… λ”°λΌμ„œ, recoil을 톡해 μƒνƒœκ΄€λ¦¬λ₯Ό 해보도둝 ν•˜μž!



⭐️ Recoil

recoil의 μƒνƒœκ΄€λ¦¬κ°€ ν•„μš”ν•œ 곳은 체크된 μƒν’ˆμ΄ κ²°μ œμ˜ˆμ • μƒν’ˆμœΌλ‘œ 좜λ ₯λ˜μ•Όν•˜λ©°, κ²°μ œνŽ˜μ΄μ§€μ—λ„ 좜λ ₯λ˜μ–΄μ•Όν•œλ‹€.


βœ”οΈ κ²°μ œμ˜ˆμ •μ°½(Will Pay)

βœ… μš°μ„ , handleCheckedChangedκ°€ 싀행될 λ•Œ 즉, μƒν’ˆμ²΄ν¬κ°€ λ λ•Œ ν•΄λ‹Ή μƒν’ˆ 데이터λ₯Ό μ „λ‹¬ν•˜μž.

  • checkedItems : reduce을 체크된 μƒν’ˆλ°μ΄ν„°λ₯Ό λˆ„μ  ν•©μ‚°ν•œ κ°’

  • useRecoilState : atom ν˜Ήμ€ selector의 값을 읽고 μ“°λ €κ³  ν•  λ•Œ μ‚¬μš©. μƒνƒœμ˜ λ³€κ²½ 사항을 λ‹€μ‹œ λ Œλ”λ§ν•˜κΈ° μœ„ν•΄ ꡬ성 μš”μ†Œλ₯Ό ꡬ독. (μ°Έκ³ )

  • useEffect: items, formData의 변경에 따라 λžœλ”λ§

  • 정리 : λ°μ΄ν„°λ“€μ˜ 체크된 μƒνƒœκ°’ κ·ΈλŒ€λ‘œ recoil Atom(μƒνƒœ μ €μž₯μ†Œ?)에 전달.
// components/cart/indext.tsx
const CartList = ({ items }: { items: CartType[] }) => {
  const [checkedCartData, setCheckedCartData] = useRecoilState(checkedCartState);
  const [formData, setFormData] = useState<FormData>();


  ...
  
  useEffect(() => {
    const checkedItems = checkboxRefs.reduce<CartType[]>((res, ref, i) => {
      if (ref.current!.checked) res.push(items[i]);
      return res;
    }, []);
    setCheckedCartData(checkedItems);
  }, [items, formData]);

  ...
};

βœ… Atom 객체 배열에 체크된 데이터 전달.

  • Atom이 μ—…λ°μ΄νŠΈλ˜λ©΄ ν•΄λ‹Ή Atom을 κ΅¬λ…ν•˜κ³  있던 λͺ¨λ“  μ»΄ν¬λ„ŒνŠΈλ“€μ΄ μƒˆλ‘œμš΄ κ°’μœΌλ‘œ λ¦¬λ Œλ”λ§.
    λ˜ν•œ, μ—¬λŸ¬ μ»΄ν¬λ„ŒνŠΈμ—μ„œ 같은 Atom을 κ΅¬λ…ν•˜κ³  있으면 κ·Έ μ»΄ν¬λ„ŒνŠΈλ“€μ΄ μƒνƒœλ₯Ό λ™μΌν•˜κ²Œ κ³΅μœ ν•œλ‹€.
// src/recoils/cart.ts
import { atom } from "recoil";
import { CartType } from "../graphql/cart";

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

βœ… 데이터 κ°€μ Έμ˜€κΈ°

  • useRecoilValue(): Recoil μƒνƒœκ°€ λ³€κ²½λ˜λŠ” 경우 ꡬ성 μš”μ†Œλ₯Ό κ΅¬λ…ν•˜μ—¬ μž¬λžœλ”λ§λ˜μ–΄ μ—…λ°μ΄νŠΈλœ 값을 κ°€μ Έμ˜¨λ‹€.
// components/willPay/indext.tsx
const WillPay = (...) => {
  const checkedItems = useRecoilValue(checkedCartState);
	...

  return (
    <div className="cart-willpay">
      <ul>
        {checkedItems.map(({ imageUrl, price, title, amount, id }) => (
          <li key={id}>
            <ItemData imageUrl={imageUrl} price={price} title={title} />
            <p>μˆ˜λŸ‰: {amount}</p>
            <p>κΈˆμ•‘: {price * amount}</p>
          </li>
        ))}
      </ul>
  	...
    </div>
  );
};

βœ… κ²°μ œμ˜ˆμ • μ°½ 좜λ ₯

// components/cart/index.tsx
const CartList = ({ items }: { items: CartType[] }) => {
    const navigate = useNavigate();
    const [checkedCartData, setCheckedCartData] = useRecoilState(checkedCartState);
  		...
    const handleSubmit = () => {
      if (checkedCartData.length) navigate("/payment");
      else alert("κ²°μ œν•  λŒ€μƒμ΄ μ—†μ–΄μš”");
    };
  
  return (
  	...
    <WillPay submitTitle="결제창으둜" handleSubmit={handleSubmit} />

  )
  
}

// components/willPay/indext.tsx
const WillPay = ({
  submitTitle,
  handleSubmit,
}: {
  submitTitle: string;
  handleSubmit: (e: SyntheticEvent) => void;
}) => {

	...
    return (
    ...
    <button onClick={handleSubmit}>{submitTitle}</button
    )

}
 

βœ… 결제창 좜λ ₯ μ •μƒμž‘λ™ 확인



⛔️ μ²΄ν¬λ°•μŠ€ μƒνƒœκ°’ κΈ°μ–΅ν•˜κΈ° + λ¦¬νŒ©ν„°λ§

체크된 μƒν’ˆμ— λŒ€ν•˜μ—¬ 데이터가 잘 μ „λ‹¬λ˜λŠ” 것을 ν™•μΈν–ˆλ‹€. ν•˜μ§€λ§Œ κ²°μ œνŽ˜μ΄μ§€λ‘œ 이동 ν›„ λ‹€μ‹œ μž₯λ°”κ΅¬λ‹ˆ νŽ˜μ΄μ§€λ‘œ 이동할 경우 체크가 ν’€λ € μ²΄ν¬ν–ˆλ˜ 데이터가 날라가버린닀.


βœ… λ”°λΌμ„œ, μž₯λ°”κ΅¬λ‹ˆνŽ˜μ΄μ§€κ°€ λžœλ”λ§λ  λ•Œ μž₯λ°”κ΅¬λ‹ˆμ˜ μƒν’ˆ 쀑에 μ²΄ν¬λ˜μ—ˆλ˜ μƒν’ˆμ˜ id와 같은 데이터λ₯Ό μ°Ύμ•„μ„œ check값이 trueκ°€ 되게 λ§Œλ“ λ‹€.

  • data-*: data-* μ „μ—­ 속성은 μ‚¬μš©μž μ§€μ • 데이터 μ†μ„±μ΄λΌλŠ” 속성 클래슀λ₯Ό ν˜•μ„±ν•˜λ©°, 이λ₯Ό 톡해 슀크립트λ₯Ό 톡해 HTMLκ³Ό ν•΄λ‹Ή DOM ν‘œν˜„ 간에 독점 정보λ₯Ό κ΅ν™˜ κ°€λŠ₯.

  • dataset: HTMLElement μΈν„°νŽ˜μ΄μŠ€μ˜ 읽기 μ „μš© 속성인 dataset은 μš”μ†Œμ˜ μ‚¬μš©μž μ •μ˜ 데이터 속성(data-*)에 λŒ€ν•œ 읽기/μ“°κΈ° μ•‘μ„ΈμŠ€λ₯Ό μ œκ³΅ν•œλ‹€. 각 data-* 속성에 λŒ€ν•œ ν•­λͺ©μ΄ μžˆλŠ” λ¬Έμžμ—΄ λ§΅(DOMStringMap)을 λ…ΈμΆœ.

// comonents/cart/CartItem.tsx
<input ... data-id={id} ... />


// components/cart/index.tsx
useEffect(() => {
    checkedCartData.forEach((item) => {
      const itemRef = checkboxRefs.find(
        (ref) => ref.current!.dataset.id === item.id
      );
      if (itemRef) itemRef.current!.checked = true;
    });
    setAllCheckedFromItems();
  }, []);

βœ… setAllCheckedFromItemsλŠ” κ°œλ³„μ μœΌλ‘œ μƒν’ˆμ„ μ²΄ν¬ν–ˆμ„ 경우 λ™μž‘ν•˜λŠ” ν•¨μˆ˜

  • κ°œλ³„μ μœΌλ‘œ μ²΄ν¬ν•˜μ—¬ μ „μ²΄μ„ νƒκΉŒμ§€ μ΄λ£¨μ–΄μ§€λŠ” λ™μž‘κ³Ό μ€‘λ³΅λ˜λŠ” λ‘œμ§μ΄λ―€λ‘œ λ³„λ„μ˜ ν•¨μˆ˜λ‘œ λ§Œλ“€μ–΄μ£Όμ—ˆλ‹€
  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>(".select-all")!.checked =
      allChecked;
  };

βœ… λ°˜λŒ€λ‘œ 전체선택을 ν΄λ¦­ν•˜μ—¬ λͺ¨λ“  μ²΄ν¬λ°•μŠ€κ°€ μ²΄ν¬λ˜λŠ” 경우λ₯Ό setItemsCheckedFromAll ν•¨μˆ˜λ‘œ.

 const setItemsCheckedFromAll = (targetInput: HTMLInputElement) => {
    const allChecked = targetInput.checked;
    checkboxRefs
      .filter((inputElem) => {
        return !inputElem.current!.disabled;
      })
      .forEach((inputElem) => {
        inputElem.current!.checked = allChecked;
      });
  };

βœ… λ”°λΌμ„œ, μ•žμ„œ λ§Œλ“  handleCheckedboxChanged ν•¨μˆ˜λŠ” λ‹€μŒκ³Ό 같이 μ‘°κΈˆλ” κ°„λ‹¨ν•˜κ²Œ μž‘μ„±

const handleCheckboxChanged = (e?: SyntheticEvent) => {
    if (!formRef.current) return;
    const targetInput = e?.target as HTMLInputElement;
    if (targetInput && targetInput.classList.contains("select-all")) {
      setItemsCheckedFromAll(targetInput);
    } else {
      setAllCheckedFromItems();
    }
    const data = new FormData(formRef.current);
    setFormData(data);
  };

βœ… μž₯λ°”κ΅¬λ‹ˆ 데이터 및 체크 μœ μ§€ 확인



βœ”οΈ κ²°μ œνŽ˜μ΄μ§€

이제 κ²°μ œνŽ˜μ΄μ§€λ₯Ό λ§Œλ“€μ–΄λ³΄μž. κ²°μ œνŽ˜μ΄μ§€μ—μ„œ νŠΉλ³„νžˆ λ™μž‘ν•˜λŠ” κΈ°λŠ₯은 μ—†κ³ , 체크된 μƒν’ˆμ„ λΆ€λŸ¬μ˜€κ³ , κ²°μ œν•˜κΈ°μ— 따라 λ°μ΄ν„°μ˜ μ‚­μ œ 및 μœ μ§€λ₯Ό λ‚˜νƒ€λ‚΄μ•Ό ν•œλ‹€.


βœ… 체크된 μƒν’ˆ 즉, κ²°μ œμ˜ˆμ •μ°½μ— 좜λ ₯된 데이터듀을 κ°€μ Έμ˜€κΈ°

  • Recoil둜 인해 μ „μ—­ μƒνƒœ 관리 되고 μžˆλŠ” 체크된 μƒν’ˆλ°μ΄ν„°λ₯Ό λΆˆλŸ¬μ˜€λŠ” μ»΄ν¬λ„ŒνŠΈμ˜€λ˜ WillPay μ»΄ν¬λ„ŒνŠΈλ₯Ό κ·ΈλŒ€λ‘œ 뢈러였자.
// components/payment/index.tsx
const Payment = () => {
 ...

  return (
    <div>
      <WillPay submitTitle="κ²°μ œν•˜κΈ°" handleSubmit={showModal} />
      ...
    </div>
  );
};

βœ… κ²°μ œν•˜κΈ° ν΄λ¦­μ‹œ λͺ¨λ‹¬μ°½ 좜λ ₯ν•˜κΈ°

  • WillPay μ»΄ν¬λ„ŒνŠΈμ˜ κ²°μ œν•˜κΈ° ν΄λ¦­μ‹œ showModal이 μ‹€ν–‰λ˜μ–΄ λͺ¨λ‹¬μ°½μ΄ 좜λ ₯.
// components/payment/index.tsx
const Payment = () => {
  const navigate = useNavigate();
  const [modalShow, setModalShow] = useState(false);
 	...

  const showModal = () => {
    setModalShow(true);
  };

  const proceed = () => {
   ...
  };

  const cancel = () => {
    setModalShow(false);
  };

  return (
    <div>
      <WillPay submitTitle="κ²°μ œν•˜κΈ°" handleSubmit={showModal} />
      <PaymentModal show={modalShow} proceed={proceed} cancel={cancel} />
    </div>
  );
};
  • React Portal을 μ΄μš©ν•˜μ—¬ μ›ν•˜λŠ” 값을 λͺ¨λ‹¬μ°½μ— 좜λ ₯ν•  수 μžˆλ‹€.(μ°Έκ³ )
    • Portal: λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ˜ DOM 계측 ꡬ쑰 λ°”κΉ₯에 μžˆλŠ” DOM λ…Έλ“œλ‘œ μžμ‹μ„ λ Œλ”λ§ν•˜λŠ” 졜고의 방법
// index.html
...
<div id='modal'><div/> 
...
// components/payment/modal.tsx
const ModalPortal = ({ children }: { children: ReactNode }) => {
  return createPortal(children, document.getElementById("modal")!);
};

const PaymentModal = ({
  show,
  proceed,
  cancel,
}: {
  show: boolean;
  proceed: () => void;
  cancel: () => void;
}) => {
  return show ? (
    <ModalPortal>
    // show true,false에 λ”°λ₯Έ λͺ¨λ‹¬μ°½ CSS 적용
      <div className={`modal ${show ? "show" : ""}`}>
        <div className="modal__inner">
          <p>정말 κ²°μ œν• κΉŒμš”?</p>
          <div>
            <button onClick={proceed}>예</button>
            <button onClick={cancel}>μ•„λ‹ˆμ˜€</button>
          </div>
        </div>
      </div>
    </ModalPortal>
  ) : null;
};

βœ… κ²°μ œν•˜κΈ°μ— λ”°λ₯Έ 데이터 μœ μ§€ 및 μ‚­μ œ κ΅¬ν˜„

λ§Œμ•½ κ²°μ œν•˜κΈ°λ₯Ό ν΄λ¦­ν•˜λ©΄ 체크된 μƒν’ˆμ€ μž₯λ°”κ΅¬λ‹ˆμ—μ„œ μ‚­μ œν•˜κ³ , μž₯λ°”κ΅¬λ‹ˆμ—λŠ” μ²΄ν¬λ˜μ§€μ•Šμ•˜λ˜ μƒν’ˆλ°μ΄ν„°λ“€μ΄ λ‚¨μ•„μžˆμ–΄μ•Όν•œλ‹€.

1️⃣ Modal창의 예λ₯Ό 클릭할 경우 proceedκ°€ μ‹€ν–‰λœλ‹€.

  • executePay(ids,... ): checkλ˜μ—ˆλ˜ μƒν’ˆ 데이터 id값듀을 인자둜 전달.
  • onSucess: ν•¨μˆ˜ 정상 μ‹€ν–‰ ν›„ λ™μž‘.
    • setCheckedCartData([]) : μ²΄ν¬μƒνƒœ μ΄ˆκΈ°ν™”
    • alert: 결제 μ •μƒμ μœΌλ‘œ μ™„λ£Œλ˜μ—ˆλ‹€λŠ” μ•Œλ¦Ό
    • navigate: μƒν’ˆλͺ©λ‘νŽ˜μ΄μ§€λ‘œ 이동, replace true둜 ν•˜μ—¬ μ΄μ „νŽ˜μ΄μ§€λ‘œ 이동을 μ œν•œ
// components/payment/index.tsx
type PaymentInfos = string[];

const Payment = () => {
  const navigate = useNavigate();
  const [checkedCartData, setCheckedCartData] = useRecoilState(checkedCartState);
 	...
  const { mutate: executePay } = useMutation((ids: PaymentInfos) =>
    graphqlFetcher(EXECUTE_PAY, ids)
  );

  ...

  const proceed = () => { // 1️⃣ 번
    const ids = checkedCartData.map(({ id }) => id);
    executePay(ids, {
      onSuccess: () => {
        setCheckedCartData([]);
        alert("결제 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.");
        navigate("/products", { replace: true });
      },
    });
  };

  ...


};


2️⃣ 체크된 μƒνƒœμ˜ 데이터듀은 cartData(μž₯λ°”κ΅¬λ‹ˆ 데이터)μ—μ„œ μ‚­μ œ

// src/mocks/handlers.ts
export const handlers = [
	...
  graphql.mutation(EXECUTE_PAY, ({ variables: ids }, res, ctx) => { // 2️⃣ 번
    ids.forEach((id: string) => {
      delete cartData[id];
    });
    return res(ctx.data(ids));
  }),
];
// src/graphql/payment.ts
import { gql } from "graphql-tag";

export const EXECUTE_PAY = gql`
  mutation EXECUTE_PAY($ids: [ID!]) {
    payInfo(info: $info)
  }
`;

βœ… 선택 μƒν’ˆ 결제 ν›„ μ‚­μ œ 및 미선택 μƒν’ˆ μž₯λ°”κ΅¬λ‹ˆμ— μœ μ§€λ˜λŠ” 것을 확인

profile
κΈ°λ‘ν•˜μ—¬ κΈ°μ–΅ν•˜κ³ , κ³„νšν•˜μ—¬ μ‹€μ²œν•˜μž. will be a FE developer (HOMEλ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ Notion으둜 λ†€λŸ¬μ˜€μ„Έμš”!)

0개의 λŒ“κΈ€