카테고리 없음

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>

 

생각했던것 보다 구현을 위해 작성해야 할게 많아고, 백엔드 에서 처리하는
1.포트원 토큰발급
2.결제취소
3.결제내역 단건조회
4.주문 정보 저장
5.결제 검증
와 같은 처리를 하는데 조금 복잡함이 있어 어려움을 많이 겪은 시간인거 같습니다.