Next.js 16에서 Stripe 구독을 설정하는 방법 (완전 가이드)

발행: (2026년 3월 24일 AM 10:44 GMT+9)
5 분 소요
원문: Dev.to

Source: Dev.to

huangyongshan46‑a11y

우리가 만들고 있는 것

  • 새로운 구독을 위한 Stripe Checkout
  • 결제 이벤트를 위한 웹훅 처리
  • 무료 / 프로 / 엔터프라이즈 티어가 포함된 플랜 관리
  • 셀프 서비스 결제를 위한 고객 포털

1️⃣ 의존성 설치

npm install stripe @stripe/stripe-js

2️⃣ 플랜 정의하기

플랜을 위한 중앙 설정을 만드세요. 이것이 기능 및 제한에 대한 진실의 원천입니다.

// src/lib/stripe.ts
import Stripe from "stripe";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export const PLANS = {
  free: {
    name: "Free",
    price: { monthly: 0 },
    features: ["Up to 3 projects", "Basic analytics", "Community support"],
    limits: { projects: 3, aiMessages: 50 },
  },
  pro: {
    name: "Pro",
    price: { monthly: 29 },
    stripePriceId: process.env.STRIPE_PRO_PRICE_ID,
    features: [
      "Unlimited projects",
      "Advanced analytics",
      "Priority support",
      "AI assistant",
    ],
    limits: { projects: -1, aiMessages: 1000 },
  },
  enterprise: {
    name: "Enterprise",
    price: { monthly: 99 },
    stripePriceId: process.env.STRIPE_ENTERPRISE_PRICE_ID,
    features: [
      "Everything in Pro",
      "SSO/SAML",
      "Unlimited AI",
      "SLA guarantee",
    ],
    limits: { projects: -1, aiMessages: -1 },
  },
};

3️⃣ 체크아웃 API 라우트 만들기

이 코드는 Stripe Checkout 세션을 생성하고 리다이렉트 URL을 반환합니다.

// src/app/api/stripe/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { auth } from "@/lib/auth";

export async function POST(req: NextRequest) {
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { priceId } = await req.json();

  const checkoutSession = await stripe.checkout.sessions.create({
    mode: "subscription",
    payment_method_types: ["card"],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing?canceled=true`,
    metadata: { userId: session.user.id },
  });

  return NextResponse.json({ url: checkoutSession.url });
}

4️⃣ 웹훅 처리 (핵심 부분)

웹훅은 데이터베이스를 Stripe와 동기화 상태로 유지합니다.

// src/app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { db } from "@/lib/db";

export async function POST(req: NextRequest) {
  const body = await req.text();
  const signature = req.headers.get("stripe-signature")!;

  let event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch {
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
  }

  switch (event.type) {
    case "checkout.session.completed": {
      const session = event.data.object as any;
      const subscription = await stripe.subscriptions.retrieve(
        session.subscription as string
      );

      await db.subscription.upsert({
        where: { userId: session.metadata.userId },
        create: {
          userId: session.metadata.userId,
          stripeCustomerId: session.customer as string,
          stripeSubscriptionId: subscription.id,
          stripePriceId: subscription.items.data[0].price.id,
          status: "ACTIVE",
          plan: "PRO",
        },
        update: {
          stripeSubscriptionId: subscription.id,
          status: "ACTIVE",
          plan: "PRO",
        },
      });
      break;
    }

    case "customer.subscription.deleted": {
      const subscription = event.data.object as any;
      await db.subscription.update({
        where: { stripeSubscriptionId: subscription.id },
        data: { status: "CANCELED", plan: "FREE" },
      });
      break;
    }
  }

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

5️⃣ 청구 페이지

사용자에게 현재 플랜을 표시하고 업그레이드할 수 있게 합니다.

// Client‑side upgrade button
async function handleUpgrade(priceId: string) {
  const res = await fetch("/api/stripe/checkout", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ priceId }),
  });
  const { url } = await res.json();
  window.location.href = url;
}

6️⃣ 플랜별 기능 보호

// Middleware or server component
const subscription = await db.subscription.findUnique({
  where: { userId: session.user.id },
});

if (
  subscription?.plan !== "PRO" &&
  subscription?.plan !== "ENTERPRISE"
) {
  redirect("/billing");
}

Common gotchas

  • Webhook 서명 검증이 로컬에서 실패 – 개발 중에는 stripe listen --forward-to localhost:3000/api/stripe/webhook를 사용하세요.
  • 구독 상태가 동기화되지 않음 – 클라이언트 측 상태보다 웹훅을 항상 신뢰하세요.
  • 메타데이터 누락 – 체크아웃 세션 메타데이터에 항상 userId를 전달하세요.
  • 취소 처리 누락customer.subscription.deleted 이벤트를 반드시 처리하세요.

취소. customer.subscription.deleted 처리

전체 구현이 필요하신가요?

Auth.js v5, Prisma, AI 채팅, 이메일 및 아름다운 UI가 사전 연결된 전체 구현을 원하신다면 **LaunchKit**을 확인해 보세요 — 모든 것이 연결된 프로덕션‑레디 SaaS 스타터 킷입니다.

GitHub | LaunchKit 구매 ($49)

0 조회
Back to Blog

관련 글

더 보기 »