토스페이먼츠·포트원 V2·Stripe 공식 문서를 기준으로 AI가 작성한 결제 코드에서 먼저 확인할 8가지 항목을 정리합니다. 세 회사의 웹훅 검증 방식 비교, Next.js 라우트 핸들러 예제, 프롬프트 점검 문장과 grep 명령을 함께 제공합니다.
Claude Code나 Cursor로 만든 결제 코드는 화면에서 결제가 끝나도 서버 검증이 비어 있는 경우가 많습니다. 이 글은 토스페이먼츠, 포트원 V2, Stripe 공식 문서(2026-10-17 확인)를 기준으로 먼저 확인할 8가지 항목과 프롬프트 점검 문장, grep 명령을 정리합니다.
핵심 요약
- 결제 금액은 브라우저가 보낸 값으로 승인하지 않고 서버 저장 금액과 대조합니다.
- 웹훅 검증 방식은 세 회사가 모두 다릅니다. 토스페이먼츠 일반 결제 웹훅에는 서명이 없고, 포트원 V2는 Standard Webhooks 서명을, Stripe는
Stripe-Signature서명을 씁니다. - 서명 검증에는 파싱 전 원문이 필요하므로 라우트 핸들러에서
await req.text()로 받습니다. - 포트원
Webhook.verify에는Headers객체 대신Object.fromEntries(req.headers)를 넘깁니다. - 정기결제 증액과 유료 전환은 그 전 30일 이내에 소비자 동의를 받습니다(전자상거래법 제13조제6항, 시행령 제20조의2).
8가지 점검 항목
| # | 점검 항목 | 토스페이먼츠 | 포트원 V2 | Stripe |
|---|---|---|---|---|
| 1 | 금액 대조 | successUrl amount를 저장 금액과 비교 후 승인 | getPayment의 amount.total을 주문과 비교 | 웹훅 객체 금액을 주문과 비교 |
| 2 | 시크릿 키 위치 | test_sk_·live_sk_는 서버 전용 | API Secret은 서버 전용, storeId·channelKey는 브라우저용 | sk_·rk_는 서버 전용, pk_만 클라이언트 |
| 3 | 웹훅 검증 | 일반 결제는 서명 없음, 조회 API로 재확인 | webhook-signature 검증 | Stripe-Signature 검증 |
| 4 | 멱등성 | Idempotency-Key(POST 전용, 15일) | 고객사가 정하는 paymentId | Idempotency-Key 헤더 |
| 5 | 테스트와 라이브 분리 | 키 접두어 test_·live_ | 콘솔 설정 모드별 웹훅 URL·시크릿 | sk_test_·sk_live_ |
| 6 | 비동기 결제 | 가상계좌는 DEPOSIT_CALLBACK 뒤 완료 | VirtualAccountIssued와 Paid 구분 | 이번 확인 범위 밖 |
| 7 | 빌링키 결제 | 서버에서 /v1/billing/{billingKey} 호출 | 서버에서 payWithBillingKey 호출 | 이번 확인 범위 밖 |
| 8 | 법정 고지 | 사업자 정보·청약철회·정기결제 증액 동의 | 같음 | 같음 |
1. 서버 저장 금액으로 승인
토스페이먼츠 LLM Quick Reference §2.2에 따르면 requestPayment는 브라우저에서 실행되므로 콘솔에서 금액을 바꿀 수 있고, 서버가 그 값을 그대로 승인 API에 넘기면 바뀐 금액으로 결제가 끝납니다. 주문 생성 시 서버 DB에 금액을 저장하고 successUrl 값과 비교한 뒤에만 승인합니다. 포트원 웹훅 예제도 조회 응답의 amount.total이 주문 금액과 다르면 위변조 시도로 봅니다.
2. 시크릿 키의 서버 전용 배치
NEXT_PUBLIC_* 같은 공개 접두어 변수는 빌드 시 브라우저 번들에 들어갑니다. 브라우저 코드에서 api.tosspayments.com을 부르면 Authorization 헤더가 개발자 도구에 보입니다. 브라우저는 자기 서버 라우트만 부르고, 결제사 API는 그 라우트만 호출합니다.
3. 회사별 웹훅 검증 방식
토스페이먼츠 문서 §8은 일반 결제 웹훅에 tosspayments-webhook-signature 검증을 만드는 것을 LLM 실수로 꼽습니다. 이 헤더는 지급대행 웹훅에만 붙습니다.
| 구분 | 토스페이먼츠 일반 결제 | 포트원 V2 | Stripe |
|---|---|---|---|
| 서명 헤더 | 없음 | webhook-id·webhook-timestamp·webhook-signature | Stripe-Signature(t=, v1=) |
| 서명 대상 | 해당 없음 | id.timestamp.본문, HMAC SHA-256 | timestamp.본문, HMAC SHA-256 |
| 검증 방법 | paymentKey로 결제 조회 API를 다시 호출 | 서버 SDK PortOne.Webhook.verify | stripe.webhooks.constructEvent |
| 시각 허용 범위 | 해당 없음 | SDK 0.19.0 소스 기준 5분 | 공식 라이브러리 기본값 5분 |
| 재전송 | 10초 안에 200이 없으면 최대 7회 | 최대 5회, 지수 백오프 | 라이브 모드 최대 3일 |
| 중복 판별 기준 | 조회한 결제 상태 | webhook-id(명세상 재전송에도 유지) | 처리한 event.id 기록 |
포트원 SDK는 헤더 객체의 속성을 순회해 헤더를 찾으므로 Headers 인스턴스를 그대로 넘기면 MISSING_REQUIRED_HEADERS가 납니다. 공식 문서의 Express 예제는 PortOneClient에 문자열을 넘기지만 npm 0.19.0 타입 정의는 { secret } 객체를 받습니다(2026-10-17 확인).
// app/api/webhooks/portone/route.ts
import * as PortOne from "@portone/server-sdk";
const portone = PortOne.PortOneClient({ secret: process.env.PORTONE_API_SECRET! });
export async function POST(req: Request) {
const body = await req.text(); // 검증 전에 JSON으로 파싱하지 않는다
let webhook;
try {
webhook = await PortOne.Webhook.verify(
process.env.PORTONE_WEBHOOK_SECRET!,
body,
Object.fromEntries(req.headers),
);
} catch (e) {
if (e instanceof PortOne.Webhook.WebhookVerificationError) {
return new Response(null, { status: 400 });
}
throw e;
}
if ("data" in webhook && "paymentId" in webhook.data) {
const payment = await portone.payment.getPayment({ paymentId: webhook.data.paymentId });
if (payment.status === "PAID") {
const order = await findOrder(payment.id); // 내 DB 조회
if (order && order.amount === payment.amount.total) {
await markPaid(order.id); // 이미 처리된 주문이면 아무것도 바꾸지 않는다
}
}
}
return new Response(null, { status: 200 });
}
Stripe 문서는 프레임워크가 본문을 조금이라도 바꾸면 검증이 실패한다고 경고합니다.
// app/api/webhooks/stripe/route.ts
import { Stripe } from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.text();
const sig = req.headers.get("stripe-signature") ?? "";
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response(null, { status: 400 });
}
if (!(await isProcessed(event.id))) await handle(event); // 처리 뒤 event.id 기록
return new Response(null, { status: 200 });
}
토스페이먼츠 일반 결제 웹훅은 본문을 알림으로만 쓰고 결제 조회 API(GET /v1/payments/{paymentKey}) 응답으로 처리합니다.
4. 멱등성과 중복 웹훅 처리
토스페이먼츠 Idempotency-Key는 POST에만 적용되고 15일간 유효하며, 같은 키로 다시 보내면 첫 응답이 돌아옵니다. 문서는 에러를 확인하지 않고 키만 바꿔 재시도하는 것을 위험하다고 적고, 승인 API 실패 시 조회 API로 상태를 먼저 확인하라고 합니다. 웹훅 쪽은 위 표의 중복 판별 기준을 쓰고, 주문 상태 갱신은 이미 결제 완료인 주문을 다시 바꾸지 않는 조건부 갱신으로 작성합니다.
5. 테스트 키와 라이브 키 분리
토스페이먼츠 문서는 두 키를 한 환경 변수로 관리하는 것을 실수로 꼽습니다. 포트원은 실연동·테스트 모드마다 웹훅 시크릿을 따로 발급합니다. 서버 시작 시 키 접두어와 실행 환경을 대조합니다.
6. 가상계좌 입금 확인
토스페이먼츠 가상계좌는 승인 응답이 WAITING_FOR_DEPOSIT이고, 입금 뒤 DEPOSIT_CALLBACK과 조회로 DONE을 확인합니다. 포트원도 Transaction.VirtualAccountIssued와 Transaction.Paid를 따로 보냅니다.
7. 서버 전용 빌링키 결제
토스페이먼츠 빌링키 결제는 successUrl과 승인 API를 거치지 않는 서버 전용 호출입니다. API 레퍼런스는 customerKey에 이메일·회원 ID 같은 유추 가능한 값을 쓰지 말고 UUID 같은 무작위 값을 쓰라고 합니다.
8. 사업자 정보·청약철회·정기결제 고지
전자상거래법 제13조제1항은 상호·대표자 성명, 주소·전화번호·전자우편주소, 통신판매업 신고번호의 표시를 정합니다. 제17조제1항은 계약내용 서면을 받은 날부터 7일 안의 청약철회를 정하고, 제2항제5호는 디지털콘텐츠 제공이 시작된 경우를 예외로 두되 제6항의 표시·시험 사용 조치를 요구합니다. 제13조제6항과 시행령 제20조의2는 정기결제 증액이나 유료 전환 전 30일 안에 소비자 동의를 받고 해지 방법을 고지하도록 합니다. 제21조의2제1항제4호는 해지 절차를 가입보다 복잡하게 만드는 것을 금지합니다. 적용 여부는 공정거래위원회나 법률 전문가에게 확인합니다.
프롬프트에 넣을 점검 문장
아래 문장을 작업 지시에 그대로 붙입니다.
- 결제 금액은 서버 DB의 주문 금액만 쓰고, 클라이언트 값과 다르면 승인하지 않는다.
- 시크릿 키를 읽는 코드는
app/api아래route.ts에만 둔다. 공개 접두어 환경 변수에 키를 넣지 않는다. - 웹훅 라우트는
await req.text()로 원문을 받고 서명 검증 뒤에 파싱한다. - 토스페이먼츠 일반 결제 웹훅은 서명 검증 대신 결제 조회 API로 재확인한다.
- 문서에 없는 필드나 헤더를 만들지 않고, 근거가 된 공식 문서 URL을 함께 적는다.
- 주문 상태 갱신은 같은 웹훅이 두 번 와도 결과가 같게 작성한다.
토스페이먼츠(@tosspayments/integration-guide-mcp)와 포트원(npx @portone/cli@latest setup)은 공식 문서를 AI 도구에 연결하는 MCP 서버와 플러그인을 제공합니다(토스, 포트원).
grep 점검 명령
# 1. 공개 접두어 변수의 시크릿
grep -rnE '(NEXT_PUBLIC|VITE|REACT_APP|EXPO_PUBLIC)_[A-Z_]*(SECRET|SK)' --include='*.ts' --include='*.tsx' --include='.env*' .
# 2. 코드에 박힌 키
grep -rnE '(test|live)_sk_|sk_(live|test)_|whsec_' --exclude-dir=node_modules .
# 3. 클라이언트 컴포넌트의 결제사 API 호출
grep -rln 'api\.tosspayments\.com\|api\.portone\.io' app src --include='*.tsx'
# 4. 웹훅 라우트의 파싱된 본문
grep -rnE '(req|request)\.json\(' app/api/webhooks
# 5. 토스 일반 결제 웹훅의 지어낸 서명 검증
grep -rn 'tosspayments-webhook-signature' app/api
# 6. 지원 중단된 Edge 런타임
grep -rn "runtime = 'edge'\|runtime = \"edge\"" app
결과가 나온 줄을 위 항목과 대조합니다.
함께 읽을 글
- 바이브코딩 결제 연동 가이드: 국내 PG·해외 MoR 선택 기준
- 토스페이먼츠 Next.js 연동: App Router 결제 승인·금액 검증·웹훅까지
- 포트원 연동 가이드: V2 웹훅 서명 검증과 PG 계약 순서
- 결제 붙이기 전 법적 준비: 사업자등록·통신판매업 신고·환불 고지 체크리스트
- 바이브코딩 기업교육: Claude Code·코드 에이전트 실습 · 서비스 구축 사업부 · 문의하기
자주 묻는 질문
MCP 서버를 붙이면 AI가 짠 결제 코드는 안전한가요?
MCP 서버는 AI가 최신 공식 문서를 찾게 해 줍니다. 금액 대조, 키 위치, 멱등 처리는 프로젝트 구조에 따라 달라지므로 8개 항목과 grep 점검은 리뷰에서 따로 수행합니다.
코드 리뷰는 무엇부터 보나요?
웹훅 라우트와 승인 라우트부터 봅니다. 원문 본문으로 서명을 검증하는지, 조회 응답 금액을 주문 금액과 비교하는지, 같은 이벤트가 다시 와도 상태가 그대로인지 확인합니다.
테스트로 어떻게 잡나요?
금액을 바꾼 successUrl 요청, 서명이 틀린 웹훅, 같은 웹훅 두 번 전송, 가상계좌 발급 이벤트를 테스트 케이스로 만듭니다.
출처
- 토스페이먼츠 LLM Quick Reference, https://docs.tosspayments.com/guides/v2/get-started/llms-quick-reference (2026-10-17 접속)
- 토스페이먼츠 웹훅 이벤트·웹훅 연결하기, https://docs.tosspayments.com/reference/using-api/webhook-events , https://docs.tosspayments.com/guides/v2/webhook (2026-10-17 접속)
- 토스페이먼츠 인증과 멱등키 헤더, https://docs.tosspayments.com/reference/using-api/authorization (2026-10-17 접속)
- 포트원 웹훅 연동하기(V2), https://developers.portone.io/opi/ko/integration/webhook/readme-v2 (2026-10-17 접속)
- @portone/server-sdk 0.19.0 패키지 타입 정의와 소스, https://www.npmjs.com/package/@portone/server-sdk (2026-10-17 확인)
- Stripe Receive events in your webhook endpoint, https://docs.stripe.com/webhooks (2026-10-17 접속)
- Stripe API keys·Idempotent requests, https://docs.stripe.com/keys , https://docs.stripe.com/api/idempotent_requests (2026-10-17 접속)
- Standard Webhooks 명세, https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md (2026-10-17 접속)
- Next.js 16.3.8 Route Handlers·runtime 문서(패키지 동봉)
- 전자상거래 등에서의 소비자보호에 관한 법률 제13조·제17조·제21조의2, https://www.law.go.kr/법령/전자상거래등에서의소비자보호에관한법률 (2026-07-21 시행본, 2026-10-17 접속)
- 같은 법 시행령 제20조의2, https://www.law.go.kr/법령/전자상거래등에서의소비자보호에관한법률시행령/제20조의2 (2026-10-17 접속)
코텍시스 창업자로 AX 컨설팅과 기업 AI 교육, AI 에이전트 개발을 직접 수행합니다.
코텍시스 AI 인사이트 최신 논문 리뷰
토큰당 KV 캐시 크기로 계산하는 온프레미스 동시 처리량
vLLM 논문이 보인 것처럼 LLM 서빙의 동시 처리량은 GPU 메모리에서 가중치를 빼고 남은 공간을 토큰당 KV 캐시 크기로 나눈 값이 정합니다. Qwen3-8B와 Qwen3-32B의 config.json으로 계산하면 80GiB GPU 1장에서 8,192토큰 세션 상한이 57개와 9개로, 가중치 크기 차이 4배보다 큰 6배 이상의 차이가 납니다.
초안 모델로 생성 속도를 높이는 추측 디코딩
Google Research의 추측 디코딩은 작은 초안 모델이 제안한 토큰을 큰 모델이 한 번에 검증하고 확률적 수락 규칙으로 출력 분포를 그대로 유지해, T5-XXL의 생성 속도를 재학습 없이 2배에서 3배 높였습니다. M3 Pro 맥북의 Gemma 4 12B 단일 사용자 실측에서는 초안을 켜면 같은 출력을 유지한 채 생성 속도가 15.3 tok/s에서 33.4 tok/s로 2.18배 올랐습니다.
AWQ 양자화: 활성값을 보고 가중치를 지키는 4비트 양자화
MIT 연구진의 AWQ는 활성값 분포를 기준으로 중요한 가중치 1%만 보호해도 저비트 양자화 오류가 크게 줄어든다는 관찰에서 출발해 재학습 없는 4비트 양자화를 구현했고 전용 커널 TinyChat으로 FP16 대비 3배 이상의 추론 속도를 달성했습니다. 온프레미스 sLLM을 소수의 GPU로 서빙해야 하는 기업에게 정확도 손실 없이 메모리와 비용을 동시에 줄이는 사실상의 표준 기법입니다.

