참고

결제 흐름

인증과 승인 구간을 나눠 운영 판단 실수를 줄여봐요.

결제 요청부터 승인, 검증, 웹훅 처리까지 전체 시퀀스를 이해해요.

개별 API 사용법은 결제 요청, 결제 결과 수신, 결제 조회에서 확인해요. 이 문서는 전체 흐름을 한눈에 보여줘요.

개념·정책 배경 (먼저 정해야 할 것)

이 문서는 "어떻게 구현하는가" 에 집중해요. "무엇을/왜" 는 블로그에서 다뤄요.

인증 vs 승인 — 핵심 구분

PG 결제는 보통 인증​​과 승인 구간을 나눠서 보면 이해하기 쉬워요. PG·결제수단마다 화면과 이벤트 이름은 다르지만, 운영에서는 “아직 돈이 빠지지 않은 승인 전 상태인지”와 “승인이 끝난 결제 완료 상태인지”를 구분해야 CS가 꼬이지 않아요.

인증 승인
하는 일 본인확인, 결제수단 준비 PG에 결제 확정 → 금액 차감
카드 결제 카드사 앱에서 인증 → 결제창으로 복귀 결제창에서 "결제하기" 버튼 → 카드사 승인
카카오페이 카카오페이 앱에서 비밀번호 입력, 잔액 부족 시 충전 결제 확정 → 카카오페이 잔액 차감
계좌이체 은행 앱에서 본인 인증 이체 실행 → 출금
여기서 중단되면? 보통 결제 승인은 완료되지 않음 (아래 주의사항 참고) 결제 완료 상태가 되고 receipt_id로 조회 가능
"결제했는데 취소됐어요" — 인증과 승인의 혼동

카카오페이로 결제할 때 잔액이 부족하면, 인증 단계에서 지정 계좌에서 카카오페이로 충전이 일어나요. 이후 구매자가 결제를 취소하거나 네트워크 이슈로 승인까지 가지 못하면​, 카카오페이 잔액은 충전되었지만 실제 결제(승인)는 되지 않은 상태예요.

이 경우 구매자에게 "카카오페이 충전은 인증 과정에서 발생한 것이며, 결제 승인은 완료되지 않았다"고 안내해요. 충전된 카카오페이 잔액은 다음 결제에 사용할 수 있어요.

두 가지 승인 방식

서버 승인 (추천) 클라이언트 승인
설정 extra.separately_confirmed: true 기본값
승인 주체 가맹점 서버 SDK 자동
장점 승인 전 서버 검증·재고·쿠폰 판단 가능 구현이 단순
주의점 프론트엔드에 onDone 안 옴 결과 이벤트 누락에 대비해 웹훅도 함께 운영

서버 승인은 운영 정합성 측면에서 권장하지만, 일부 PG·결제수단은 서버 승인 API를 지원하지 않을 수 있어요. 그 경우에는 클라이언트에서 승인한 뒤 서버에서 receipt_id로 조회·검증하는 흐름을 사용해요.

서버 승인 흐름 (권장)

참여자: 고객 | 가맹점 프론트 | 가맹점 서버 | Bootpay | PG사

단계 흐름 유형 비고
1 고객 -> 가맹점 프론트: 결제 요청 실선
2 가맹점 프론트 -> Bootpay: requestPayment() separately_confirmed: true 실선
3 Bootpay -> PG사: 결제 요청 실선
4 PG사 -> Bootpay: 결제 결과 점선
5 Bootpay -> 가맹점 프론트: onConfirm 이벤트 점선
6 가맹점 프론트 -> 가맹점 서버: receipt_id 전달 실선
7 가맹점 서버: 재고 확인 + 비즈니스 로직 실선 재고 확인 + 비즈니스 로직
8 가맹점 서버 -> Bootpay: 승인 API 호출 실선
9 Bootpay -> 가맹점 서버: 승인 완료 점선
10 가맹점 서버: DB 저장 실선 DB 저장
11 가맹점 프론트 -> 고객: 결과 페이지 (서버 DB 조회) 점선
12 Bootpay -> 가맹점 서버: 웹훅 (done) 실선

