취업 리부트 코스 7주차 WIL

김선은·2024년 5월 7일
1

취업 리부트코스

목록 보기
20/20

이번 주 항해 취업 리부트코스에서 내가 구현한 기능은 무엇인가요?

  • 장바구니에 담은 상품으로 가상 결제를 구현하기.

해당 기능을 구현하기 위해, 어떤 기술적 의사결정을 거쳤나요?

  • 장바구니와 결제와 관련되어 전역으로 상태를 참조하고 업데이트를 하는 경우가 많으니 Context API를 이용했다.
  • Provider 함수 내부에 상태와 관련된 로직을 작성하여 page 부분의 컴포넌트에서는 주로 Provider 함수 내에서 작성된 함수를 사용하고 상태를 참조하는 구조로 진행했다.
  • 상태가 복잡하지 않기에 context API를 사용해도 괜찮다고 생각했으며 최적화 부분에서 단점이 있기에 화면에서 보여지는 비율이 큰 상품리스트 컴포넌트에 React.memo를 적용했다.

기능 구현사항

3주차 기능 구현 요구 사항을 간략히 소개하면 다음과 같다.

  • 상품 구매 기능
  • 결제 SDK 연동
  • 판매자 주문 상태 변경 가능

구매자가 장바구니에 담은 상품으로 구매를 진행해야해서 결제 SDK를 연동해 가상 결제를 진행하고, 결제가 완료되면 주문 상태를 DB에 새로운 ORDER로 생성하는게 주요 목표였다.

고민한 사항들과 구현한 방법, 결제 프로세스를 정리해보자.

주문 처리 순서

  1. 장바구니에서 주문 진행: 사용자가 장바구니에서 "주문하기"를 선택하면, 주문 정보(선택한 상품, 수량, Dine in/Take out, 총 금액)를 임시로 저장, 결제 모달 띄우기(결제 정보를 입력받는 모달)
  2. 결제 처리: 사용자가 필요한 정보를 입력하고 결제를 진행. 아임포트 결제 게이트웨이를 사용하여 처리. 결제가 성공적으로 완료되면,
  3. DB에 주문 저장: 결제 완료 후, 주문 정보와 함께 주문 상태를 "주문 완료"로 설정하여 데이터베이스에 저장.

Firebase Firestore 데이터 스키마

  • Orders 컬렉션
    • Document ID: 자동 생성된 ID
    • products: Array of Objects
      • product_id: 상품 ID
      • name: 상품 이름
      • quantity: 수량
    • total_amount: 총 금액
    • order_status: 주문 상태 (예: 주문 완료, 제조 대기, 제조 완료, 주문 취소)
    • order_type: Dine in/Take out
    • timestamp: 주문 시각
    • customer_name: 고객 NAME (선택적)

장바구니와 결제와 관련된 데이터를 참조하거나 상태를 변경하는 함수는 ContextAPI를 활용해서 Provider 내부에 함수를 선언하여 사용하는 방식을 이용했다.

  • CartContext와 PaymentContext로 나누어 구성

장바구니의 데이터와 결제 모달창에서 구매자의 정보를 입력받고 결제 모듈 창을 띄우기까지의 과정은 다음과 같다.

  • Cart 컴포넌트에서 DB에 저장할 형태의 데이터인 orderData를 생성하고, 이를 PaymentProvider로 전송.
  • PaymentModal에서는 React Hook Form을 사용하여 폼 데이터를 수집하고, 이 데이터를 PaymentProvider로 전송.
  • PaymentProviderCartPaymentModal에서 전달받은 데이터를 통합하여 paymentData를 관리하고, 이 데이터를 payment.tsstartPayment 함수로 넘겨 결제를 진행합니다.
  • startPayment는 결제 데이터를 받아서 PG사의 결제 모듈창을 띄우고 성공과 실패시의 로직을 작성할 수 있습니다.

흐름을 더 자세히 살펴보면 다음과 같다.

파일 구성과 결제 프로세스 확인하기

