나의 개발 일지

페이팔(Paypal) 결제 기능 구현하기 (Sandbox를 활용해서 테스트 버전 만들기) #javascript #nodejs #paynemt

designer DK 2025. 2. 9. 17:42
728x90

준비 중인 프로젝트에 글로벌 페이먼트를 구축하는데 있어서 스트라이프로 개발을 진행했었다.

페이팔이 국내 계정으로 결제할 수 없다는 이유가 있어서 그렇게 진행을 했었는데

당시 챗GPT에게 물어보니 스트라이프는 문제가 없다고 해서 그렇게 개발을 진행했었다.

 

그런데 이게 왠걸..스트라이프는 알고보니 더 큰 문제가 있었다.

국내 사업자는 스트라이프로 사업을 할 수 없다는 사실 @ㅁ@;;

바보같게도 뒤늦게 이 사실을 알게되어서 어쩔 수 없이 개발 방향을 페이팔로 선회했다.

(ai가 천연덕스럽게 거짓말을 잘하기 때문에 조심해야 한다.)

 

페이팔은 샌드박스의 미국 계정을 만들어서 진행해야해서 초반에 좀 번거로운 부분이 있다.

(한국 계정으로 하면 튕기게 된다. 규제가 좀 풀렸으면...)

 

먼저 페이팔을 가입 후 개발자 대쉬보드로 들어간다.

 

 

 

메뉴 중에 Testing Tools에 Sandbox Accounts를 선택.

 

 

 

우상단에 Create Account를 진행

 

 

 

그리고 이런 창이 뜨면 비즈니스 어카운트로 해서 국가를 미국으로 지정 (페이팔 가능한 국가면 된다)

 

 

그러면 이런식으로 테스트용 가상 계정이 생성된다. 여러개 생성 가능

 

 

 

계정을 생성했으면 이제 앱을 생성해야 한다.
상단 메뉴 중 Apps & Credentials를 클릭하면 생성했던 앱들 리스트들과 함께 Create App버튼이 있다.
여기서 생성 버튼을 클릭

 

 

여기서 앱의 이름을 지정하고 타입 및 어카운트를 지정한다.

타입은 Merchant와 Platform 2가지가 있는데

Merchant는 상품을 직접 파는 케이스에 맞고 Platform은 직접파는 것 + 중간 유통을 하는 서비스에 적합하다.

나는 직접 파는 케이스여서 Merchant를 선택

 

Sandbox Account의 경우 아까 생성했던 어카운트를 지정해주면 되는데

한국 계정이 아닌 미국 등 꼭 페이팔 지원 계정으로 해야한다.

 

생성하게되면 이렇게 리스트에 포함되게 되고 클릭하면 상세페이지로 진입

 

 

상세에 들어가면 내 Client ID, Secret key와 같은 주요 정보와 샌드박스 계정 정보가 있다.

 

 

 

그리고 어카운트에서 view details를 클릭하면 더 상세한 어카운트 정보를 볼 수 있는데 이는 테스트 할 때 필요하다.

 

 

여기까지가 Paypal Sandbox 계정 생성 및 앱 생성에 대한 내용이고

이제 이걸 이용해서 개발하는 부분에 대한 내용

 

먼저 html에 이렇게 Paypal sdk를 가져오는 스크립트가 필요하다.

클라이언트 아이디를 넣어주고 통화 및 버튼에 적히는 언어 등을 지정

intent=capture로 하게되면 즉시 결제에 적합한 방식으로 구축된다.

반면 intent=authorize는 결제를 즉시 처리하지 않고 일단 승인만 받아두는 케이스. 예약 시스템이나 선주문 등에 적합.

현재 나의 프로젝트 케이스에서는 intent=capture가 맞기 때문에 이걸로 진행

 

 

다음은 백엔드의 API 부분. 결제 생성, 성공, 취소 등으로 케이스를 나누었다.

const express = require("express");
const paypalController = require("../controllers/paypalController");
const authController = require("../controllers/authController");
const router = express.Router();

// 결제 생성
router.post("/create-payment", paypalController.createPayment);

// 결제 성공 처리 (주문 생성 포함)
router.post("/success", authController.authenticate, paypalController.executePayment);

// 결제 취소 처리
router.get("/cancel", paypalController.cancelPayment);

module.exports = router;

 

 