핵심 포인트

  1. PG 승인 전에 서버 로직 실행onConfirm 시점에서 재고 확인, 쿠폰 차감 등을 처리한 뒤 승인 API를 호출해야 해요
  2. 클라이언트에 onDone 없음 — 서버가 승인하므로 클라이언트는 결제 결과를 직접 받지 못해요. 결과 페이지에서 서버 DB를 polling해야 해요
  3. 정합성 관리가 쉬움 — 재고 차감과 결제 승인을 서버 흐름 안에서 묶어 처리하므로, 클라이언트 승인 방식보다 주문 정합성을 맞추기 쉬워요

단계별 상세

1단계: 결제 요청 (프론트엔드)

extra.separately_confirmed: true로 결제창을 열어요.

const response = await Bootpay.requestPayment({
    client_key: '[ Client Key ]',
    price: 1000,
    order_name: '상품명',
    order_id: 'order_' + Date.now(), // 테스트용. 실제로는 서버에서 생성한 주문번호 사용
    extra: {
        separately_confirmed: true
    }
})javascript

2단계: 서버 승인 (가맹점 서버)

onConfirm에서 받은 receipt_id를 서버로 전달해야 해요. 서버는 비즈니스 로직을 처리한 뒤 Bootpay 승인 API를 호출해야 해요.

const confirmed = await Bootpay.confirmPayment(receipt_id)

await db.orders.update({
    status: 'done',
    bootpay_receipt_id: receipt_id,
    paid_amount: confirmed.price
}, { where: { id: order_id } })javascript

3단계: 결과 표시 (프론트엔드)

결과 페이지에서 서버에 주문 상태를 조회해야 해요.

// 결과 페이지에서 polling
const checkResult = async (orderId) => {
    const res = await fetch(`/api/orders/${orderId}/status`)
    const data = await res.json()

    if (data.status === 'done') {
        // 결제 완료 표시
    } else {
        // 아직 처리 중 — 재조회
        setTimeout(() => checkResult(orderId), 1000)
    }
}javascript

클라이언트 승인 흐름

참여자: 고객 | 가맹점 프론트 | 가맹점 서버 | Bootpay | PG사

단계 흐름 유형 비고
1 고객 -> 가맹점 프론트: 결제 요청 실선
2 가맹점 프론트 -> Bootpay: requestPayment() 실선
3 Bootpay -> PG사: 결제 요청 실선
4 PG사 -> Bootpay: 결제 결과 점선
5 Bootpay: 자동 승인 실선 자동 승인
6 Bootpay -> 가맹점 프론트: onDone 이벤트 점선
7 가맹점 프론트 -> 가맹점 서버: receipt_id 전달 실선
8 가맹점 서버 -> Bootpay: 결제 조회 (GET) 실선
9 Bootpay -> 가맹점 서버: 검증 결과 점선
10 가맹점 서버: DB 저장 실선 DB 저장
11 가맹점 서버 -> 가맹점 프론트: 결과 반환 점선
12 가맹점 프론트 -> 고객: 완료 페이지 점선
13 Bootpay -> 가맹점 서버: 웹훅 (done) 실선

핵심 포인트

  1. SDK가 자동 승인 — 별도 설정 없이 PG 결제가 완료되면 SDK가 바로 승인해요
  2. done 이후 서버 검증 필요 — 프론트엔드에서 받은 receipt_id를 백엔드로 전달하여 결제 조회을 수행해야 해요
  3. 이벤트 누락에 대비 — 네트워크 문제, 앱 종료 등으로 done 처리가 누락될 수 있으므로 웹훅을 함께 운영해야 해요

웹훅으로 이벤트 누락 보완

[정상]  프론트엔드 onDone → 백엔드 검증 → DB 저장
[누락]  부트페이 웹훅 (done) → 서버 검증 → DB 저장

클라이언트 승인 방식은 승인과 검증 사이에 시간차가 있어, 그 사이에 재고가 변동될 수 있어요. 재고·쿠폰 정합성이 중요하다면 서버 승인 방식을 우선 검토해야 해요.