OPEN HYPER STEP
← 목록으로 (ai-startup)
AI-STARTUP · 52 / 100
ai-startup
CHAPTER 52 / 100
읽기 약 2
FUNCTION

Stripe 구독 결제 완전 구현


핵심 개념

Checkout·Customer Portal·Webhook·Proration — 자동 결제 + 셀프 서비스.

본문

Stripe Subscription 흐름

📋 코드 (7줄)
1. 사용자 → 가격 페이지
2. "구독" 클릭 → Stripe Checkout (호스팅 페이지)
3. 결제 완료 → success_url 리다이렉트
4. Stripe webhook → 우리 DB 업데이트
5. 사용자가 Pro 기능 사용
6. 매월 자동 결제
7. 해지 시 — Customer Portal에서 셀프

Stripe 셋업

BASH📋 코드 (1줄)
pnpm add stripe @stripe/stripe-js
TYPESCRIPT📋 코드 (13줄)
// .env
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...


// Stripe Dashboard:
// 1. Products 생성
// - Pro Monthly: $19/mo
// - Pro Annual: $190/yr (2개월 할인)
// - Business: $99/mo
// 2. Price ID 복사 (price_xxx)

Checkout Session 생성

TYPESCRIPT📋 코드 (38줄)
// app/api/checkout/route.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const { priceId } = await req.json();
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return new Response('Unauthorized', { status: 401 });

  // Stripe Customer 생성/조회
  let customerId = user.user_metadata.stripe_customer_id;
  if (!customerId) {
    const customer = await stripe.customers.create({
      email: user.email,
      metadata: { user_id: user.id },
    });
    customerId = customer.id;
    await supabase.auth.updateUser({
      data: { stripe_customer_id: customerId },
    });
  }

  const session = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_URL}/billing?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
    allow_promotion_codes: true,
    subscription_data: {
      trial_period_days: 14,
      metadata: { user_id: user.id },
    },
  });

  return Response.json({ url: session.url });
}

Webhook 처리

TYPESCRIPT📋 코드 (59줄)
// app/api/webhooks/stripe/route.ts
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: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch {
    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.user_plans.upsert({
        where: { user_id: sub.metadata.user_id },
        create: {
          user_id: sub.metadata.user_id,
          plan: priceIdToPlan(sub.items.data[0].price.id),
          stripe_subscription_id: sub.id,
          status: sub.status,
          current_period_end: new Date(sub.current_period_end * 1000),
        },
        update: {
          plan: priceIdToPlan(sub.items.data[0].price.id),
          status: sub.status,
          current_period_end: new Date(sub.current_period_end * 1000),
        },
      });
      break;
    }

    case 'customer.subscription.deleted': {
      const sub = event.data.object;
      await db.user_plans.update({
        where: { stripe_subscription_id: sub.id },
        data: { plan: 'free', status: 'canceled' },
      });
      break;
    }

    case 'invoice.payment_failed': {
      // 이메일 알림 + 재시도 안내
      const invoice = event.data.object;
      await sendPaymentFailedEmail(invoice);
      break;
    }
  }

  return Response.json({ received: true });
}


// Next.js — body raw 보장
export const runtime = 'nodejs';

Customer Portal

TYPESCRIPT📋 코드 (19줄)
// app/api/billing/portal/route.ts
export async function POST(req: Request) {
  const { user } = await getUser(req);
  const customerId = user.user_metadata.stripe_customer_id;

  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${process.env.NEXT_PUBLIC_URL}/billing`,
  });

  return Response.json({ url: session.url });
}


// 사용자가 셀프로:
// - 플랜 변경
// - 결제수단 변경
// - 인보이스 다운로드
// - 구독 해지

Stripe Tax (자동 세금)

TYPESCRIPT📋 코드 (11줄)
const session = await stripe.checkout.sessions.create({
  // ...
  automatic_tax: { enabled: true },
  customer_update: {
    address: 'auto',  // 주소 → 세금 자동
  },
});


// → 미국 sales tax, EU VAT 등 자동
// → Stripe Tax 활성화 ($0.5 per transaction)

결제 실패 자동 처리 (Smart Retries)

📋 코드 (10줄)
Stripe Dashboard → Subscriptions → Settings:
- Smart Retries 활성
- 4번까지 자동 재시도
- 실패 시 자동 cancel


또는 Dunning email:
- 1일 후: "결제 실패, 카드 확인"
- 3일 후: "곧 서비스 중단"
- 7일 후: "구독 취소됨"

다음 챕터

CH.53 "사용량 기반 과금: AI API 호출 단위".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
무료

월 $0 — 검증·시작 단계

Stripe 구독을 무료 도구만으로
시작하는 방법을 알려줘.
소자본

월 $20~50 — MVP·초기 운영

월 $20~50 예산으로 Stripe 구독을
검증·MVP 단계까지 진행하는 전략은?
프로덕션

월 $200~500 — 성장 단계

Stripe 구독을 프로덕션 단계로
확장할 때 필요한 도구·운영 체계는?
스택

풀스택 — 도구 조합 분석

2026년 Stripe 구독 관련 도구 5개를
조합한 추천 스택을 알려줘.

⭐ 이것만 기억하세요
Stripe 구독 결제 완전 구현 이 3가지만 확실히 잡으세요
1.Stripe Checkout + Webhook + Portal = 완전 자동 결제
2.Webhook이 진실 — DB는 Stripe 따라감
3.Customer Portal로 셀프 서비스 — CS 부담 90% 감소


공유하기
진행도 52 / 100