흐름도는 아래와 같다.
TossPayments는 크게 가상계좌와 간편결제를 이용할 수 있었다. 하지만 간편결제를 사용할 경우 네이버 페이를 이용한 계좌이체도 지원하고 있었기에 가상계좌까지 나눌 필요성을 느끼지 못해 간편결제만 구현하기로 마음을 먹은 것이다. 또한 간편결제의 경우 다양한 은행(국민 등)과 간편결제를 지원하는 기업들과의 연동이 좋기 때문에 결제 수단을 사용자가 선택하기에 폭이 넓다고 생각한 부분도 있었다.
소셜 로그인과 마찬가지로 공식문서를 참고하는 것이 가장 좋지만 나도 절차를 다시 한번 확인할겸 차근차근 설명하겠다.
REACT_APP_xxxx
처럼 REACT_APP
이 접두사로 붙어줘야한다는 것을 명심하자. 또한, 우리가 발급받은 키는 테스트 키이기 때문에 실제 결제는 이루어지지 않는다.쉽게 말해서 지금은 그냥 사진에 있는 것으로 쓰면되고 차후 실제 연동을 할 경우에는 위의 형식을 따라야한다. 이때 중요한 부분을 아래 코드에서 잠시 보자.base64란?
8비트 이진 데이터(예를 들어 실행 파일이나, ZIP 파일 등)를 문자 코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식
{SECRET_KEY}: 의 형식으로 되어있는 경우 제대로된 Base64 인코딩이 되지 않는다.
{}을 제외하고 SECRET_KEY: 형태로만 인코딩을 해줘야한다.
이렇게 인증키까지 준비가 되었다면 요청을 보낼 수 있다.요청은 React를 기준으로 설명하겠다.
#설치 커맨드
npm install @tosspayments/payment-sdk --save
설치가 완료되었다면 컴포넌트에 아래와 같이 import를 해주고, loadTossPayments()를 통해 UI 및 결체요청을 진행하면된다.
import { loadTossPayments } from "@tosspayments/payment-sdk";
//페이호출 메소드
const handlePayment = (subject) => {
const random = new Date().getTime() + Math.random(); //난수생성
const randomId = btoa(random); //난수를 btoa(base64)로 인코딩한 orderID
if (subject === "카드") { //간편결제 함수 실행
loadTossPayments(client_id).then(tossPayments => {
tossPayments.requestPayment(subject, {
amount: reserv.amount, //주문가격
orderId: `${randomId}`, //문자열 처리를 위한 ``사용
orderName: orderName(), //결제 이름(여러건일 경우 복수처리)
customerName: '테스트', //판매자, 판매처 이름
successUrl: process.env.REACT_APP_TOSS_SUCCESS,
failUrl: process.env.REACT_APP_TOSS_FAIL,
})
});
}
}
이렇게 요청을 하면 아래와 같은 화면 또는 동의 화면이 출력된다. 혹시라도 위의 파라미터들에 대한 정보가 필요하다면 여기를 참고하자.
여기서 중요한 부분은 successURl과 failUrl이다. 반드시 다음 요청이 이루어지는 Url을 지정하여 Redirect를 받아야만 정상적인 결제요청 데이터를 서버(Spring)에 전송할 수 있다.
https://{ORIGIN}/success?paymentKey={PAYMENT_KEY}&orderId={ORDER_ID}&amount={AMOUNT}
RedirectUrl 형식이다. 여기서 paymentKey, orderId, amount는 API 서버에서 RedirectUrl의 파라미터로 전달해준다. 이때 해당 정보를 포함하는 객체를 생성하여 Spring 서버로 요청을하는 것이다.
나는 아래와 같이 Callback 컴포넌트를 생성하여 RedirectUrl 응답과 동시에 Spring으로 요청을 보냈다.
function TossCallback() {
const navigate = useNavigate();
useEffect(() => {
const paymentKey = new URL(window.location.href).searchParams.get("paymentKey");
const orderId = new URL(window.location.href).searchParams.get("orderId");
const amount = new URL(window.location.href).searchParams.get("amount");
//spring 서버로 인증키를 통해 유저정보를 획득하고 로그인 처리 요청
accessClient.post(`${process.env.REACT_APP_REQUEST_URL}/api/client/token/payment`, {
//spring 서버로 전달할 요청 params
paymentKey: paymentKey,
orderId: orderId,
amount: amount,
}).then((res) => {
//spring에서 처리된 정보 전달
const reserv = res.data;
//예약내역 페이지로 전환 및 임시저장 정보 삭제
localStorage.removeItem("rsvIdx");
navigate("/shop/reservation/detail", {state: reserv});
}).catch((err) => {
//에러발생 시 경고처리 후 login 페이지로 전환
alert(err.response.data.detail);
window.history.back();
})
}, []);
해당 컴포넌트는 RedirectUrl을 받자마자 자동으로 Spring 서버로 post 요청을 보내게 되는 것이다. 여기까지하면 결제요청에 대한 부분은 마무리가 된다. 하지만 결제요청이 되었다고 해서 결제가 된 것은 아니다. 요청된 정보를 Spring에서 다시 Toss API 서버로 승인요청을 보내야한다.
결제 승인에서 주의할 점은 표시해놓은 두가지 일 것이다. 아래 사진을 참고하여 기능을 구현해보자.
public Payment getConfirmPayment(PaymentParams paymentParams) {
//인증키 base64 인코딩
String authorization = Base64.getEncoder().encodeToString(toss_secret.getBytes());
log.debug("------ API 전송 요청 -------" + authorization);
RestTemplate rt = new RestTemplate();
//헤더 구성
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + authorization);
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
//요청 객체 생성
HttpEntity<PaymentParams> paymentRequest = new HttpEntity<>(paymentParams, headers);
//Post 요청과 동시에 데이터 매핑
TossPayment tossPayment = rt.postForObject(toss_url, paymentRequest, TossPayment.class);
log.debug("------ API 응답 반환 -------");
return getPaymentInfo(tossPayment, paymentParams);
}
요청에 대한 로직은 위의 사진과 같다. 다만 RestTemplate를 사용했을 뿐이다. 승인을 보내면 결제승인에 대한 응답 객체가 아래와 같은 형태로 반환된다.
{
"mId": "tosspayments",
"version": "2022-11-16",
"paymentKey": "437cqAK3STB6934xB2MZ2",
"lastTransactionKey": "dvlxwdQuXByXEjdY6gDzJ",
"orderId": "Nz_LpeYO8G2cHt1MznJnt",
"orderName": "토스 티셔츠 외 2건",
"status": "DONE",
"requestedAt":"2022-08-17T15:38:47+09:00",
"approvedAt":"2022-08-17T15:39:14+09:00",
...중략
"type": "NORMAL",
"easyPay": {
"provider": "토스페이",
"amount": 15000,
"discountAmount": 0
},
"country": "KR",
"failure": null,
"isPartialCancelable": true,
"receipt": {
"url": "https://dashboard.tosspayments.com/sales-slip?transactionId=cno3Idq53AKHoXP%2BJnAWt70lTLYJHVytjcCu%2FhEIUd56LAMEmBlJ9FWaQinp0uZ1&ref=PX"
},
"checkout": {
"url": "https://api.tosspayments.com/v1/payments/437cqAK3STB6934xB2MZ2/checkout"
},
"currency": "KRW",
"totalAmount": 15000,
"balanceAmount": 15000,
"suppliedAmount": 13636,
"vat": 1364,
"taxFreeAmount": 0,
"taxExemptionAmount": 0,
"method": "간편결제"
}
여기서 필요한 정보들을 획득하기 위해 아래와 같이 객체를 별도로 선언해주었다.
// API 응답값을 전달할 DTO
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
public class TossPayment {
private String orderName;
private String method;
private String totalAmount;
private Card card;
private VirtualAccount virtualAccount;
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
public class Card{
private String approveNo;
}
}
응답 객체를 통해 결제승인 결과에 대한 데이터를 매핑하고, 실제 DB에 저장할 객체에게 해당 정보를 다시 한번 매핑해준다.
//DTO
@Data
public class Payment {
private int payIdx;
private int rsvIdx;
private String payMethod;
private String payName;
private String payAmount;
private String payApproval;
private String payDate;
}
//매핑 메소드
public Payment getPaymentInfo(TossPayment tossPayment, PaymentParams params) {
// Empty 객체 생성 및 데이터 세팅
Payment payment = new Payment();
payment.setRsvIdx(params.getRsvIdx());
payment.setPayMethod(tossPayment.getMethod());
payment.setPayName(tossPayment.getOrderName());
payment.setPayApproval(tossPayment.getCard().getApproveNo());
payment.setPayAmount(tossPayment.getTotalAmount());
return payment;
}
이렇게 데이터 매핑까지 완료된 Payment는 Service와 DAO를 통해 DB로 저장된다. DB저장 로직은 개발자마다 상이한 부분이 있으니 굳이 작성하지 않겠다.
React와 Spring을 연동하여 Toss API를 사용하다보니 자잘한 오류들이 많았기 때문에 이런 부분들을 간단히 공유하려한다.
원인 : base64로 인코딩 할 때 형식이 제대로 작성되지 않아 발생
해결 : SECRET_KEY:
형태로 인코딩이 되어야함.
예를 들어 secret_key가 "test.toss"라고 가정할 때 아래와 같이 작성해야함.
String authorization = Base64.getEncoder().encodeToString("test.toss:");
여기서 :이 빠지거나 "{test.toss}:" 형태로 작성하면 오류가 발생한다. 참고로 테스트는 아까도 이야기했지만 그냥 토스 홈페이지에 나온 것을 복사해서 사용하면 된다.
원인 : post 전송 시 amount의 자료형이 불일치
해결 : 전송 시 amount의 자료형을 동일하게 작성
amount는 필수 파라미터로 number형이다. 따라서 String형으로 작성되면 정상적인 데이터를 읽지 못하는 것 같다. 나는 DTO의 형식과 동일한 객체를 구성하여 넘겨줌으로써 해당 오류를 해결하였다.
어떻게보면 배포단계에서 가장 많이 발생하는 오류 일 수도 있다. 해당 오류는 개발단계에서는 크게 무리가 되지 않지만 배포를 하게되면 Origin이 바뀌게 되어 정상적으로 작동하지 않는다.
예를 들어 개발단계에서는 localhost.3000/toss/callback을 RedirectUrl로 지정했는데 AWS에 배포하니 xx.xx.xxx.323:3000/toss/callback이 되었다면 해당 Origin으로 지정한 데이터를 변경해주어야 한다는 것이다. 이 부분은 배포를 진행할 경우에만 적용될 수 있으니 참고하도록 하자.
프로젝트를 진행하면서 벌써 4번째 Open API를 연동했다. 대부분 비슷한 원리로 흘러가고 특정 파라미터가 다르다거나 요청 절차가 상이한 것 외에는 비슷하게 느껴진다. 아주 기초적인 부분을 연동한 것이기는 하지만 조금 더 익숙해진다면 OpenAPI를 다루는 부분에 있어서 효율적으로 코드를 작성하고 여러가지 부분을 응용할 수 있을 것이라는 생각을 한다.
참고로 API를 연동하면서 발생하는 오류들은 기존에 개발하면서 발생한 오류들과는 성질이 다르다보니 여기서 얻어가는 경험적인 부분들이 상당히 많은 것 같다.
그럼 이만.👊🏽