아래는 컨트롤러 코드. 나의 경우 주문 생성이 필요해서 코드가 조금 길어지긴 했는데

주문 생성을 제외하고 결제 부분만 보면 생각보다 간단하게 처리할 수 있다.

env파일에 아까 페이팔 앱에서 생성된 Client ID, Secret key를 넣어주고 그 값을 가져와서 넣어주어야한다.

const Order = require("../models/Order");
const checkoutNodeJssdk = require('@paypal/checkout-server-sdk');

const environment = new checkoutNodeJssdk.core.SandboxEnvironment(
process.env.PAYPAL_CLIENT_ID,
process.env.PAYPAL_CLIENT_SECRET
);
const client = new checkoutNodeJssdk.core.PayPalHttpClient(environment);

const paypalController = {
// 결제 생성
createPayment: async (req, res) => {
try {
const { totalAmount } = req.body;
 
const request = new checkoutNodeJssdk.orders.OrdersCreateRequest();
request.requestBody({
intent: 'CAPTURE',
application_context: {
shipping_preference: 'NO_SHIPPING',
user_action: 'PAY_NOW'
},
purchase_units: [{
amount: {
currency_code: 'USD',
value: totalAmount
}
}]
});

const response = await client.execute(request);
res.json(response.result);
} catch (error) {
console.error('PayPal create payment error:', error);
res.status(500).json({ error: 'Failed to create order' });
}
},

// 결제 실행 및 주문 생성
executePayment: async (req, res) => {
try {
const {
orderNum,
subTotal,
vat,
totalPrice,
assets,
paymentDetails
} = req.body;

// 주문 생성
const newOrder = new Order({
orderNum,
subTotal,
vat,
totalPrice,
userId: req.userId,
assets,
paymentDetails
});

const savedOrder = await newOrder.save();
res.status(200).json({
status: "success",
message: "Payment successful and order created",
order: savedOrder,
orderId: savedOrder._id
});
} catch (error) {
console.error('PayPal execute payment error:', error);
res.status(500).json({ error: 'Failed to process payment and create order' });
}
},

// 결제 취소
cancelPayment: async (req, res) => {
res.status(200).json({ status: "cancelled", message: "Payment was canceled" });
}
};

module.exports = paypalController;

 

 

다음은 프론트엔드 코드. 원하는 위치에 Paypal button container를 넣어주어야 한다.

그 위치에 자동으로 페이팔 결제 버튼이 생성됨.

<div class="payment-container">
<div id="paypal-button-container"></div>
</div>

 

아래는 결제 요청 및 승인 관련 코드.

import { systemUI } from '../../common/systemUtils.js';

export function initializePayPalButton() {
const paypalContainer = document.getElementById('paypal-button-container');
if (!paypalContainer || typeof paypal === 'undefined') {
return;
}

const orderDataToStore = JSON.parse(localStorage.getItem('orderDataToStore'));
const overallTotalPrice = (Number(orderDataToStore.totalPrice) || 0).toFixed(2);
console.log('orderDataToStore', orderDataToStore);

paypal.Buttons({
style: {
layout: 'vertical',
color: 'black', // 'black' 또는 'white'만 가능
shape: 'rect',
label: 'pay'
},

fundingSource: paypal.FUNDING.CARD,

createOrder: function(data, actions) {

return actions.order.create({
application_context: {
shipping_preference: 'NO_SHIPPING',
user_action: 'PAY_NOW'
},
purchase_units: [{
amount: {
currency_code: 'USD',
value: overallTotalPrice
}
}]
});
},
 
onApprove: function(data, actions) {
systemUI.showLoading(); // 로딩 시작
return actions.order.capture()
.then(async function(orderData) {
try {
console.log('Capture result', orderData, JSON.stringify(orderData, null, 2));
const transaction = orderData.purchase_units[0].payments.captures[0];
 
// success 엔드포인트로 주문 생성 요청
const token = localStorage.getItem("token");
const response = await fetch(`${URI}/api/paypal/success`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
orderNum: orderDataToStore.orderNum,
subTotal: orderDataToStore.subTotal,
vat: orderDataToStore.vat,
totalPrice: orderDataToStore.totalPrice,
assets: orderDataToStore.orderAssets.map(asset => asset._id),
paymentDetails: {
cardBrand: 'PayPal',
customerEmail: orderData.payer.email_address,
customerName: `${orderData.payer.name.given_name} ${orderData.payer.name.surname}`,
customerCountry: orderData.payer.address.country_code,
paymentId: transaction.id,
status: transaction.status
}
})
});

if (!response.ok) {
throw new Error('주문 생성 실패');
}

const orderResult = await response.json();
console.log('주문 생성 성공:', orderResult);

// 주문 성공 페이지로 이동
window.location.href = `/order-success.html?orderId=${orderResult.orderId}`;

} catch (error) {
console.error('주문 처리 중 오류:', error);
systemUI.hideLoading(); // 에러 시 로딩 숨김
window.location.href = '/order-cancel.html';
}
});
},
 
