이 문서는 Web SDK로 결제창을 띄울 때 참고하는 문서예요. App SDK에는 해당하지 않아요.
App SDK(iOS·Android·Flutter·React Native)는 부트페이 SDK가 내장 redirect_url을 사용해 앱 복귀를 처리해요. 가맹점에서 redirect_url을 직접 지정하지 않아야 하며, Open Type을 별도로 신경 쓸 필요도 없어요.
Open Type이 필요한 이유
Web SDK에서는 Redirect 방식을 기본값으로 권장해요. 현재 페이지가 PG 결제창으로 이동하고, 인증 결과 또는 결제 결과를 redirect_url에서 받는 방식이에요. iFrame 삽입 제한이나 팝업 차단 같은 브라우저 기술 제약을 덜 받기 때문에 결제창을 가장 안정적으로 띄울 수 있어요.
대신 Redirect 방식은 결과를 서버에서 받아야 하므로 개발량이 늘어나요. 정말 빠르게 붙여야 하거나 서버 결과 수신까지 구현하기 어려우면 iFrame 또는 Popup을 쓸 수 있어요. 다만 iFrame과 Popup은 일부 결제수단(네이버페이)이나 브라우저(인스타그램)에서 제약이 생길 수 있으므로, 가능하면 Redirect로 연동하길 권장해요.
방식 비교
| 방식 | 장점 | 단점 | 권장 사용 |
|---|---|---|---|
| Redirect | 가장 안정적 | redirect_url 서버 라우트 필요 | 기본 권장 |
| iFrame | 개발량 최소 | 네이버페이/인스타그램 제한 | 제한적인 Web 결제 |
| Popup | iFrame 미지원 결제 수단 대응 | 구매자 클릭 필요 (팝업 차단 정책)/인스타그램 제한 | iFrame 미지원 PG |
운영 결제라면 웬만하면 redirect를 사용해요. 개발량을 줄이는 것이 더 중요하고 제약을 감수할 수 있을 때만 iframe 또는 popup을 선택해요.
Redirect 방식
Redirect 방식은 현재 페이지가 PG 결제창으로 이동하는 방식이에요. 결제 안정성은 가장 높지만, 인증 결과와 결제 결과를 프론트엔드 콜백이 아니라 redirect_url에서 받아야 해요.
Web SDK에서 구매자가 카드사 인증(승인 전)하면 결제 요청 시 보냈던 redirect_url로 승인 전 결과가 전달돼요. Web redirect에서는 프론트엔드 콜백보다 서버 라우트가 먼저 결과를 받아요.
redirect_url에 confirm이 들어왔다는 것은 카드사 인증이 끝났고 승인 직전이라는 뜻이에요. 아직 주문 확정 상태가 아니므로 서버에서 조회 API로 receipt_id를 다시 확인하고, DB에 저장해 둔 금액·주문 상태와 비교한 뒤 승인 API를 호출해야 해요.
iFrame 방식
개발 중인 Web 페이지에 부트페이 SDK가 iFrame을 삽입해 결제창을 띄우는 방식이에요. Redirect보다 구현은 간단하지만, iFrame을 지원하지 않는 결제 환경에서는 사용할 수 없어요.
기술적 제약으로 네이버페이, 이니시스 모바일 카드 결제에서 일부 기능이 정상 동작하지 않을 수 있어요. 이 두 결제 방식에서는 Popup 또는 Redirect 방식을 권장해요.
Popup 방식
팝업 결제는 구매자에게 팝업 진행 여부를 먼저 물어요. 구매자가 "결제 계속하기" 버튼을 누르면 팝업으로 결제가 진행돼요.
구매자의 직접적인 액션이 없으면 브라우저 팝업 차단 정책에 걸릴 수 있어요. 그래서 Popup 방식은 결제 전 확인 단계가 한 번 더 들어가요.
Open Type별 결과 처리
결과 처리는 크게 두 가지로 나눠요.
redirect는 Web SDK에서 쓰고, 인증 결과를redirect_url서버 라우트가 먼저 받아요.iframe/popup은 결제창이 닫힌 뒤 클라이언트 코드가confirm,done,issued,cancel,error를 받아요.
App SDK는 Open Type 선택 대상은 아니지만, confirm과 done 콜백의 의미는 아래 iframe / popup 흐름과 같아요.
redirect일 때
Web SDK에서 open_type: 'redirect'를 쓰면 구매자가 카드사 인증을 마친 뒤 redirect_url로 돌아와요. 이때 confirm은 인증 완료 상태예요. 아직 결제 승인이나 주문 확정이 아니므로 서버에서 receipt_id로 결제 정보를 조회하고, DB 주문 금액·상태와 비교한 뒤 승인 API를 호출해야 해요.
import { Bootpay } from '@bootpay/client-js'
try {
await Bootpay.requestPayment({
client_key: '[ Client Key ]',
price: 50000,
order_name: '나이키 운동화 외 2건',
order_id: 'order_' + Date.now(),
extra: {
open_type: 'redirect',
redirect_url: 'https://yoursite.com/order/result'
}
})
} catch (data) {
// redirect로 이동하기 전에 취소·오류가 난 경우만 여기서 처리해요.
if (data.event === 'cancel') keepOrderUnpaid(data)
if (data.event === 'error') showPaymentError(data.message)
}
// redirect에서는 confirm/done 결과를 프론트 콜백보다 redirect_url 서버 라우트가 먼저 받아요.javascriptapp.get('/order/result', async (req, res) => {
const { event, receipt_id, order_id } = req.query
if (event === 'confirm') {
// confirm = 구매자 인증 완료, 승인 직전 상태예요.
// 서버에서 결제 조회 → DB 금액·주문 상태 검증 → 승인 API 호출 순서로 처리해야 해요.
await approveAfterServerCheck(receipt_id, order_id)
return res.redirect(`/orders/${order_id}/complete`)
}
if (event === 'done') {
// 서버 승인 분리를 쓰지 않는 PG/흐름에서는 done으로 돌아올 수 있어요.
// 그래도 서버에서 receipt_id로 다시 조회한 뒤 주문을 확정해야 해요.
await verifyPayment(receipt_id)
return res.redirect(`/orders/${order_id}/complete`)
}
return res.redirect(`/orders/${order_id}/failed`)
})javascriptredirect_url로 넘겨받는 데이터는 상세 결제 정보가 아니에요. 서버에서는 receipt_id와 order_id를 우선 확보하고, 결제 상세 정보는 receipt_id로 다시 조회해야 해요.
redirect가 아닐 때
iframe / popup에서는 클라이언트가 결과 이벤트를 받아요. confirm은 인증 완료 상태이고, 여기서 Bootpay.confirm(), return true, transactionConfirm() 같은 SDK별 승인 함수를 실행하면 결제 승인 요청이 진행돼요. 승인 요청이 성공하면 이후 done이 호출돼요.
import { Bootpay } from '@bootpay/client-js'
const response = await Bootpay.requestPayment({
client_key: '[ Client Key ]',
price: 50000,
order_name: '나이키 운동화 외 2건',
order_id: 'order_' + Date.now(),
extra: {
open_type: 'iframe', // popup도 같은 이벤트 흐름으로 처리해요.
separately_confirmed: true
}
})
if (response.event === 'confirm') {
// confirm = 인증 완료, 승인 직전 상태예요.
// 서버 사전 검증이 필요하면 여기서 먼저 검증해요.
await checkOrderBeforeApproval(response.receipt_id)
// Bootpay.confirm()을 호출하면 결제 승인 요청이 진행돼요.
const approved = await Bootpay.confirm()
if (approved.event === 'done') {
// 승인 요청 성공 후 done이 와요. 서버 검증 후 주문을 확정해야 해요.
await verifyPayment(approved.receipt_id)
}
}
if (response.event === 'done') {
// 서버 승인 분리를 쓰지 않으면 requestPayment 응답으로 done을 받을 수 있어요.
await verifyPayment(response.receipt_id)
}javascriptBootpay.requestPayment(viewController: self, payload: payload)
.onConfirm { data in
// confirm = 인증 완료, 승인 직전 상태예요.
// 서버 사전 검증이 필요하면 여기서 먼저 검증해요.
let canApprove = true
if canApprove {
// true를 반환하면 SDK가 결제 승인 요청을 진행해요.
return true
}
// 직접 승인 함수를 호출하는 방식도 가능해요.
// Bootpay.transactionConfirm()
return false
}
.onDone { data in
// 승인 요청 성공 후 done이 와요. 서버 검증 후 주문을 확정해야 해요.
verifyPayment(receiptId: data["receipt_id"] as? String ?? "")
}
.onError { data in print("error: \(data)") }
.onCancel { data in print("cancel: \(data)") }swiftBootpay.init(this)
.setPayload(payload)
.setEventListener(object : BootpayEventListener {
override fun onConfirm(data: String): Boolean {
// confirm = 인증 완료, 승인 직전 상태예요.
// 서버 사전 검증이 필요하면 여기서 먼저 검증해요.
val canApprove = true
if (canApprove) {
// true를 반환하면 SDK가 결제 승인 요청을 진행해요.
return true
}
// 직접 승인 함수를 호출하는 방식도 가능해요.
// Bootpay.transactionConfirm(data)
return false
}
override fun onDone(data: String) {
// 승인 요청 성공 후 done이 와요. 서버 검증 후 주문을 확정해야 해요.
verifyPayment(data)
}
override fun onIssued(data: String) {}
override fun onError(data: String) {}
override fun onCancel(data: String) {}
override fun onClose() { Bootpay.removePaymentWindow() }
})
.requestPayment()kotlinBootpay().requestPayment(
context: context,
payload: payload,
onConfirm: (String data) {
// confirm = 인증 완료, 승인 직전 상태예요.
// 서버 사전 검증이 필요하면 여기서 먼저 검증해요.
final canApprove = true;
if (canApprove) {
// transactionConfirm()을 호출하면 결제 승인 요청이 진행돼요.
Bootpay().transactionConfirm();
return false;
}
// 바로 승인해도 되는 흐름이라면 return true로 처리할 수도 있어요.
// return true;
return false;
},
onDone: (String data) {
// 승인 요청 성공 후 done이 와요. 서버 검증 후 주문을 확정해야 해요.
verifyPayment(data);
},
onIssued: (String data) {},
onError: (String data) {},
onCancel: (String data) {},
onClose: () {},
);dartconst bootpay = useRef<Bootpay>(null)
const onConfirm = (data: string) => {
// confirm = 인증 완료, 승인 직전 상태예요.
// 서버 사전 검증이 필요하면 여기서 먼저 검증해요.
// transactionConfirm(data)를 호출하면 결제 승인 요청이 진행돼요.
bootpay.current?.transactionConfirm(data)
}
const onDone = (data: string) => {
// 승인 요청 성공 후 done이 와요. 서버 검증 후 주문을 확정해야 해요.
verifyPayment(data)
}
<Bootpay
ref={bootpay}
client_key={'[ Client Key ]'}
onConfirm={onConfirm}
onDone={onDone}
onIssued={(data) => savePendingVbankOrder(data)}
onError={(data) => showPaymentError(data)}
onCancel={(data) => keepOrderUnpaid(data)}
onClose={() => resetPaymentUi()}
/>tsxconfirm에서 클라이언트 승인 함수를 호출하지 않고 서버로 receipt_id를 보내요. 서버가 주문 금액·상태를 검증한 뒤 승인 API를 호출하면 돼요. 서버 승인 방식에서는 클라이언트의 done이 호출되지 않으므로 결과 페이지에서 서버 DB를 조회해요.
Open Type 설정하기
Web SDK 결제 요청 시 extra 옵션으로 Open Type을 설정해요. 운영에서는 redirect를 기본으로 두는 것을 권장해요. 브라우저별 예외 처리는 가능하지만, 꼭 필요한 상황이 아니라면 복잡하게 분기하지 않아도 돼요.
import { Bootpay } from '@bootpay/client-js'
await Bootpay.requestPayment({
// 기존 요청과 동일
extra: {
open_type: 'redirect',
redirect_url: 'https://yoursite.com/order/result'
}
})javascript옵션 설명
| 파라미터 | 설명 |
|---|---|
extra.open_type |
결제창 오픈 타입. redirect, iframe, popup 중 선택해요. 별도 이유가 없다면 redirect를 권장해요. |
extra.redirect_url |
redirect 결과를 받을 가맹점 서버 URL이에요. Web SDK에서만 직접 지정해요. |
extra.browser_open_type |
브라우저별로 Open Type을 다르게 지정할 수 있는 예외 옵션이에요. 카카오톡, 네이버앱, 모바일 사파리처럼 특정 브라우저에서만 다르게 열어야 할 때 사용해요. |
browser_open_type으로 브라우저별 분기 처리를 할 수는 있어요. 다만 운영 기본값은 redirect로 두고, 실제로 문제가 확인된 브라우저만 예외 처리하는 편이 관리하기 쉬워요.