결제 프로세스는 주로 PaymentContext, PaymentModal, 그리고 payment.ts 파일에서 관리중입니다.

  1. 결제 정보 수집 및 처리
    • 결제 정보 입력: 사용자는 결제 모달창에서 결제 정보(예: 이름, 전화번호 등)를 입력합니다.
      • onSubmit 함수가 실행되면, 입력된 데이터는 PaymentContextupdateOrderUserData 함수를 통해 상태에 저장되고, 폼은 리셋됩니다.
    • 결제 데이터 생성: 사용자 데이터가 입력되면 handlePayment 함수가 호출되어 결제 데이터를 생성하고 startPayment 함수를 호출합니다.
      • useEffect 훅으로 orderUserData 상태에 변화가 있을 때 handlePayment 함수를 호출합니다.
  2. 결제 처리 및 확인 (handlePayment):
    • PaymentContext에서 createPaymentData 함수는 orderUserData와 orderData로 결제 모듈에 보낼 결제 데이터를 만들어 리턴합니다.
    • PaymentContext에서 handlePayment 함수는 startPayment 함수에 ****결제 데이터를 보내고 KG 이니시스 결제 모듈을 통해 결제를 요청합니다. 성공적인 결제 후, 주문 정보는 데이터베이스에 저장됩니다.
    • startPayment 함수에는 결제 완료 후 실행할 콜백 함수가 인자로 전달됩니다. 이 콜백은 **handlePayment** 내부에서 정의됩니다.
  3. 결제 요청 (startPayment):
    • payment.ts 파일에서 startPayment 함수는 실제 결제를 처리합니다. 이 함수는 IMP.request_pay를 호출하여 결제를 시도하고, 성공 여부에 따라 적절한 조치를 취합니다.
    • 결제 성공 시, 데이터베이스에 주문 정보를 저장하고, 전달받은 콜백 함수를 실행합니다.
  4. 결제 성공 콜백:
    • 성공 콜백 내에서 결제 모달을 닫고, orderData의 상태를 비우고, 결제 모달창을 닫고 장바구니를 비우는 등의 후속 조치가 이루어집니다.

결제가 완료 후 처리 로직 개선하기

주요 개선 사항

1. 결제 모달 리셋 및 닫기

  • 모달 리셋: React Hook Form의 reset() 함수를 사용하여 결제 폼의 입력 필드를 초기화합니다.
  • 모달 닫기: handlePayment 함수 내 startPayment 호출 시 콜백으로 closeModal() 함수를 전달하여 결제 성공 후 모달을 자동으로 닫습니다.

2. 장바구니 관리

  • 장바구니 비우기: 결제가 성공적으로 완료되면 clearCart() 함수를 호출하여 장바구니를 비웁니다. 이는 사용자가 새로운 쇼핑 세션을 깔끔하게 시작할 수 있도록 돕습니다.
  • 장바구니 모달 닫기: 결제 완료와 동시에 장바구니 모달도 닫히며, 사용자는 주문 완료 페이지나 주문 완료 모달로 이동할 수 있습니다.

3. 결제 처리 로직 개선

  • 결제 중복 호출 문제 해결: useEffect의 의존성 배열에서 handlePayment를 제거하여 결제가 중복으로 발생하는 문제를 해결했습니다. 이로 인해 결제 모달창이 한 번만 뜨고 적절히 처리됩니다.

4. 결제 데이터 초기화

  • 결제 데이터 지속 문제: 한 번 결제를 완료한 후 orderUserData 상태가 유지되어, 장바구니에서 '주문하기' 버튼을 다시 누를 때 정보 입력 없이 바로 결제창이 뜨는 문제가 있었습니다.
  • 모달창 데이터 초기화: 결제 모달창에서 폼 제출 시 useEffecthandlePayment를 호출하고, handlePaymentstartPayment 함수를 사용하여 결제를 진행합니다. 결제가 완료된 후, startPayment의 콜백 함수에서 모든 관련 상태(orderUserData, orderData)를 초기화하여 이 문제를 해결합니다.

작성한 코드 살펴보기

📂 PaymentContext.tsx

const PaymentContext = createContext<PaymentContextProps | null>(null)

export const usePayment = () => useContext(PaymentContext)

