토스페이먼츠 결제 연동 — 프론트부터 웹훅까지
결제 연동은 서비스 개발에서 가장 긴장되는 작업 중 하나입니다. 돈이 오가는 만큼 실수가 허용되지 않고, 공식 문서만으로는 "실제 서비스"에서 어떻게 동작하는지 감이 잘 안 잡히죠. 코드픽이 수십 개의 프로젝트에서 토스페이먼츠를 연동해온 경험을 바탕으로, 프론트엔드 초기 설정부터 백엔드 웹훅 처리까지 한 번에 정리해드립니다.
1. 토스페이먼츠를 선택해야 하는 이유
국내 결제 PG 시장에는 KG이니시스, NHN KCP, 카카오페이 등 여러 선택지가 있습니다. 그럼에도 스타트업과 신규 서비스에서 토스페이먼츠를 선택하는 이유는 명확합니다.
개발자 경험(DX)이 압도적으로 좋습니다. 공식 문서가 한국어로 잘 정리되어 있고, JavaScript SDK가 현대적인 Promise 기반으로 설계되어 있습니다. 테스트 환경(샌드박스)도 실제 결제 플로우를 그대로 따라가서 개발 단계에서 충분히 검증할 수 있습니다.
지원 결제 수단이 다양합니다. 신용카드, 계좌이체, 가상계좌, 휴대폰 소액결제, 카카오페이, 네이버페이, 토스페이 등 국내 주요 결제 수단을 모두 커버합니다.
정산이 빠릅니다. 매일 정산 기능(D+1)을 지원해 현금흐름 관리가 용이하고, 대시보드에서 실시간 매출 현황을 확인할 수 있습니다.
2. 연동 전 준비사항
토스페이먼츠 연동을 시작하기 전에 다음을 준비하세요.
가입 및 키 발급:
- 토스페이먼츠 개발자센터에서 가입
- 테스트 클라이언트 키 / 시크릿 키 확인
- 실제 서비스 시 사업자 심사 후 라이브 키 발급
# 테스트 키 예시 (실제 사용 X)
CLIENT_KEY=test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq
SECRET_KEY=test_sk_zXLkKEypNArWmo50nX3lmeaxYG5p
필수 설정 정보:
successUrl: 결제 성공 후 리다이렉트될 URLfailUrl: 결제 실패 후 리다이렉트될 URL- 웹훅 수신 URL (백엔드 서버)
3. 프론트엔드 연동 — JavaScript SDK 세팅
SDK 설치
npm install @tosspayments/payment-sdk
또는 CDN으로 직접 로드:
<script src="https://js.tosspayments.com/v1/payment"></script>
결제창 호출
import { loadTossPayments } from '@tosspayments/payment-sdk';
async function requestPayment(orderInfo) {
const tossPayments = await loadTossPayments(process.env.TOSS_CLIENT_KEY);
await tossPayments.requestPayment('카드', {
amount: orderInfo.amount,
orderId: orderInfo.orderId, // 주문 ID (영문+숫자, 6~64자)
orderName: orderInfo.orderName, // 주문명
customerName: orderInfo.customerName,
customerEmail: orderInfo.customerEmail,
successUrl: `${window.location.origin}/payment/success`,
failUrl: `${window.location.origin}/payment/fail`,
});
}
orderId는 반드시 고유값이어야 합니다. 같은 orderId로 결제를 두 번 요청하면 오류가 발생합니다. UUID 또는 yyyyMMdd-{랜덤} 형식을 권장합니다.
import { v4 as uuidv4 } from 'uuid';
const orderId = `order-${uuidv4()}`;
SvelteKit에서의 구현 예시
<script>
import { loadTossPayments } from '@tosspayments/payment-sdk';
import { env } from '$env/dynamic/public';
async function handlePayment() {
const tossPayments = await loadTossPayments(env.PUBLIC_TOSS_CLIENT_KEY);
try {
await tossPayments.requestPayment('카드', {
amount: 50000,
orderId: `order-${crypto.randomUUID()}`,
orderName: '코드픽 개발 서비스',
customerName: '홍길동',
successUrl: `${window.location.origin}/payment/success`,
failUrl: `${window.location.origin}/payment/fail`,
});
} catch (error) {
if (error.code === 'USER_CANCEL') {
console.log('사용자가 결제를 취소했습니다.');
} else {
console.error('결제 오류:', error);
}
}
}
</script>
<button on:click={handlePayment}>결제하기</button>
4. 백엔드 연동 — 결제 승인 API
결제창에서 결제가 완료되면 successUrl로 리다이렉트되면서 쿼리 파라미터로 paymentKey, orderId, amount가 전달됩니다. 이 정보로 서버에서 결제를 최종 승인해야 실제 결제가 완료됩니다.
FastAPI 결제 승인 엔드포인트
import httpx
import base64
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter()
TOSS_SECRET_KEY = "test_sk_zXLkKEypNArWmo50nX3lmeaxYG5p"
def get_toss_auth_header():
"""토스페이먼츠 Basic 인증 헤더 생성"""
credentials = base64.b64encode(f"{TOSS_SECRET_KEY}:".encode()).decode()
return {"Authorization": f"Basic {credentials}"}
class PaymentConfirmRequest(BaseModel):
paymentKey: str
orderId: str
amount: int
@router.post("/payments/confirm")
async def confirm_payment(request: PaymentConfirmRequest):
# 1. DB에서 주문 금액 검증 (가장 중요!)
order = await get_order_from_db(request.orderId)
if not order:
raise HTTPException(status_code=404, detail="주문을 찾을 수 없습니다.")
if order.amount != request.amount:
raise HTTPException(status_code=400, detail="결제 금액이 일치하지 않습니다.")
# 2. 토스페이먼츠 승인 API 호출
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.tosspayments.com/v1/payments/confirm",
headers={
**get_toss_auth_header(),
"Content-Type": "application/json",
},
json={
"paymentKey": request.paymentKey,
"orderId": request.orderId,
"amount": request.amount,
},
)
if response.status_code != 200:
error_data = response.json()
raise HTTPException(
status_code=400,
detail=f"결제 승인 실패: {error_data.get('message', '알 수 없는 오류')}"
)
payment_data = response.json()
# 3. DB에 결제 정보 저장
await save_payment_to_db(payment_data)
return {"success": True, "payment": payment_data}
금액 검증은 절대 프론트에서 하면 안 됩니다. 반드시 서버의 DB에서 주문 금액을 가져와 비교해야 합니다. 프론트 파라미터는 얼마든지 변조 가능합니다.
5. 웹훅(Webhook) 처리 — 비동기 결제 알림
가상계좌 입금, 결제 취소 등 비동기 이벤트는 웹훅으로 처리해야 합니다. 토스페이먼츠는 이벤트 발생 시 설정한 URL로 POST 요청을 보냅니다.
웹훅 엔드포인트 구현
from fastapi import Request
import hmac
import hashlib
@router.post("/webhooks/toss")
async def toss_webhook(request: Request):
body = await request.body()
payload = await request.json()
event_type = payload.get("eventType")
if event_type == "PAYMENT_STATUS_CHANGED":
await handle_payment_status_changed(payload)
elif event_type == "PAYMENT_CANCELED":
await handle_payment_canceled(payload)
# 반드시 200 OK를 빠르게 반환
return {"success": True}
async def handle_payment_status_changed(payload: dict):
"""가상계좌 입금 완료 등 상태 변경 처리"""
data = payload.get("data", {})
order_id = data.get("orderId")
status = data.get("status")
if status == "DONE":
# 결제 완료 처리: 주문 상태 업데이트, 이메일 발송 등
await update_order_status(order_id, "paid")
await send_payment_confirmation_email(order_id)
웹훅 처리 시 주의사항:
- 200 OK를 최대한 빠르게 반환하세요. 처리가 느리면 토스가 재시도합니다.
- 같은 웹훅이 여러 번 올 수 있습니다 (멱등성 보장 필요).
paymentKey로 DB를 조회해 이미 처리된 이벤트인지 확인하세요.
6. 환불(취소) 처리
@router.post("/payments/{payment_key}/cancel")
async def cancel_payment(
payment_key: str,
cancel_reason: str,
cancel_amount: int | None = None, # None이면 전액 취소
):
cancel_body = {"cancelReason": cancel_reason}
if cancel_amount:
cancel_body["cancelAmount"] = cancel_amount
async with httpx.AsyncClient() as client:
response = await client.post(
f"https://api.tosspayments.com/v1/payments/{payment_key}/cancel",
headers={
**get_toss_auth_header(),
"Content-Type": "application/json",
},
json=cancel_body,
)
if response.status_code != 200:
raise HTTPException(status_code=400, detail="환불 처리 실패")
return response.json()
7. 실전 체크리스트
프로덕션 배포 전 반드시 확인해야 할 사항들입니다.
보안 체크리스트
- 시크릿 키가 환경변수로 관리되고 있는가 (절대 코드에 하드코딩 금지)
- 결제 금액을 서버의 DB에서 검증하는가
- HTTPS가 적용되어 있는가 (라이브 환경 필수)
- SQL Injection 방지를 위해 ORM 또는 파라미터 바인딩을 사용하는가
기능 체크리스트
- 테스트 카드로 정상 결제 플로우 확인
- 결제 취소(사용자 취소) 시 failUrl 처리 확인
- 부분 취소 동작 확인
- 가상계좌 생성 및 입금 웹훅 처리 확인
- 중복 결제 방지 로직 구현 여부
- 결제 실패 시 사용자에게 명확한 오류 메시지 표시
운영 체크리스트
- 결제 내역 관리자 대시보드 연동
- 이상 거래(금액 불일치, 중복 요청) 알림 설정
- 결제 실패율 모니터링
- 정산 주기 및 정산 계좌 확인
마치며
토스페이먼츠 연동은 공식 문서가 잘 되어있어 기본 플로우 자체는 어렵지 않습니다. 하지만 실제 서비스를 운영하다 보면 금액 변조, 중복 결제 요청, 웹훅 재시도 같은 엣지 케이스에서 문제가 생기는 경우가 많습니다.
이 글에서 소개한 패턴 — 서버에서 금액 재검증, 멱등성 보장, 빠른 웹훅 응답 — 을 지키면 안정적인 결제 시스템을 구축할 수 있습니다.
결제 연동을 포함한 서비스 개발이 필요하다면, 코드픽의 AI 바이브 코딩 팀이 도와드립니다. 기획 단계부터 프로덕션 배포까지 함께하겠습니다.