바이브코더를 위한 수익화: 빌링(Billing) 구현

25.09.30 / JUN

바이브코딩 웹앱 빌드 후 사용자의 비용 지불 방법 구현

  • 글로벌/간편: Stripe (카드, Apple/Google Pay, Checkout 페이지 제공)
  • 국내 간편결제/다수 PG 연동: PortOne(아임포트) → 토스페이먼츠/카카오페이/네이버페이 등 다수 간편결제 라우팅 쉬움
  • 기타: PayPal(해외 친화), Toss Payments(카드+간편), Bootpay(국내), Paddle(탈세팅형)

바이브코딩 입문자에겐 Stripe Checkout(해외 중심) 또는 PortOne 일반결제 JS SDK(국내 중심) 추천


1) 공통 설계 마인드 (필수 개념)

  1. 클라이언트 vs 서버 역할
  • 클라(브라우저): 상품 선택 → “결제 세션 만들기 요청”만 보냄.
  • 서버(Node/Express 등): 가격·상품ID·금액을 신뢰구간에서 결정, 결제 세션/요청 생성, Webhook으로 결제 성공을 검증 후 권한 부여.
  1. 보안 핵심
  • 공개키(클라) / 비밀키(서버) 분리(.env)
  • 금액을 클라에서 받아 그대로 쓰지 말기(서버에서 재계산)
  • Webhook으로 최종 검증 후 기능 개통(프리미엄 권한 부여 등)
  1. 유형
  • 일회성 구매(예: Pro 코드 번들)
  • 구독(월/년 정기 결제) → Webhook으로 결제/해지 동기화
  1. 테스트 모드로 먼저 전 과정 완주 → 샌드박스 카드/계정으로 검증

2A) 글로벌 쉬운 루트: Stripe Checkout (가장 빠른 MVP)

https://stripe.com/payments/checkout
https://checkout.stripe.dev/checkout

A-1. 준비

  • Stripe 계정 생성 → 테스트 모드
  • Dashboard에서 Product / Price 생성(예: pro_monthly)
  • 비밀키(서버용) & 공개키(클라용) 발급

A-2. 서버(예: Node + Express)

npm init -y
npm i express stripe dotenv cors
// server.js
import express from "express";
import Stripe from "stripe";
import dotenv from "dotenv";
import cors from "cors";

dotenv.config();
const app = express();
app.use(cors());
app.use(express.json());

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // sk_test_...

