
왜 Stripe인가 — 글로벌 결제의 표준
국내 서비스라면 토스페이먼츠나 아임포트를 먼저 고려하겠지만, 글로벌 고객을 대상으로 하거나 구독형 SaaS를 만든다면 Stripe는 사실상 표준입니다. Stripe의 강점은 단순한 결제 처리를 넘어선 정교한 구독 관리, 웹훅 신뢰성, 방대한 문서와 SDK에 있습니다. FastAPI + SvelteKit 스택에서 Stripe를 연동하는 전체 과정을 실전 코드와 함께 정리합니다.
환경 설정 — API 키와 Stripe CLI
Stripe 대시보드에서 테스트 모드 API 키를 발급받고, 로컬 웹훅 테스트를 위해 Stripe CLI를 설치합니다. Stripe CLI는 웹훅을 로컬 서버로 포워딩해주는 필수 도구입니다.
# Stripe CLI 설치 (macOS)
brew install stripe/stripe-cli/stripe
# 로그인
stripe login
# 웹훅 로컬 포워딩 (개발 중)
stripe listen --forward-to localhost:8000/api/payments/webhook
# .env 설정
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_... # stripe listen 출력값
FastAPI 백엔드 — Checkout 세션 생성
Stripe Checkout은 Stripe가 호스팅하는 결제 페이지입니다. 직접 카드 폼을 만들 필요 없이 Stripe가 PCI 컴플라이언스를 처리해줍니다. FastAPI에서 Checkout 세션을 생성하는 엔드포인트를 작성합니다.
import stripe
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
import os
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
router = APIRouter(prefix="/api/payments", tags=["payments"])
class CheckoutRequest(BaseModel):
price_id: str
success_url: str
cancel_url: str
customer_email: str | None = None
@router.post("/create-checkout-session")
async def create_checkout_session(req: CheckoutRequest):
try:
session = stripe.checkout.Session.create(
payment_method_types=["card"],
line_items=[{"price": req.price_id, "quantity": 1}],
mode="payment",
success_url=req.success_url + "?session_id={CHECKOUT_SESSION_ID}",
cancel_url=req.cancel_url,
customer_email=req.customer_email,
metadata={"user_id": "..."},
)
return {"session_id": session.id, "url": session.url}
except stripe.error.StripeError as e:
raise HTTPException(status_code=400, detail=str(e))
SvelteKit 프론트엔드 — 결제 페이지 연동
SvelteKit에서 Stripe Checkout으로 리다이렉트하는 방법은 간단합니다. @stripe/stripe-js 패키지를 사용해 클라이언트 사이드에서 결제 페이지로 이동합니다. SSR 환경에서는 브라우저 체크가 필요합니다.
// src/lib/stripe.ts
import { loadStripe } from "@stripe/stripe-js";
import { PUBLIC_STRIPE_PUBLISHABLE_KEY } from "$env/static/public";
let stripePromise: ReturnType<typeof loadStripe>;
export const getStripe = () => {
if (!stripePromise) stripePromise = loadStripe(PUBLIC_STRIPE_PUBLISHABLE_KEY);
return stripePromise;
};
// src/routes/pricing/+page.svelte
async function handleCheckout(priceId: string) {
const res = await fetch("/api/payments/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
price_id: priceId,
success_url: `${location.origin}/payment/success`,
cancel_url: `${location.origin}/pricing`,
})
});
const { url } = await res.json();
window.location.href = url; // Stripe Checkout으로 이동
}
웹훅 처리 — 결제 완료 이벤트 신뢰성 있게 처리하기
결제 성공/실패 처리는 리다이렉트 URL이 아닌 웹훅으로 해야 합니다. 사용자가 브라우저를 닫거나 네트워크 문제가 생겨도 웹훅은 Stripe가 재시도해주기 때문입니다. 웹훅 서명 검증은 보안상 필수입니다.
WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET")
@router.post("/webhook")
async def stripe_webhook(request: Request):
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
try:
event = stripe.Webhook.construct_event(payload, sig_header, WEBHOOK_SECRET)
except stripe.error.SignatureVerificationError:
raise HTTPException(status_code=400, detail="Invalid signature")
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
user_id = session["metadata"].get("user_id")
amount = session["amount_total"] # 센트 단위
await create_order(user_id=user_id, amount=amount // 100, status="paid")
elif event["type"] == "payment_intent.payment_failed":
pass # 결제 실패 알림 처리
return {"status": "ok"}
구독 결제 — Stripe Billing 연동
SaaS의 월간/연간 구독은 Stripe Billing을 사용합니다. Checkout 세션의 mode를 “subscription”으로 바꾸고, Price ID를 구독 플랜의 반복 결제 Price로 설정하면 됩니다. 고객 포털(Customer Portal)을 활성화하면 구독 관리 UI도 Stripe가 제공합니다.
# 구독 Checkout 세션
session = stripe.checkout.Session.create(
payment_method_types=["card"],
line_items=[{"price": "price_monthly_plan_xxx", "quantity": 1}],
mode="subscription",
success_url="...",
cancel_url="...",
subscription_data={"trial_period_days": 14}
)
# 고객 포털 세션 생성
@router.post("/customer-portal")
async def customer_portal(customer_id: str):
session = stripe.billing_portal.Session.create(
customer=customer_id,
return_url="https://yoursite.com/dashboard",
)
return {"url": session.url}
환불 처리 — Refund API
환불은 stripe.Refund.create()로 처리합니다. 전액 환불과 부분 환불 모두 지원하며, 환불 완료 시 charge.refunded 웹훅 이벤트가 발생합니다. 환불 요청은 관리자 전용 엔드포인트로 보호해야 합니다.
@router.post("/refund")
async def refund_payment(payment_intent_id: str, amount: int | None = None):
try:
refund = stripe.Refund.create(
payment_intent=payment_intent_id,
amount=amount, # None이면 전액, 정수(센트)면 부분 환불
reason="requested_by_customer",
)
return {"refund_id": refund.id, "status": refund.status}
except stripe.error.InvalidRequestError as e:
raise HTTPException(status_code=400, detail=str(e))
테스트 카드 번호와 프로덕션 체크리스트
Stripe 테스트 모드에서는 실제 카드 없이 다양한 결제 시나리오를 테스트할 수 있습니다.
• 4242 4242 4242 4242 — 결제 성공 (Visa)
• 4000 0000 0000 0002 — 카드 거절
• 4000 0025 0000 3155 — 3D Secure 인증 필요
• 4000 0000 0000 9995 — 잔액 부족
프로덕션 전환 시 체크리스트: 라이브 API 키로 교체, Stripe 대시보드에 웹훅 엔드포인트 등록(HTTPS 필수), 웹훅 서명 검증 로직 확인, Stripe 계좌 KYC 인증 완료. 특히 KYC 인증을 미리 해두지 않으면 입금이 보류되므로 주의하세요.
코드벤터는 글로벌 결제 연동, SaaS 백엔드 구축, Stripe·다양한 결제 시스템 연동 프로젝트를 수행해왔습니다. FastAPI + SvelteKit 풀스택 개발 및 결제 시스템 구축이 필요하다면 코드벤터와 함께하세요.



