
25.09.30 / JUN
바이브코딩 웹앱 빌드 후 사용자의 비용 지불 방법 구현
- 글로벌/간편: Stripe (카드, Apple/Google Pay, Checkout 페이지 제공)
- 국내 간편결제/다수 PG 연동: PortOne(아임포트) → 토스페이먼츠/카카오페이/네이버페이 등 다수 간편결제 라우팅 쉬움
- 기타: PayPal(해외 친화), Toss Payments(카드+간편), Bootpay(국내), Paddle(탈세팅형)
바이브코딩 입문자에겐 Stripe Checkout(해외 중심) 또는 PortOne 일반결제 JS SDK(국내 중심) 추천
1) 공통 설계 마인드 (필수 개념)
- 클라이언트 vs 서버 역할
- 클라(브라우저): 상품 선택 → “결제 세션 만들기 요청”만 보냄.
- 서버(Node/Express 등): 가격·상품ID·금액을 신뢰구간에서 결정, 결제 세션/요청 생성, Webhook으로 결제 성공을 검증 후 권한 부여.
- 보안 핵심
- 공개키(클라) / 비밀키(서버) 분리(.env)
- 금액을 클라에서 받아 그대로 쓰지 말기(서버에서 재계산)
- Webhook으로 최종 검증 후 기능 개통(프리미엄 권한 부여 등)
- 유형
- 일회성 구매(예: Pro 코드 번들)
- 구독(월/년 정기 결제) → Webhook으로 결제/해지 동기화
- 테스트 모드로 먼저 전 과정 완주 → 샌드박스 카드/계정으로 검증
2A) 글로벌 쉬운 루트: Stripe Checkout (가장 빠른 MVP)


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 빠른 시작

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) 권한 설계(프로비저닝) 예시
- 결제 성공(Webhook/검증 완료) →
users테이블의plan='pro',pro_until=2025-10-26갱신 - 프론트엔드는 로그인 시 토큰/세션에
plan반영 → Pro 기능 UI 노출 - 만료일이 지나면 백엔드 미들웨어에서 Pro API 접근 차단
5) 최소 체크리스트
- 테스트 모드에서 완료 → Webhook/서버 검증 → 권한 반영까지 한 번에 흐름 성공
- 금액/상품ID는 서버에서 결정
- Idempotency(중복 방지): 같은 주문번호로 중복 처리 막기
- 로그/모니터링: 실패 원인 빠르게 파악
- 개인정보/영수증/환불 규정/이용약관/개인정보 처리방침 페이지 준비
- 운영 전 실제 소액 결제로 리허설
6) 어떤 걸로 시작할지 빠른 추천
- 해외 결제/글로벌 고객이 주: Stripe Checkout(일회성 → 구독 확장)
- 한국 사용자·국내 간편결제 필수: PortOne(아임포트) + 토스/카카오/네이버 연동