// 1) 결제 세션 생성 (Checkout)
app.post("/create-checkout-session", async (req, res) => {
  try {
    // 서버에서 신뢰 가능한 상품/가격 결정
    const session = await stripe.checkout.sessions.create({
      mode: "subscription", // 일회성이면 "payment"
      line_items: [{ price: process.env.STRIPE_PRICE_ID, quantity: 1 }],
      success_url: "https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}",
      cancel_url: "https://yourapp.com/cancel",
      customer_email: req.body.email, // 로그인 사용자 이메일(선택)
      // 자동세금/쿠폰/프로모션 등은 필요 시 추가
    });
    res.json({ url: session.url });
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

// 2) Webhook(결제 성공/실패 이벤트 수신)
import bodyParser from "body-parser";
app.post(
  "/webhook",
  bodyParser.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.headers["stripe-signature"];
    let event;
    try {
      event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
    } catch (err) {
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    // 예) 구독 활성화
    if (event.type === "checkout.session.completed") {
      const session = event.data.object;
      // session.customer / subscription 등으로 DB에 프리미엄 권한 부여
      // e.g., grantPro(session.customer_email)
    }
    res.json({ received: true });
  }
);

app.listen(3001, () => console.log("Server on :3001"));

.env

STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_PRICE_ID=price_12345
STRIPE_WEBHOOK_SECRET=whsec_abc123

A-3. 클라이언트(바닐라 JS)

<button id="buy">구독 시작</button>
<script>
document.getElementById("buy").onclick = async () => {
  const email = "user@example.com"; // 로그인 사용자의 이메일
  const res = await fetch("http://localhost:3001/create-checkout-session", {
    method: "POST",
    headers: {"Content-Type":"application/json"},
    body: JSON.stringify({ email })
  });
  const data = await res.json();
  location.href = data.url; // Stripe Checkout 페이지로 이동
};
</script>

핵심: 결제가 끝나면 Stripe가 success_url로 리다이렉션 → 서버의 Webhook에서 “진짜 성공”을 확인하고 DB에 권한 부여.


2B) 국내 간편결제 중심: PortOne 빠른 시작

https://admin.portone.io/

B-1. 준비

  • PortOne 계정 → 연동할 PG/간편결제(토스, 카카오페이, 네이버페이 등) 설정
  • 가맹점 식별코드(impXXXX), PG 설정 키 확보(테스트 모드 가능)

B-2. 클라이언트(아임포트 JS SDK)

입문자는 클라→서버로 주문 생성 요청 → 서버가 결제요청 파라미터 생성 후 클라 SDK로 결제창 띄우기 방식이 이해하기 쉽다.

<script src="https://cdn.iamport.kr/js/iamport.payment-1.2.0.js"></script>

<button id="pay">결제하기</button>
<script>
  const IMP = window.IMP;
  IMP.init("impXXXXXXXX"); // 아임포트 가맹점 식별코드

  document.getElementById("pay").onclick = async () => {
    // 서버로 “주문 생성” 요청 → 금액/주문번호 받기
    const order = await fetch("https://yourserver.com/create-order", { method: "POST" }).then(r=>r.json());

    IMP.request_pay({
      pg: "tosspayments",
      pay_method: "card", // 카드/삼성페이/카카오페이 등
      merchant_uid: order.merchant_uid, // 서버가 발급한 주문번호(고유)
      name: "Pro 구독 1개월",
      amount: order.amount,             // 서버가 결정한 금액
      buyer_email: order.email,
    }, async (rsp) => {
      if (rsp.success) {
        // imp_uid, merchant_uid를 서버로 보내 검증 & 권한 부여 요청
        await fetch("https://yourserver.com/confirm", {
          method: "POST",
          headers: {"Content-Type":"application/json"},
          body: JSON.stringify({ imp_uid: rsp.imp_uid, merchant_uid: rsp.merchant_uid })
        });
        alert("결제 성공! 이제 Pro 기능을 사용해보세요.");
        location.href="/dashboard";
      } else {
        alert("결제 실패: " + rsp.error_msg);
      }
    });
  };
</script>

B-3. 서버(검증 & 권한부여)

  • create-order: 로그인 사용자·상품 기준으로 merchant_uid(주문번호), 금액 계산 → DB 저장
  • confirm: 아임포트 서버 API로 imp_uid를 조회하여 실제 결제금액/상태 검증 → OK면 DB에 권한 부여

포인트: 서버 검증 없이 클라 응답만 믿지 말 것. 반드시 imp_uid로 결제건을 서버에서 조회해 금액/상태를 확인해야 함.


3) 구독형(월/년) 붙일 때 핵심 포인트

  • Stripe: Price를 recurring으로 만들면 Checkout에서 구독 생성 가능. 취소/결제 실패/갱신 등은 Webhook으로 동기화.
  • PortOne: 정기결제(빌링키 발급) 기능 제공. 최초 결제 시 빌링키 생성 → 서버가 주기적으로 결제 API 호출 → 성공 시 권한 유지, 실패 시 해지 처리.

4) 권한 설계(프로비저닝) 예시

  1. 결제 성공(Webhook/검증 완료) → users 테이블의 plan='pro', pro_until=2025-10-26 갱신
  2. 프론트엔드는 로그인 시 토큰/세션에 plan 반영 → Pro 기능 UI 노출
  3. 만료일이 지나면 백엔드 미들웨어에서 Pro API 접근 차단

5) 최소 체크리스트

  • 테스트 모드에서 완료 → Webhook/서버 검증 → 권한 반영까지 한 번에 흐름 성공
  • 금액/상품ID는 서버에서 결정
  • Idempotency(중복 방지): 같은 주문번호로 중복 처리 막기
  • 로그/모니터링: 실패 원인 빠르게 파악
  • 개인정보/영수증/환불 규정/이용약관/개인정보 처리방침 페이지 준비
  • 운영 전 실제 소액 결제로 리허설

6) 어떤 걸로 시작할지 빠른 추천

  • 해외 결제/글로벌 고객이 주: Stripe Checkout(일회성 → 구독 확장)
  • 한국 사용자·국내 간편결제 필수: PortOne(아임포트) + 토스/카카오/네이버 연동

Leave a Reply