카테고리 없음
Nestjs + React 환경에서 PortOne 을 사용한 결제 서비스 구현해보기
콩국수는설탕
2024. 8. 25. 04:07
현재 진행중인 프로젝트에서 결제기능을 통해 유저의 포인트를 증가시키는 로직을 구현하게 되었습니다.
그래서 결제 로직을 구현할때 "포트원(PortOne)" 을 사용하여 구현하기로하였습니다.
포트원(PortOne) 이란
다양한 결제 서비스를 한 번에 통합 관리할 수 있는 결제 플랫폼 입니다.
포트원 주소: https://portone.io/korea/ko
먼저 코드를 작성하기전 간단한 워크플로우 를 그려봤습니다.
유저가 결제버튼을 클릭하면,
1.주문 정보가 저장되고, 포트원 결제 API가 호출되어 결제가 진행됩니다.
2.결제 정보를 검증하고, 유저의 포인트를 증가시키며 포인트 내역을 저장합니다.
3.만약 이 과정 중에 실패나 오류가 발생한다면 결제가 취소 되고, 변경사항이 롤백되며, 이미 결제 된 금액은 환불 됩니다.
위 3 과정을 먼저 기획하고 작성한 코드 를 보여드리겠습니다.
(코드 의 설명은 코드 내에 있는 주석으로 대체합니다)
결제 로직을 구현하기위해 React를 사용하여 구현한 프론트 로 기록하겠습니다.
이번에 구현한건 React 환경에서 구축하였는데, 결제 로직을 위해 처음으로 React를 사용하다보니
다소 부정확한 정보를 제공할수 있는점 참고 바랍니다.
useEffect() 를 사용하여, 아임포트 를 초기화 하는 작업을 진행하게 구현하였는데
공부하면서 안게 useEffect() 는 페이지가 로드되면 한번씩 실행이되는 메서드 라고 배웠습니다.
아임포트 초기화 작업
/** useEffect()는 페이지가 로드될때 한번 실행됨 **/
useEffect(() => {
/** 'script' 라는 요소를 만드는다는뜻 **/
const script = document.createElement('script');
/* scipt 요소 안에 src 값을 추가해주고 async true 로 동기적으로 실행한다는걸 명시함 */
script.src = 'https://cdn.iamport.kr/v1/iamport.js';
script.async = true;
/** 위에서 createElement로 생성한 요소를 <body> 를 부모로둔 자식 태그를 만든다는뜻 **/
document.body.appendChild(script);
// 아임포트 초기화
script.onload = () => {
/* 윈도우 객체 안에 IMP라는 속성이 있다면 실행될 로직 */
if (window.IMP) {
/** init 메서를 사용하여 IMP 를 초기화 하면서 입력한 가맹점
식별코드 를 사용하여 가맹점의 정보를 설정함 **/
window.IMP.init('imp55558125'); // 가맹점 식별코드
}
};
return () => {
/**메모리 누수를 방지하고자 이제 사용하지 않을 요소를 삭제해주는 작업, 이같은 경우는 아까 위에서 생성한 script를 제거하는것.**/
document.body.removeChild(script);
};
}, []);
```
다음으로 작성한건 결제요청 로직입니다.
/* 결제 호출을 위한 로직 */
const requestPay = async () => {
try {
/* 주문 정보를 생성하고 저장하는 작업을 위해 백엔드 서버 에서 만들었던 createOrder() 라는 메서드를 호출해서
response 에 답아주는 작업 */
const response = await createOrder(pointMenuId);
/* 아까 위에서 response 에 백엔드서버에서 createOrder를 통해 생성해준 MerchantUid 와 amount를 넘겨주었기때문에
이렇게 꺼내올수 있는거임 (한마디로 response에 있는걸 끄내온다라고 생각하면됨) */
const { merchantUid, amount } = response.data;
if (window.IMP) {
/* 결제정보를 설정해주는 코드 */
window.IMP.request_pay(
{
pg: 'uplus.tlgdacomxpay', //PG 사 식별코드
pay_method: 'card', //결제방법
merchant_uid: merchantUid, // 주문 고유 번호
name: '포인트 충전',
amount: amount, // 주문된 금액을 사용
},
async function (response) {
//결제 성공시 실행될코드
if (response.success) {
try {
/* 백엔드에서 생성해준 verifyPayment 로직을 사용해
merchantUid 와 response 에 있는 imp_uid 를 통해 결제를 검증해주는 작업을 진행해준다 */
await verifyPayment(merchantUid, response.imp_uid);
alert('결제가 성공적으로 완료되었습니다.');
} catch (error) {
console.error('검증 실패', error);
/* 백엔드 에서 생성한 결제 실패시 호출할 refund 라는 환불 로직을 실행시켜주는 작업 */
await refund(response.imp_uid);
alert('결제 검증 과정에서 오류가 발생했습니다.');
}
} else {
// 결제가 실패했을 때 로직
console.error('결제 실패:', response);
alert('결제에 실패했습니다.');
}
/* 결제 실패시 /points 페이지로 이동시킴 */
navigate(`/points`);
},
);
}
} catch (error) {
console.error('Error creating order:', error);
if (error.response) {
console.error('Error response data:', error.response.data);
}
}
};
참고) 포인트 메뉴는 그냥 하드코딩으로 생성해주었습니다
const pointMenus = [
{ id: 1, price: 10000, name: '10000P' },
{ id: 2, price: 20000, name: '20000P' },
{ id: 3, price: 30000, name: '30000P' },
{ id: 4, price: 40000, name: '40000P' },
{ id: 5, price: 40000, name: '50000P' },
];
마지막으로 유저가 결제버튼을 누르면 호출되게끔 만들었습니다
<button onClick={requestPay} className="payment-button">
결제하기
</button>