onError: function(err) {
console.error('PayPal error:', err);
window.location.href = '/order-cancel.html';
},
 
onCancel: function() {
window.location.href = '/order-cancel.html';
}
}).render('#paypal-button-container');
}

 

createOrder는 결제 생성, onApprove는 결제 성공에 대한 처리 부분이라고 보면 되는데

onApprove는 아까 말한대로 내 프로젝트에서 필요한 주문 생성을 진행하기 위한 코드가 많아서 그렇지

결제 자체에만 필요한 건 이 코드면 된다.

const transaction = orderData.purchase_units[0].payments.captures[0];
 

 

처리가 되면 이런식으로 주문 성공페이지로 이동하게 된다.

(나는 바로 주문내역으로 보내야하는 케이스여서 오더ID를 URL에 넣어서 보냈다.)

window.location.href = `/order-success.html?orderId=${orderResult.orderId}`;

 

이런식으로 구성하면 페이팔 결제를 진행할 수 있다.

카트에서 결제 버튼을 눌러서 결제 페이지 테스트 화면을 가게되면 이렇게 페이팔 카드 결제버튼이 생성되어 있다.

 

버튼을 누르면 이런 입력 폼이 나오게 되는데

 

사실 나는 Billing Address 부분은 입력받지 않아도 되어서 저 부분을 없애보려고 부단히 노력했으나 실패했다.

createOrder: function(data, actions) {

return actions.order.create({
application_context: {
shipping_preference: 'NO_SHIPPING',
user_action: 'PAY_NOW'
},
purchase_units: [{
amount: {
currency_code: 'USD',
value: overallTotalPrice
}
}]
});
},

 

AI에게 물어봤을 때 프론트나 백엔드에 이런식으로 shipping_preference: 'NO_SHIPPING' 으로 설정해주면

저런 배송관련 주소 입력창이 뜨지 않는다고 답해주었으나 아무리 해봐도 전혀 변화가 없었다.

구글링을 해봤을 때 페이팔 정책적으로 안전한 거래를 위해 꼭 주소를 받는다는 것 같아서 저 부분을 없애는 건 일단 보류하기로 했다.

 

그리고 해당 부분 입력 시 그냥 아무 정보나 입력하면 실패하게 되는데 성공적인 케이스를 테스트하려면

 

카드 번호로는 4111 1111 1111 1111

 

그리고 유저 정보로는 아까 최초에 만든 어카운트의 디테일 정보들을 이용해서 입력해야 올바른 테스트를 진행해 볼 수 있다.

페이팔 샌드박스 계정 정보 내용
샌드박스 계정 정보를 통해 결제 정보 입력

 

 

이렇게 입력해서 결제를 하면 결제가 진행되고 결제 성공을 하게 되면 그 내용을 페이팔 샌드박스에서 확인할 수 있다.

https://sandbox.paypal.com/ 로 접속하면 샌드박스를 접속할 수 있는데

여기서 중요한건 페이팔 샌드박스에 접속 시 원래 나의 페이팔 계정이 아닌 아까 가상으로 생성한 샌드박스 계정으로 로그인 해야 한다.

 

 

 

 

접속하게 되면 이렇게 샌드박스용 관리자 기능들이 제공된다. (가상 머니이긴하지만 총 잔고 및 매출 등이 확인 된다.)

 

 

여기서 Activity - All Transactions를 클릭하면

 

이런식으로 내가 테스트 했던 결제 내용들을 확인할 수 있다.

 

과정이 꽤 복잡하긴 했지만 페이팔이 역시 결제 서비스 전통의 강자답게 체계적이고 안정적인 시스템을 제공해주는 듯 했다.

(홈페이지가 전반적으로 엄청 느린 건 불만...)