이번 주 항해 취업 리부트코스에서 내가 구현한 기능은 무엇인가요?
- 장바구니에 담은 상품으로 가상 결제를 구현하기.
해당 기능을 구현하기 위해, 어떤 기술적 의사결정을 거쳤나요?
- 장바구니와 결제와 관련되어 전역으로 상태를 참조하고 업데이트를 하는 경우가 많으니 Context API를 이용했다.
- Provider 함수 내부에 상태와 관련된 로직을 작성하여 page 부분의 컴포넌트에서는 주로 Provider 함수 내에서 작성된 함수를 사용하고 상태를 참조하는 구조로 진행했다.
- 상태가 복잡하지 않기에 context API를 사용해도 괜찮다고 생각했으며 최적화 부분에서 단점이 있기에 화면에서 보여지는 비율이 큰 상품리스트 컴포넌트에 React.memo를 적용했다.
3주차 기능 구현 요구 사항을 간략히 소개하면 다음과 같다.
구매자가 장바구니에 담은 상품으로 구매를 진행해야해서 결제 SDK를 연동해 가상 결제를 진행하고, 결제가 완료되면 주문 상태를 DB에 새로운 ORDER로 생성하는게 주요 목표였다.
고민한 사항들과 구현한 방법, 결제 프로세스를 정리해보자.
장바구니와 결제와 관련된 데이터를 참조하거나 상태를 변경하는 함수는 ContextAPI를 활용해서 Provider 내부에 함수를 선언하여 사용하는 방식을 이용했다.
장바구니의 데이터와 결제 모달창에서 구매자의 정보를 입력받고 결제 모듈 창을 띄우기까지의 과정은 다음과 같다.
orderData
를 생성하고, 이를 PaymentProvider
로 전송.PaymentProvider
로 전송.Cart
와 PaymentModal
에서 전달받은 데이터를 통합하여 paymentData
를 관리하고, 이 데이터를 payment.ts
의 startPayment
함수로 넘겨 결제를 진행합니다.startPayment
는 결제 데이터를 받아서 PG사의 결제 모듈창을 띄우고 성공과 실패시의 로직을 작성할 수 있습니다.흐름을 더 자세히 살펴보면 다음과 같다.
결제 프로세스는 주로 PaymentContext
, PaymentModal
, 그리고 payment.ts
파일에서 관리중입니다.
onSubmit
함수가 실행되면, 입력된 데이터는 PaymentContext
의 updateOrderUserData
함수를 통해 상태에 저장되고, 폼은 리셋됩니다.handlePayment
함수가 호출되어 결제 데이터를 생성하고 startPayment
함수를 호출합니다.useEffect
훅으로 orderUserData
상태에 변화가 있을 때 handlePayment
함수를 호출합니다.handlePayment
):PaymentContext
에서 createPaymentData 함수는 orderUserData와 orderData로 결제 모듈에 보낼 결제 데이터를 만들어 리턴합니다.PaymentContext
에서 handlePayment
함수는 startPayment
함수에 ****결제 데이터를 보내고 KG 이니시스 결제 모듈을 통해 결제를 요청합니다. 성공적인 결제 후, 주문 정보는 데이터베이스에 저장됩니다.startPayment
함수에는 결제 완료 후 실행할 콜백 함수가 인자로 전달됩니다. 이 콜백은 **handlePayment**
내부에서 정의됩니다.startPayment
):payment.ts
파일에서 startPayment
함수는 실제 결제를 처리합니다. 이 함수는 IMP.request_pay
를 호출하여 결제를 시도하고, 성공 여부에 따라 적절한 조치를 취합니다.reset()
함수를 사용하여 결제 폼의 입력 필드를 초기화합니다.handlePayment
함수 내 startPayment
호출 시 콜백으로 closeModal()
함수를 전달하여 결제 성공 후 모달을 자동으로 닫습니다.clearCart()
함수를 호출하여 장바구니를 비웁니다. 이는 사용자가 새로운 쇼핑 세션을 깔끔하게 시작할 수 있도록 돕습니다.useEffect
의 의존성 배열에서 handlePayment
를 제거하여 결제가 중복으로 발생하는 문제를 해결했습니다. 이로 인해 결제 모달창이 한 번만 뜨고 적절히 처리됩니다.orderUserData
상태가 유지되어, 장바구니에서 '주문하기' 버튼을 다시 누를 때 정보 입력 없이 바로 결제창이 뜨는 문제가 있었습니다.useEffect
로 handlePayment
를 호출하고, handlePayment
는 startPayment
함수를 사용하여 결제를 진행합니다. 결제가 완료된 후, 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