// 카트 컴포넌트에서 DB에 저장할 데이터 받아오기
  const updateOrderData = (data: TypeOrderData) => {
    setOrderData(data)
  }

  // 결제 모달창에서 받은 유저 정보 받아오기
  const updateOrderUserData = (data: TypeOrderUserData) => {
    setOrderUserData(data)
  }

  // PG사에 보낼 데이터만들기
  const createPaymentData = () => {
    if (!orderData || !orderUserData) {
      console.error('주문 데이터 또는 사용자 데이터가 누락되었습니다.')
      return
    }

    const firstProductName = orderData.products[0]?.name
    const additionalProductCount = orderData.products.length - 1
    const paymentName =
      additionalProductCount > 0
        ? `${firstProductName}${additionalProductCount}`
        : firstProductName

    const paymentData: paymentDataProps = {
      ...orderUserData,
      buyer_email: orderUserData.buyer_email || '',
      pg: 'html5_inicis', // KG 이니시스
      pay_method: 'card',
      merchant_uid: `merchant_${new Date().getTime()}`, // 고유 주문번호
      name: paymentName, // 구매 상품명
      amount: orderData.total_amount, // 총 결제 금액
    }
    return paymentData
  }
  // PG사에 결제 요청 보내기
  const handlePayment = () => {
    const paymentData = createPaymentData()
    if (paymentData && orderData) {
      startPayment(paymentData, orderData, () => {
        setOrderData(null)
        setOrderData(null)
        closeModal()
        clearCart()
        closeCart()
      })
    } else {
      console.error('결제 데이터 또는 주문 데이터가 누락되었습니다')
    }
  }

return (
    <PaymentContext.Provider
      value={{
        isOpen,
        openModal,
        closeModal,
        updateOrderData,
        updateOrderUserData,
        handlePayment,
        orderUserData,
      }}
    >
      {children}
    </PaymentContext.Provider>
  )

}

결제모달창

📂 PaymentModal.tsx

const PaymentModal = () => {
  const paymentContext = usePayment()
  if (!paymentContext) {
    return
  }
  const { closeModal, updateOrderUserData, handlePayment, orderUserData } =
    paymentContext

  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<TypeOrderUserData>()

  // 입력값을 PG사에 보낼 data로 사용하기 위해 PaymentProvider의 updateOrderUserData로 전달
  const onSubmit = (data: TypeOrderUserData) => {
    updateOrderUserData(data)
    reset()
  }

  useEffect(() => {
    if (orderUserData) {
      handlePayment()
    }
  }, [orderUserData])
  // 이하 생략
}  

고민한 부분

장바구니에 상품을 담고서 결제를 진행하는 과정을 어떻게, 어디서 코드를 분리하고 실행할 지 구조적인 부분에서 고민을 많이했다. 최대한 Provider에는 state로 관리중인 상태와 그 상태를 업데이트하는 함수들로 구성했다.

그렇기에 PG사의 결제를 요청하고 결제가 성공하면 DB에 ORDER 데이터를 업데이트하는 startPayment 함수를 provider에 두지 않고 따로 작성했다.

startPayment 는 결제 요청시 보낼 데이터와, 성공했을때 실행할 콜백함수를 받는 구조이다. 이 함수에 데이터와 결제 요청이 성공했을 때 실행할 함수들은 paymentProvider의 handlePayment 함수가 담당하고 있다.

📂 PaymentContext.tsx
// PG사에 결제 요청 보내기
  const handlePayment = () => {
    const paymentData = createPaymentData()
    if (paymentData && orderData) {
      startPayment(paymentData, orderData, () => {
        setOrderData(null)
        setOrderData(null)
        closeModal()
        clearCart()
        closeCart()
      })
    } else {
      console.error('결제 데이터 또는 주문 데이터가 누락되었습니다')
    }
  }
📂 payment.ts

import { collection, doc, setDoc } from 'firebase/firestore'
import { db } from '@/firebase'
import { paymentDataProps, TypeOrderData } from '@/types/common'

let initialized = false
const IMP = (window as any).IMP
const initialzeIMP = () => {
  if (!initialized) {
    IMP.init('imp30282078')
    initialized = true
  }
}

// PG사 KG 이니시스에 결제 요청
const startPayment = (
  paymentData: paymentDataProps,
  orderData: TypeOrderData,
  onSuccess: () => void
) => {
  initialzeIMP()

  IMP.request_pay(paymentData, function (response: any) {
    if (response.success) {
      alert('결제가 완료되었습니다.')
      onSuccess()

      const orderDataDB = async () => {
        try {
          const orderCollection = collection(db, 'orders')
          const docRef = doc(orderCollection) // 새 문서 참조 생성
          await setDoc(docRef, {
            ...orderData,
            order_id: docRef.id,
          })
        } catch (error) {
          alert('결제가 실패하였습니다.')
          console.error('주문 실패', error)
        }
      }
      orderDataDB()
    } else {
      console.error('결제 실패', response.error)
    }
  })
}

export default startPayment
profile
기록은 기억이 된다

0개의 댓글