Payment Request API는 브라우저 네이티브 결제 시트를 통해 주소, 연락처, 결제수단을 빠르게 입력하게 해주는 웹 표준입니다. React 앱에서는 이를 프로그레시브 인핸스먼트로 도입하여 체크아웃 전환율을 올릴 수 있습니다. 다만 결제수단은 브라우저와 설치된 결제 핸들러에 따라 달라지며, 일부 메서드는 제한적 지원이므로 적절한 폴백을 함께 준비해야 합니다.
1. 지원과 제약 이해
- Payment Request API 자체는 크롬/엣지 등의 일부 환경에서 사용 가능합니다. Safari는 Apple Pay를 Payment Request를 통해 노출하지만 설정과 도메인 검증이 필요합니다.
- 과거 예제로 자주 쓰이던 basic-card는 대부분의 최신 브라우저에서 비활성화되었거나 더 이상 권장되지 않습니다. 실제 서비스에서는 결제 핸들러(예: Apple Pay) 또는 Secure Payment Confirmation(SPC) 같은 메서드를 사용하거나, 게이트웨이 SDK와 병행해야 합니다.
- 반드시 HTTPS 환경과 사용자 제스처(버튼 클릭) 내에서 show()를 호출해야 합니다. 서버 검증 흐름을 준비하고, Payment Request가 불가한 경우 전통적인 체크아웃으로 폴백합니다.
2. 최소 통합: 버튼 클릭으로 Payment Request 열기
아래 예제는 React 컴포넌트에서 Payment Request를 생성하고 결제 시트를 띄운 뒤 결과를 서버로 검증하는 흐름입니다. 데모 용도로 basic-card를 사용하지만, 실제 서비스에서는 6장에서 설명하는 결제 메서드로 바꾸는 것을 권장합니다.
import React, { useCallback, useState } from 'react';
export default function PaymentButton() {
const [loading, setLoading] = useState(false);
const createRequest = useCallback(() => {
// 데모용: basic-card. 실제 서비스는 다른 메서드로 교체 권장
const methodData = [
{
supportedMethods: 'basic-card',
data: {
supportedNetworks: ['visa', 'mastercard', 'amex'],
supportedTypes: ['debit', 'credit']
}
}
];
const details = {
displayItems: [
{ label: '상품 합계', amount: { currency: 'KRW', value: '49000' } },
{ label: '배송비', amount: { currency: 'KRW', value: '0' } }
],
total: {
label: '총 결제금액',
amount: { currency: 'KRW', value: '49000' }
},
shippingOptions: [
{ id: 'std', label: '일반 배송', amount: { currency: 'KRW', value: '0' }, selected: true },
{ id: 'exp', label: '익일 배송', amount: { currency: 'KRW', value: '3000' }, selected: false }
]
};
const options = {
requestPayerName: true,
requestPayerEmail: true,
requestPayerPhone: false,
requestShipping: true
};
const request = new PaymentRequest(methodData, details, options);
// 배송지/옵션 변경 시 합계 업데이트
request.addEventListener('shippingaddresschange', (event) => {
event.updateWith(new Promise((resolve) => {
const region = request.shippingAddress?.country || 'KR';
let shippingCost = '0';
if (region !== 'KR') shippingCost = '15000';
const updatedDetails = {
...details,
displayItems: [
{ label: '상품 합계', amount: { currency: 'KRW', value: '49000' } },
{ label: '배송비', amount: { currency: 'KRW', value: shippingCost } }
],
total: {
label: '총 결제금액',
amount: { currency: 'KRW', value: (49000 + Number(shippingCost)).toString() }
}
};
resolve(updatedDetails);
}));
});
request.addEventListener('shippingoptionchange', (event) => {
event.updateWith(new Promise((resolve) => {
const selected = request.shippingOption;
const shippingCost = selected === 'exp' ? '3000' : '0';
const updatedDetails = {
...details,
displayItems: [
{ label: '상품 합계', amount: { currency: 'KRW', value: '49000' } },
{ label: '배송비', amount: { currency: 'KRW', value: shippingCost } }
],
total: {
label: '총 결제금액',
amount: { currency: 'KRW', value: (49000 + Number(shippingCost)).toString() }
},
shippingOptions: [
{ id: 'std', label: '일반 배송', amount: { currency: 'KRW', value: '0' }, selected: selected === 'std' },
{ id: 'exp', label: '익일 배송', amount: { currency: 'KRW', value: '3000' }, selected: selected === 'exp' }
]
};
resolve(updatedDetails);
}));
});
return request;
}, []);
const onClick = useCallback(async () => {
if (typeof window === 'undefined' || !('PaymentRequest' in window)) {
// 폴백 체크아웃
window.location.href = '/checkout';
return;
}
setLoading(true);
try {
const request = createRequest();
let canMake = true;
try {
canMake = await request.canMakePayment();
} catch (err) {
// 일부 브라우저는 에러를 던질 수 있음 - 사용자에게 시도 기회를 줌
canMake = true;
}
if (!canMake) {
window.location.href = '/checkout';
return;
}
const response = await request.show();
const ok = await fetch('/api/payments/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
methodName: response.methodName,
details: response.details,
payerEmail: response.payerEmail,
payerName: response.payerName,
shippingAddress: response.shippingAddress,
shippingOption: response.shippingOption
})
}).then((r) => r.ok);
await response.complete(ok ? 'success' : 'fail');
if (ok) {
window.location.href = '/order/success';
} else {
alert('결제 승인에 실패했습니다. 다른 결제 수단을 시도해주세요.');
}
} catch (err) {
// 사용자가 결제창을 닫았거나(AbortError) 기타 오류
console.error(err);
} finally {
setLoading(false);
}
}, [createRequest]);
return (
<button type="button" onClick={onClick} disabled={loading}>
{loading ? '처리 중…' : '빠른 결제'}
</button>
);
}
3. 동적 합계/배송 업데이트 포인트
- shippingaddresschange: 사용자가 배송지를 변경할 때 호출됩니다. 국가/우편번호에 따라 배송비와 총합을 재계산합니다.
- shippingoptionchange: 사용자가 배송 옵션을 바꿨을 때 호출됩니다. 선택 상태와 총합을 업데이트합니다.
- updateWith는 Promise 또는 객체를 받습니다. 비동기 계산(배송비 API 조회 등)에도 안전합니다.
4. 서버 검증과 영수증 처리
- response.details는 결제수단에 따라 다른 형태를 가집니다. 실제 결제 승인/정산은 반드시 서버에서 수행합니다.
- 서버에서 승인 후 response.complete('success')를 호출해야 UI가 정상 종료됩니다. 실패 시 'fail'을 전달하고 폴백 경로를 안내합니다.
5. 폴백 전략
- PaymentRequest 미지원, canMakePayment=false, 사용자 취소 시 전통적인 체크아웃 페이지로 폴백합니다.
- 버튼 문구를 “빠른 결제”로 유지하고 옆에 “다른 결제” 링크를 제공하면 이탈을 줄일 수 있습니다.
6. 실제 결제 메서드 연결 예시
아래는 서비스 적용 시 methodData를 교체하는 예시입니다. 각 메서드는 별도 상점 등록/인증이 필요합니다.
- Apple Pay(Safari):
const methodData = [
{
supportedMethods: 'https://apple.com/apple-pay',
data: {
version: 12, // Safari 버전에 맞춰 설정
merchantIdentifier: 'merchant.com.example',
merchantCapabilities: ['supports3DS'],
supportedNetworks: ['visa', 'masterCard'],
countryCode: 'KR',
currencyCode: 'KRW'
}
}
];
- Secure Payment Confirmation(SPC): FIDO/WebAuthn 기반 강인증. 선행 등록된 credentialIds가 필요합니다.
const methodData = [
{
supportedMethods: 'secure-payment-confirmation',
data: {
action: 'authenticate',
credentialIds: [/* Uint8Array 목록 */],
payeeName: 'My Store',
payeeOrigin: 'https://yourdomain.com',
instrument: {
displayName: 'Visa ••34',
icon: 'https://yourdomain.com/card.png'
},
timeout: 60000
}
}
];
- 결제 게이트웨이 연동: 일부 게이트웨이는 Payment Handler 또는 자체 JS SDK를 제공합니다. Payment Request로 가능한 환경에서는 methodData를 사용하고, 불가할 때는 SDK 폼으로 폴백하세요.
7. React/SSR 실무 팁
- PaymentRequest 인스턴스는 사용자 제스처 안에서 생성하고 show()를 호출합니다. 렌더 단계에서 생성하지 마세요.
- SSR 환경에서는 typeof window 체크로 안전하게 가드합니다.
- 상태 관리: 결제 중 버튼 비활성화, 중복 클릭 방지, 에러 알림을 명확히 합니다.
- 금액은 소수/통화 형식에 주의합니다. KRW는 소수점이 없지만 API는 문자열을 요구합니다.
8. 보안과 접근성
- HTTPS, 엄격한 CSP, 서버 측 승인/검증을 필수로 적용합니다.
- 스크린리더 친화적 버튼 라벨과 키보드 포커스 관리로 접근성을 지킵니다.
- 로깅 시 결제 상세 정보(카드 PAN 등)는 절대 저장/출력하지 않습니다.
9. 테스트 체크리스트
- 실제 단말(안드로이드, iOS Safari)에서 테스트합니다. 결제 핸들러/지갑이 설치되어 있어야 canMakePayment가 true가 됩니다.
- 사용자 취소, 네트워크 오류, 서버 승인 실패, 배송지 변경 등 엣지 케이스를 모두 시뮬레이션합니다.
- 지원 불가 환경에서도 폴백이 매끄럽게 동작하는지 확인합니다.
정리하면, Payment Request API는 지원되는 환경에서 체크아웃을 빠르게 만들어주는 훌륭한 도구입니다. React에서는 사용자 제스처 기반으로 동적으로 생성하고, 서버 검증과 폴백을 함께 구성하면 안정적으로 도입할 수 있습니다. 실제 결제수단은 서비스와 브라우저 지원에 맞춰 Apple Pay, SPC 등으로 구성하고, 기본 카드 입력은 게이트웨이 SDK 폼으로 보완하는 전략이 현실적입니다.
'React' 카테고리의 다른 글
| React 앱에서 화면 녹화(Screen Recording) 기능 구현 (0) | 2026.07.03 |
|---|---|
| React에서 비동기 컴포넌트 로딩 순서 제어하기 (0) | 2026.07.02 |
| React에서 쿠키 기반 사용자 설정 저장하기 (0) | 2026.07.01 |
| React 앱에서 로컬 네트워크 프린터 연동하기 (0) | 2026.06.30 |
| React에서 이미지 스프라이트(Sprite) 기법 적용하기 (0) | 2026.06.30 |