stack-analysis
CHAPTER 62 / 90
읽기 약 2분
FUNCTION
결제 연동: Stripe Subscription 전체 흐름
핵심 개념
Checkout·Customer Portal·Webhook·Proration — 구독 SaaS 결제 표준.
본문
핵심 개념
Customer: 한 사용자(=tenant) = 1 customer
Product: 상품 (Pro Plan, Business Plan)
Price: 가격 (월/연·금액·통화)
Subscription: customer가 price 구독 중
Invoice: 매달 청구서 (자동 생성)Stripe 셋업
pnpm add stripe @stripe/stripe-js// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
});
// .env
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...1. Checkout Session 생성
// app/api/checkout/route.ts
import { stripe } from '@/lib/stripe';
import { auth } from '@/lib/auth';
export async function POST(req: Request) {
const { user } = await auth();
if (!user) return new Response('Unauthorized', { status: 401 });
const { priceId } = await req.json();
// Stripe customer 생성/조회
let customerId = user.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: user.email,
metadata: { userId: user.id, tenantId: user.tenantId },
});
customerId = customer.id;
await db.user.update({
where: { id: user.id },
data: { stripeCustomerId: customerId },
});
}
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.APP_URL}/billing?success=true`,
cancel_url: `${process.env.APP_URL}/billing?canceled=true`,
allow_promotion_codes: true,
billing_address_collection: 'auto',
subscription_data: {
trial_period_days: 14, // 14일 무료 체험
metadata: { tenantId: user.tenantId },
},
});
return Response.json({ url: session.url });
}클라이언트
'use client';
export function UpgradeButton({ priceId }: { priceId: string }) {
const handleClick = async () => {
const res = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ priceId }),
});
const { url } = await res.json();
window.location.href = url;
};
return <button onClick={handleClick}>Pro로 업그레이드</button>;
}2. Webhook — 구독 상태 동기화
// app/api/webhooks/stripe/route.ts
import { stripe } from '@/lib/stripe';
import { headers } from 'next/headers';
export async function POST(req: Request) {
const body = await req.text();
const sig = (await headers()).get('stripe-signature')!;
let event;
try {
event = stripe.webhooks.constructEvent(
body, sig, process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return new Response('Invalid signature', { status: 400 });
}
switch (event.type) {
case 'customer.subscription.created':
case 'customer.subscription.updated':
const sub = event.data.object;
await db.tenant.update({
where: { id: sub.metadata.tenantId },
data: {
plan: priceIdToPlan(sub.items.data[0].price.id),
stripeSubscriptionId: sub.id,
subscriptionStatus: sub.status,
currentPeriodEnd: new Date(sub.current_period_end * 1000),
},
});
break;
case 'customer.subscription.deleted':
await db.tenant.update({
where: { stripeSubscriptionId: event.data.object.id },
data: { plan: 'free', subscriptionStatus: 'canceled' },
});
break;
case 'invoice.payment_failed':
// 이메일 알림 + UI에 결제 실패 배너
await emailQueue.add('payment_failed', {
customerId: event.data.object.customer,
});
break;
}
return Response.json({ received: true });
}
// Next.js — body raw 보장
export const runtime = 'nodejs';
export const preferredRegion = 'icn1';3. Customer Portal (해지·플랜 변경)
// app/api/billing/portal/route.ts
export async function POST(req: Request) {
const { user } = await auth();
if (!user?.stripeCustomerId) {
return new Response('No subscription', { status: 400 });
}
const session = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.APP_URL}/billing`,
});
return Response.json({ url: session.url });
}
// → 사용자가 플랜 변경, 결제수단 변경, 구독 해지 모두
// → Stripe가 호스팅하는 표준 UI
// → 별도 UI 개발 불필요4. Proration (요금 비례 계산)
사용자가 월중 Pro($20) → Business($50) 변경:
- 남은 일수: 15일
- Pro 환불: $20 × 15/30 = $10
- Business 청구: $50 × 15/30 = $25
- 즉시 청구: $25 - $10 = $15
Stripe 자동 처리 — proration_behavior:
- 'create_prorations' (기본): 다음 청구서에 추가
- 'always_invoice': 즉시 청구다음 챕터
CH.63 "사용자 관리: 초대/역할/팀".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 프로젝트의 Stripe 결제 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
Stripe 결제 관련 실제 서비스 5개를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 코드베이스에서 Stripe 결제 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 풀스택 시장의 Stripe 결제 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
결제 연동: Stripe Subscription 전체 흐름은 이 3가지만 확실히 잡으세요
1.Stripe Checkout + Webhook + Portal = 표준 3단 — 자체 결제 UI 불필요
2.Webhook으로 DB 동기화 — single source of truth는 Stripe
3.Customer Portal로 셀프 서비스 — 해지·플랜 변경 자동
공유하기
진행도 62 / 90