토스페이먼츠 결제 연동 — 프론트부터 웹훅까지 - 코드픽 블로그
토스페이먼츠 결제 연동 — 프론트부터 웹훅까지
기술 가이드

토스페이먼츠 결제 연동 — 프론트부터 웹훅까지

2026년 4월 7일 253 views by 코드벤터

토스페이먼츠 결제 연동 — 프론트부터 웹훅까지

결제 연동은 서비스 개발에서 가장 긴장되는 작업 중 하나입니다. 돈이 오가는 만큼 실수가 허용되지 않고, 공식 문서만으로는 "실제 서비스"에서 어떻게 동작하는지 감이 잘 안 잡히죠. 코드픽이 수십 개의 프로젝트에서 토스페이먼츠를 연동해온 경험을 바탕으로, 프론트엔드 초기 설정부터 백엔드 웹훅 처리까지 한 번에 정리해드립니다.

결제 시스템 개발


1. 토스페이먼츠를 선택해야 하는 이유

국내 결제 PG 시장에는 KG이니시스, NHN KCP, 카카오페이 등 여러 선택지가 있습니다. 그럼에도 스타트업과 신규 서비스에서 토스페이먼츠를 선택하는 이유는 명확합니다.

개발자 경험(DX)이 압도적으로 좋습니다. 공식 문서가 한국어로 잘 정리되어 있고, JavaScript SDK가 현대적인 Promise 기반으로 설계되어 있습니다. 테스트 환경(샌드박스)도 실제 결제 플로우를 그대로 따라가서 개발 단계에서 충분히 검증할 수 있습니다.

지원 결제 수단이 다양합니다. 신용카드, 계좌이체, 가상계좌, 휴대폰 소액결제, 카카오페이, 네이버페이, 토스페이 등 국내 주요 결제 수단을 모두 커버합니다.

정산이 빠릅니다. 매일 정산 기능(D+1)을 지원해 현금흐름 관리가 용이하고, 대시보드에서 실시간 매출 현황을 확인할 수 있습니다.


2. 연동 전 준비사항

토스페이먼츠 연동을 시작하기 전에 다음을 준비하세요.

가입 및 키 발급:

  1. 토스페이먼츠 개발자센터에서 가입
  2. 테스트 클라이언트 키 / 시크릿 키 확인
  3. 실제 서비스 시 사업자 심사 후 라이브 키 발급
bash
# 테스트 키 예시 (실제 사용 X)
CLIENT_KEY=test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq
SECRET_KEY=test_sk_zXLkKEypNArWmo50nX3lmeaxYG5p

필수 설정 정보:

  • successUrl: 결제 성공 후 리다이렉트될 URL
  • failUrl: 결제 실패 후 리다이렉트될 URL
  • 웹훅 수신 URL (백엔드 서버)

3. 프론트엔드 연동 — JavaScript SDK 세팅

SDK 설치

bash
npm install @tosspayments/payment-sdk

또는 CDN으로 직접 로드:

html
<script src="https://js.tosspayments.com/v1/payment"></script>

결제창 호출

javascript
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-{랜덤} 형식을 권장합니다.

javascript
import { v4 as uuidv4 } from 'uuid';
const orderId = `order-${uuidv4()}`;

SvelteKit에서의 구현 예시

svelte
<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 결제 승인 엔드포인트

python
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 요청을 보냅니다.

웹훅 엔드포인트 구현

python
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. 환불(취소) 처리

python
@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 바이브 코딩 팀이 도와드립니다. 기획 단계부터 프로덕션 배포까지 함께하겠습니다.

👉 코드픽 개발 의뢰하기

개발 의뢰 상담

AI 서비스나 플랫폼 개발을
고민 중이신가요?

CodePick에서는
기획 → 개발 → 운영까지 함께합니다.
아이디어만 있어도 상담 가능합니다.

CodeVenter 개발팀이 직접 담당 · 1~2 영업일 내 회신

✓ 스타트업 MVP 개발✓ AI 서비스 개발✓ 웹 플랫폼 개발✓ 기업 시스템 구축✓ 모바일 앱 개발

AI Development Studio

코드픽 by 코드벤터

  • 대표: 윤승환 · 사업자등록번호: 121-57-64983
  • 대구광역시 중구 국채보상로 586, 16층 · info@codeventer.com

© 2025 코드벤터. All rights reserved.