master-project
CHAPTER 44 / 50
읽기 약 2분
FUNCTION
결제 연동: Stripe Checkout + 웹훅
핵심 개념
Stripe 제품·가격·Checkout·Customer Portal·구독 상태 동기화 — 결제 풀스택.
본문
Stripe 셋업
pnpm add stripe @stripe/stripe-jsSTRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...제품·가격 생성 (Stripe Dashboard)
1. stripe.com Dashboard → Products
2. + Add product:
- Name: Pro Plan
- Price: $19/mo (Recurring)
- Lookup key: pro_monthly
3. 추가 가격: $190/yr (할인)
- Lookup key: pro_yearly
→ Price IDs 메모 (price_1ABC...)Checkout 세션 생성
// src/app/api/stripe/checkout/route.ts
import Stripe from 'stripe'
import { createClient } from '@/lib/supabase/server'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(request: Request) {
const { priceId } = await request.json()
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return new Response('unauthorized', { status: 401 })
// 기존 customer 확인
const { data: existing } = await supabase
.from('users')
.select('stripe_customer_id')
.eq('id', user.id)
.single()
let customerId = existing?.stripe_customer_id
if (!customerId) {
const customer = await stripe.customers.create({
email: user.email!,
metadata: { user_id: user.id },
})
customerId = customer.id
await supabase.from('users').update({ stripe_customer_id: customerId }).eq('id', user.id)
}
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?welcome=1`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
allow_promotion_codes: true, // 쿠폰 적용 가능
billing_address_collection: 'required',
metadata: { user_id: user.id },
})
return Response.json({ url: session.url })
}Pricing 페이지
// src/app/pricing/page.tsx
'use client'
const PLANS = [
{
name: 'Free',
price: '$0',
features: ['월 5회 생성', '기본 템플릿'],
cta: '시작하기',
href: '/signup',
},
{
name: 'Pro',
price: '$19/mo',
priceId: 'price_1ABC...',
features: ['무제한 생성', '모든 템플릿', '우선 지원'],
cta: '시작하기',
popular: true,
},
{
name: 'Team',
price: '$49/user/mo',
priceId: 'price_1DEF...',
features: ['Pro 전체', '팀 협업', 'API 접근'],
cta: '시작하기',
},
]
export default function PricingPage() {
const handleCheckout = async (priceId: string) => {
const res = await fetch('/api/stripe/checkout', {
method: 'POST',
body: JSON.stringify({ priceId }),
})
const { url } = await res.json()
window.location.href = url
}
return (
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto p-6">
{PLANS.map(p => (
<div key={p.name} className={`border rounded-lg p-6 ${p.popular ? 'ring-2 ring-primary' : ''}`}>
<h3 className="font-bold text-xl">{p.name}</h3>
<p className="text-3xl font-bold my-3">{p.price}</p>
<ul className="space-y-2 mb-6">
{p.features.map(f => <li key={f} className="text-sm">✓ {f}</li>)}
</ul>
<button
onClick={() => p.priceId ? handleCheckout(p.priceId) : (window.location.href = p.href!)}
className="w-full bg-primary text-primary-foreground py-2 rounded"
>
{p.cta}
</button>
</div>
))}
</div>
)
}Customer Portal (구독 관리)
// src/app/api/stripe/portal/route.ts
export async function POST() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
const { data: u } = await supabase.from('users').select('stripe_customer_id').eq('id', user!.id).single()
const session = await stripe.billingPortal.sessions.create({
customer: u!.stripe_customer_id!,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings`,
})
return Response.json({ url: session.url })
}Webhook 동기화 (CH.32 참조)
// 핵심 이벤트
case 'checkout.session.completed':
// 결제 완료 → 구독 활성화
break
case 'customer.subscription.updated':
// 갱신·플랜 변경
break
case 'customer.subscription.deleted':
// 취소 → free로 전환
break
case 'invoice.payment_failed':
// 결제 실패 → 사용자 알림
break환불·해지
[해지]
- Customer Portal에서 사용자가 직접
- 또는 Stripe Dashboard에서 관리자가
- 즉시 vs 기간 종료 시 (cancel_at_period_end)
[환불]
- Stripe Dashboard → Refund
- 또는 stripe.refunds.create({ charge })
- 자동 환불 정책: 7일 안 무조건 (한국 법)한국 결제 (KakaoPay·Toss)
Stripe는 한국 카드 지원이지만 KakaoPay·Toss는 별도:
- Toss Payments (1순위)
- KakaoPay
- 결제 단가 낮으면 Stripe 충분 (한국 카드 OK)다음 챕터
CH.45 "Product Hunt + 커뮤니티 런칭".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 마스터 프로젝트의 Stripe 결제 부분을 분석해서 실전 적용 + 개선 우선순위 3가지를 알려줘.
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년 한국 1인 개발자 시장의 Stripe 결제 트렌드와 차별화 포인트를 정리해줘.
⭐ 이것만 기억하세요
결제 연동: Stripe Checkout + 웹훅은 이 3가지만 확실히 잡으세요
1.Stripe Checkout + Customer Portal = 결제 풀스택
2.Webhook 동기화로 구독 상태 항상 정확
3.다음 챕터에서 런칭 전략
공유하기
진행도 44 / 50