Claude Code로 Stripe 결제 통합하기 (한국 SaaS 가이드)
Claude Code로 Stripe를 연동하려면 stripe 패키지를 설치하고, 서버 측에서 Checkout Session을 생성한 뒤 클라이언트를 Stripe 호스팅 페이지로 리다이렉트하면 됩니다. 웹훅 엔드포인트를 등록해 결제 완료·구독 갱신 이벤트를 수신하고, stripe.webhooks.constructEvent()로 서명을 검증해야 보안이 완성됩니다. 원화(KRW) 결제는 currency: "krw"와 금액 단위 원 정수로 설정하며, 한국 카드 지원은 Stripe Korea 베타(2024년 이후) 계정 승인이 필요합니다. 이 가이드는 Next.js 15 App Router 기반 TypeScript 예제와 함께 처음부터 끝까지 설명합니다.
환경 설정
Stripe SDK 설치 및 환경 변수 구성부터 시작합니다.
# 패키지 설치
bun add stripe @stripe/stripe-js
# Stripe CLI 설치 (로컬 웹훅 테스트용)
brew install stripe/stripe-cli/stripe
.env.local 파일에 키를 설정합니다:
# Stripe 대시보드 → Developers → API Keys에서 복사
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# 웹훅 서명 시크릿 (stripe listen 실행 후 출력되는 값)
STRIPE_WEBHOOK_SECRET=whsec_...
서버용 Stripe 인스턴스를 싱글턴으로 초기화합니다:
// lib/stripe.ts
import Stripe from "stripe";
// 서버 사이드에서만 사용 — 클라이언트 번들에 포함 금지
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia", // 최신 API 버전 고정
typescript: true,
});
체크아웃 세션
Next.js Route Handler에서 Checkout Session을 생성합니다. Claude Code 완전 가이드에서 API Route 패턴을 함께 참고하세요.
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
export async function POST(req: NextRequest) {
const { priceId, userId } = await req.json();
// Checkout Session 생성
const session = await stripe.checkout.sessions.create({
mode: "subscription", // 구독 결제
payment_method_types: ["card"],
line_items: [
{
price: priceId, // Stripe 대시보드에서 생성한 Price ID
quantity: 1,
},
],
// 원화 결제 설정 — KRW는 소수점 없는 정수
currency: "krw",
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
metadata: {
userId, // 결제 완료 후 사용자 식별용
},
// 한국 사업자 VAT 수집 (선택)
tax_id_collection: { enabled: true },
});
return NextResponse.json({ url: session.url });
}
클라이언트에서 체크아웃으로 이동하는 버튼 컴포넌트:
// components/CheckoutButton.tsx
"use client";
interface CheckoutButtonProps {
priceId: string;
userId: string;
}
export function CheckoutButton({ priceId, userId }: CheckoutButtonProps) {
const handleCheckout = async () => {
const res = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId, userId }),
});
const { url } = await res.json();
// Stripe 호스팅 결제 페이지로 이동
window.location.href = url;
};
return (
<button onClick={handleCheckout} className="btn-primary">
결제하기
</button>
);
}
Stripe 통합 템플릿 + Claude Code 30개 프롬프트 레시피
Power Prompts for Claude Code (₩38,000)는 Stripe 체크아웃·구독·웹훅 완성 코드, 데이터베이스 연동 패턴, 프로덕션 체크리스트를 포함합니다.
웹훅 처리 (서명 검증)
웹훅 서명 검증은 보안의 핵심입니다. Stripe가 보낸 요청인지 반드시 확인해야 합니다.
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import Stripe from "stripe";
export async function POST(req: NextRequest) {
// raw body 필수 — JSON.parse() 하면 서명 검증 실패
const body = await req.text();
const signature = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
// 서명 검증 — 위변조된 요청은 이 단계에서 차단
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error("웹훅 서명 검증 실패:", err);
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
// 이벤트 유형에 따라 처리
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutCompleted(session);
break;
}
case "customer.subscription.updated": {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionUpdated(subscription);
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionCanceled(subscription);
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
default:
// 처리하지 않는 이벤트는 무시 (200 반환 필수 — 재시도 방지)
break;
}
return NextResponse.json({ received: true });
}
async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
const userId = session.metadata?.userId;
const subscriptionId = session.subscription as string;
// DB에 구독 정보 저장
// await db.user.update({ where: { id: userId }, data: { subscriptionId } });
console.log(`결제 완료 — userId: ${userId}, 구독ID: ${subscriptionId}`);
}
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
const status = subscription.status; // active | past_due | canceled
console.log(`구독 업데이트 — ID: ${subscription.id}, 상태: ${status}`);
}
async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
console.log(`구독 해지 — ID: ${subscription.id}`);
}
async function handlePaymentFailed(invoice: Stripe.Invoice) {
console.log(`결제 실패 — 인보이스: ${invoice.id}, 고객: ${invoice.customer}`);
}
Next.js App Router에서 웹훅 엔드포인트의 bodyParser를 비활성화해야 합니다:
// app/api/webhooks/stripe/route.ts 상단에 추가
export const runtime = "nodejs"; // edge runtime은 text() 미지원
구독 관리
구독 정보 조회·업그레이드·해지 API를 구현합니다. 데이터베이스 마이그레이션 패턴은 Claude Code 데이터베이스 마이그레이션 가이드를 참고하세요.
// lib/subscription.ts
import { stripe } from "@/lib/stripe";
import Stripe from "stripe";
// 현재 구독 상태 조회
export async function getSubscription(subscriptionId: string) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
expand: ["items.data.price", "customer"],
});
return {
id: subscription.id,
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
plan: subscription.items.data[0]?.price.nickname ?? "알 수 없음",
};
}
// 플랜 업그레이드 (즉시 적용 + 비례 청구)
export async function upgradeSubscription(
subscriptionId: string,
newPriceId: string
) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
return stripe.subscriptions.update(subscriptionId, {
items: [
{
id: subscription.items.data[0].id,
price: newPriceId,
},
],
proration_behavior: "create_prorations", // 잔여 기간 비례 계산
});
}
// 구독 해지 (기간 만료 후 해지)
export async function cancelSubscription(subscriptionId: string) {
return stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true, // 즉시 해지 아님 — 기간 끝나면 자동 종료
});
}
// 구독 해지 취소 (재활성화)
export async function reactivateSubscription(subscriptionId: string) {
return stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: false,
});
}
환불 처리
부분 환불과 전액 환불을 모두 지원하는 API입니다:
// app/api/refund/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
export async function POST(req: NextRequest) {
const { paymentIntentId, amount, reason } = await req.json();
// amount가 없으면 전액 환불
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
...(amount && { amount }), // KRW 단위 정수 (원)
reason: reason ?? "requested_by_customer",
// reason 옵션: duplicate | fraudulent | requested_by_customer
});
return NextResponse.json({
refundId: refund.id,
status: refund.status, // pending | succeeded | failed
amount: refund.amount, // 환불된 금액 (원)
});
}
한국 시장 고려사항 (TossPayments 비교)
| 항목 | Stripe | TossPayments |
|---|---|---|
| 원화(KRW) 결제 | 지원 (베타 2024+) | 완전 지원 |
| 한국 카드 | 일부 지원 (Visa/MC) | 국내 전 카드사 |
| 계좌이체 | 미지원 | 지원 |
| 간편결제 (카카오·네이버) | 미지원 | 지원 |
| 해외 카드 | 완전 지원 | 제한적 |
| 사업자 등록증 필요 | 예 | 예 |
| 월 수수료 | 없음 (건당 2.9%+) | 없음 (건당 1.4~3.3%) |
결론: 해외 고객 + 구독 SaaS라면 Stripe가 유리합니다. 한국 내수만 타깃한다면 TossPayments가 카드사 커버리지와 간편결제 측면에서 우위입니다. 두 PG사를 병행하는 것도 전략적으로 유효합니다.
Stripe Korea 베타 주의사항:
- 2024년 이후 한국 법인 계정으로 일부 KRW 수신 가능
- 사업자등록번호, 대표자 신분증, 통장 사본 제출 필요
- 정산 주기: 영업일 기준 7일 (해외 계정과 동일)
Stripe CLI 로컬 테스트
웹훅을 배포하지 않고 로컬에서 테스트하는 방법입니다:
# 1. Stripe CLI 로그인
stripe login
# 2. 로컬 서버로 웹훅 포워딩 (포트 3000)
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# 출력 예시:
# Ready! Your webhook signing secret is whsec_xxxxx (paste into .env.local)
# 3. 별도 터미널에서 테스트 이벤트 발송
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_failed
# 4. 특정 이벤트만 필터링해서 포워딩
stripe listen \
--events checkout.session.completed,customer.subscription.deleted \
--forward-to localhost:3000/api/webhooks/stripe
테스트 카드 번호 (Stripe 테스트 모드):
| 번호 | 결과 |
|---|---|
| 4242 4242 4242 4242 | 결제 성공 |
| 4000 0000 0000 0002 | 결제 거절 |
| 4000 0025 0000 3155 | 3D Secure 인증 필요 |
| 4000 0000 0000 9995 | 잔액 부족 |
만료일은 미래 날짜, CVC는 임의 3자리를 입력하면 됩니다.
Stripe 통합 완성 코드 + Claude Code 프롬프트 30개
Power Prompts for Claude Code (₩38,000)는 Stripe 웹훅 재시도 로직, 구독 상태 머신, 프로덕션 체크리스트, 한국 TossPayments 병행 설정 가이드를 포함합니다.
Frequently Asked Questions
Claude Code로 Stripe를 어떻게 연동하나요?
stripe 패키지를 설치한 뒤 서버 사이드에서 stripe.checkout.sessions.create()로 체크아웃 세션을 만들고, 반환된 session.url로 사용자를 리다이렉트하면 됩니다. 결제 완료 이벤트는 웹훅 엔드포인트(/api/webhooks/stripe)에서 stripe.webhooks.constructEvent()로 서명을 검증한 뒤 처리합니다. Claude Code에게 "Next.js App Router에서 Stripe 체크아웃 세션 생성 API를 만들어줘"라고 요청하면 이 가이드의 코드 구조를 그대로 생성해 줍니다.
원화(KRW) 결제 설정은 어떻게 하나요?
Checkout Session 생성 시 currency: "krw"를 지정하고 amount는 원 단위 정수(예: 38000)로 입력합니다. KRW는 소수점이 없는 zero-decimal 통화이므로 센트 단위 변환이 불필요합니다. Stripe Korea 베타 계정이 승인된 경우 한국 Visa/Mastercard로 직접 수신할 수 있으며, 미승인 상태에서는 USD로 결제 후 환전 정산됩니다.
웹훅 서명 검증이 왜 중요한가요?
웹훅 엔드포인트는 공개 URL이므로 누구나 임의 요청을 보낼 수 있습니다. stripe.webhooks.constructEvent()는 Stripe가 각 요청에 포함시킨 HMAC-SHA256 서명을 검증해 Stripe에서 실제로 발송된 이벤트인지 확인합니다. 서명 검증을 생략하면 가짜 결제 완료 이벤트로 무단 구독 활성화 공격이 가능합니다. 반드시 req.text()로 raw body를 읽어야 하며 JSON 파싱 후에는 서명이 일치하지 않습니다.
Stripe와 TossPayments 중 어느 것을 선택해야 하나요?
해외 고객이 포함된 SaaS이거나 구독 관리 기능이 복잡하다면 Stripe를 권장합니다. Stripe는 구독 로직, 프로레이션, 쿠폰, 트라이얼 기간 등 SaaS에 필요한 기능을 모두 내장합니다. 반면 한국 내수 서비스로 카카오페이·네이버페이·계좌이체가 필요하다면 TossPayments가 적합합니다. 양쪽 고객을 모두 대상으로 한다면 Stripe(해외/카드) + TossPayments(국내 간편결제)를 병행 운영하는 전략도 효과적입니다.
Stripe CLI 없이 웹훅을 테스트할 수 있나요?
Stripe CLI가 가장 간단하지만, ngrok으로 로컬 서버를 외부에 노출한 뒤 Stripe 대시보드 → Developers → Webhooks에 ngrok URL을 등록하는 방법도 있습니다. 또는 Stripe 대시보드의 "Send test webhook" 버튼으로 특정 이벤트를 등록된 엔드포인트로 직접 발송할 수 있습니다. 로컬 개발 효율을 높이려면 Stripe CLI 사용이 권장됩니다.