토스페이먼츠 Next.js 연동: App Router 결제 승인·금액 검증·웹훅까지
개발

토스페이먼츠 Next.js 연동: App Router 결제 승인·금액 검증·웹훅까지

블로그로 돌아가기

토스페이먼츠 V2 SDK를 Next.js App Router 라우트 핸들러에 붙이는 절차를 주문 생성, 금액 대조, 멱등키 승인, 웹훅 secret 대조와 조회 API 재검증 순서로 정리합니다. 공식 문서 기준(2026-10-17 확인)으로 AI 생성 결제 코드를 검수하는 대조표를 함께 제공합니다.

AX Specialist 김지우토스페이먼츠Next.jsApp Router결제 연동웹훅바이브코딩

토스페이먼츠 결제는 서버가 승인 API를 호출해야 확정됩니다. 이 글은 토스페이먼츠 공식 문서(2026-10-17 확인)를 기준으로 주문 생성·금액 대조·승인·웹훅 재검증을 라우트 핸들러 단위로 정리합니다. AI가 생성한 결제 코드의 검수 기준으로도 쓸 수 있습니다.

핵심 요약

  • 금액은 주문 생성 때 서버가 저장하고, successUrl의 amount를 그 값과 대조한 뒤에만 승인합니다.
  • 시크릿 키는 접두어 없는 서버 환경변수에 두고 api.tosspayments.com은 라우트 핸들러에서만 호출합니다.
  • 승인 요청에는 주문마다 저장해 둔 Idempotency-Key를 붙입니다. 멱등키는 15일간 유효합니다.
  • 일반 결제 웹훅에는 서명이 없습니다. 본문의 secret을 저장값과 대조하고 조회 API로 상태를 다시 확인합니다.
  • 가상계좌는 DEPOSIT_CALLBACK으로 입금을 확인한 시점에 완료합니다.

전체 흐름과 파일 구조

브라우저는 결제 인증만 요청하고 승인과 결과 저장은 상점 서버가 시크릿 키로 처리합니다. App Router에서는 아래처럼 파일을 나눕니다.

단계파일(예시)실행 위치와 키AI 생성 코드가 자주 틀리는 것
주문 생성app/api/orders/route.ts서버, 키 없음클라이언트가 보낸 금액을 그대로 저장
결제창 렌더·요청app/checkout/page.tsx ("use client")브라우저, 클라이언트 키시크릿 키를 NEXT_PUBLIC_*에 넣음
승인app/api/payments/confirm/route.ts서버, 시크릿 키쿼리의 amount를 검증 없이 전달
웹훅 수신app/api/webhooks/toss/route.ts서버, 시크릿 키(조회용)일반 결제 웹훅에 없는 서명 검증을 지어냄

키 분리: 테스트와 라이브, 클라이언트와 시크릿

API 키 문서 기준으로 주문서형 연동 키는 클라이언트 키가 test_gck/live_gck, 시크릿 키가 test_gsk/live_gsk로 시작합니다. g가 없는 test_ck/test_sk는 구버전 결제창·브랜드페이·자동결제용 개별 연동 키입니다. 세트가 다른 키나 테스트·라이브 키를 섞으면 INVALID_API_KEY 오류가 납니다.

환경변수는 NEXT_PUBLIC_TOSS_CLIENT_KEY와 TOSS_SECRET_KEY 두 개로 나눕니다. Next.js 문서는 NEXT_PUBLIC_ 값이 빌드 시점에 브라우저 번들에 인라인된다고 설명합니다. 테스트와 라이브는 같은 변수 이름을 배포 환경별로 다르게 채우고, 운영 서버 기동 시 시크릿 키가 live_로 시작하는지 확인합니다.

주문 생성과 결제창 요청

연동 가이드는 결제 요청 전에 orderId와 amount를 서버에 저장하라고 안내합니다. 주문 생성 라우트는 상품 ID만 받아 서버 가격표로 금액을 계산하고, 승인에 쓸 멱등키(UUID)도 함께 저장합니다. orderId는 영문 대소문자, 숫자, -, _로 이루어진 6~64자 문자열이어야 합니다.

브라우저 쪽은 클라이언트 컴포넌트에서 @tosspayments/tosspayments-sdk 패키지의 loadTossPayments(클라이언트 키)로 초기화하고 widgets({ customerKey }), setAmount({ currency: "KRW", value }), renderPaymentMethods({ selector }), renderAgreement({ selector }) 순서로 호출합니다. 결제 버튼에서는 requestPayment({ orderId, orderName, successUrl, failUrl })를 부릅니다. V2 SDK에서 금액은 객체이고 금액 변경은 setAmount()로 합니다.

customerKey에는 이메일이나 회원 ID처럼 추측 가능한 값을 쓰지 않고 비회원은 ANONYMOUS를 씁니다. requestPayment()의 Promise 방식은 모바일에서 쓸 수 없으므로 리다이렉트 방식을 기본으로 둡니다.

successUrl에서 금액 대조 후 승인

인증이 성공하면 successUrl에 paymentType, orderId, paymentKey, amount 네 값이 쿼리로 붙습니다. 성공 페이지는 이 값을 승인 라우트로 POST합니다. LLM Quick Reference는 브라우저 콘솔에서 바꾼 금액을 상점이 그대로 승인하면 그 금액으로 결제가 완료된다고 명시합니다. 승인 라우트는 저장된 금액과 비교하고 승인 API에도 저장된 금액을 넣습니다. TOSS(https://api.tosspayments.com/v1/payments)와 tossAuth()는 서버 전용 파일 lib/toss.ts에 둡니다.

// app/api/payments/confirm/route.ts
import { TOSS, tossAuth } from "@/lib/toss";
import { getOrder, saveTossResult } from "@/lib/orders";

export async function POST(req: Request) {
  const { paymentKey, orderId, amount } = await req.json();
  const order = await getOrder(orderId);
  if (!order || Number(amount) !== order.amount)
    return Response.json({ code: "AMOUNT_MISMATCH" }, { status: 400 });

  const res = await fetch(`${TOSS}/confirm`, {
    method: "POST",
    headers: {
      Authorization: tossAuth(),
      "Content-Type": "application/json",
      "Idempotency-Key": order.idempotencyKey, // 주문 생성 때 저장한 UUID
    },
    body: JSON.stringify({ paymentKey, orderId, amount: order.amount }),
  });
  if (res.ok) {
    const p = await res.json(); // Payment 객체
    await saveTossResult(orderId, { paymentKey, status: p.status, secret: p.secret });
    return Response.json({ status: p.status }); // DONE 또는 WAITING_FOR_DEPOSIT
  }
  // 실패 응답만으로 실패 처리하지 않는다: GET /v1/payments/{paymentKey}로 상태 조회 후 저장
  return Response.json({ error: await res.json() }, { status: 409 });
}

인증 헤더는 시크릿 키:를 base64로 인코딩한 Basic 값이며 콜론 누락이 가장 흔한 실수입니다. 권한(구독 등급, 다운로드 권한)은 status가 DONE일 때만 부여합니다. 가상계좌는 승인 응답이 WAITING_FOR_DEPOSIT이므로 이 시점에 완료로 표시하지 않습니다.

멱등키는 최대 300자이고 첫 요청일부터 15일간 유효합니다. 처리 중인 같은 요청을 다시 보내면 409 IDEMPOTENT_REQUEST_PROCESSING이 돌아오며, 에러가 난 요청을 키만 바꿔 재시도하는 것은 위험하다고 문서에 적혀 있습니다. 결제 인증 후 10분 안에 승인 API를 호출하지 않으면 상태가 EXPIRED가 됩니다.

failUrl에는 code, message, orderId가 붙으며 이때는 승인 API를 호출하지 않습니다. 구매자가 취소한 PAY_PROCESS_CANCELED에는 orderId가 없습니다. 실패 페이지는 code별 안내와 주문서로 돌아가는 경로를 둡니다.

웹훅: 서명 대신 secret 대조와 조회 API

웹훅은 개발자센터에서 상점아이디(MID)별로 등록합니다. 서명 헤더 tosspayments-webhook-signature는 지급대행 이벤트에만 붙습니다.

이벤트본문의 식별자검증 방법완료 처리 기준
PAYMENT_STATUS_CHANGEDdata(Payment 객체)저장한 secret 대조 후 GET /v1/payments/{paymentKey}조회 결과 status
DEPOSIT_CALLBACKorderId, secret (paymentKey 없음)저장한 secret 대조 후 GET /v1/payments/orders/{orderId}조회 결과 DONE
payout.changed, seller.changed지급대행 객체서명 헤더 HMAC SHA-256지급대행 전용

API 레퍼런스는 Payment 객체의 secret이 웹훅 본문의 secret과 같으면 정상 웹훅이라고 설명합니다. 두 이벤트를 모두 등록하면 가상계좌 웹훅이 두 번 오므로 처리는 멱등해야 합니다. 자동결제와 키인 결제는 완료 웹훅을 보내지 않습니다.

// app/api/webhooks/toss/route.ts
import { TOSS, tossAuth } from "@/lib/toss";
import { getOrder, saveTossResult } from "@/lib/orders";

export async function POST(req: Request) {
  const body = JSON.parse(await req.text());
  const p0 = body.eventType === "PAYMENT_STATUS_CHANGED" ? body.data : body;
  const order = p0?.orderId ? await getOrder(p0.orderId) : null;
  if (!order) return new Response("ignored", { status: 200 });
  if (order.tossSecret && p0.secret !== order.tossSecret)
    return new Response("secret mismatch", { status: 400 });

  const url = p0.paymentKey
    ? `${TOSS}/${encodeURIComponent(p0.paymentKey)}`
    : `${TOSS}/orders/${encodeURIComponent(p0.orderId)}`;
  const res = await fetch(url, { headers: { Authorization: tossAuth() } });
  if (!res.ok) return new Response("retry", { status: 500 }); // 재전송 유도

  const p = await res.json();
  const status = p.totalAmount === order.amount ? p.status : "AMOUNT_MISMATCH";
  await saveTossResult(order.orderId, { paymentKey: p.paymentKey, status, secret: p.secret });
  return new Response("ok", { status: 200 });
}

본문은 Next.js 웹훅 예제처럼 request.text()로 받습니다. 웹훅 가이드는 10초 안에 200 응답을 요구하고, 실패하면 1분부터 4096분까지 간격을 늘려 최대 7회(3일 19시간) 재전송합니다.

토스페이먼츠 웹훅 재전송 상태 흐름(웹훅 연결하기 가이드)

조회가 느린 구조라면 큐에 넣고 먼저 200을 응답합니다. 금액이 다르면 AMOUNT_MISMATCH로 저장해 사람이 확인합니다.

라이브 전환 전 점검과 비용

배포 체크리스트는 라이브 키·시크릿 키 노출·API 버전(최신 2022-11-16)·카드사와 통신사 심사·결제수단별 테스트·취소와 부분 취소를 점검하게 합니다. 테스트 환경 API는 분당 100건으로 제한되고 카카오페이는 계약 후 상점 테스트 키로만, 페이코는 라이브 키로만 테스트됩니다. 가상계좌는 문서용 테스트 키로 테스트할 수 없습니다. 에러 재현에는 TossPayments-Test-Code 헤더를 씁니다.

항목(토스페이먼츠 요금 안내)금액
가입비220,000원 (최초 1회)
연관리비110,000원 (연 1회)
신용·체크카드 일반, 간편결제3.4%
가상계좌건당 400원
계좌이체2.0% (최저 건당 200원)

요금 페이지(기준일 표기 없음, 2026-10-17 확인)는 부가세 10% 별도·일부 간편결제 추가 수수료·계약 형태별 가입비와 연관리비 차이를 함께 적고 있습니다. 실제 요율은 계약 시 확인합니다.

AI 도구로 연동할 때

토스페이먼츠는 MCP 서버와 llms.txt, LLM Quick Reference를 제공합니다. Claude Code 등록 명령은 claude mcp add tosspayments-integration-guide -- npx -y @tosspayments/integration-guide-mcp@latest입니다. 생성된 코드는 Quick Reference §8 오류 패턴 표로 사람이 확인합니다. 결제 구조 설계는 서비스 구축 문의하기로 요청할 수 있습니다.

함께 읽을 글

자주 묻는 질문

사업자등록 없이 테스트 키를 받을 수 있나요?

API 키 문서에 따르면 회원가입은 이메일만으로 할 수 있고, 전자결제 신청 전에는 체험 상점의 일부 테스트 키와 문서용 테스트 키로 연동할 수 있습니다. 내 상점 키와 라이브 키는 전자결제 신청 이후에 나옵니다. 신청 요건은 토스페이먼츠 고객센터(1544-7772)에 확인합니다.

웹훅 서명 검증은 어떻게 하나요?

일반 결제 웹훅에는 서명 헤더가 없습니다. 저장한 secret과 본문의 secret을 비교하고 조회 API로 상태를 확인합니다. HMAC 서명 검증은 지급대행 웹훅에만 적용합니다.

승인 API가 실패하면 결제도 실패한 것인가요?

단정하지 않습니다. Quick Reference는 승인 실패를 즉시 결제 실패로 처리하는 것을 오류 패턴으로 꼽고, 조회 API로 상태를 확인한 뒤 분기하라고 안내합니다.

MCP를 붙이면 무엇이 달라지나요?

AI 도구가 공식 문서를 직접 검색하므로 구버전 SDK 메서드나 다른 PG의 필드명이 섞일 여지가 줄어듭니다. 금액 검증과 키 관리는 상점 코드의 책임으로 남습니다.

출처

토스페이먼츠 공식 문서, 모두 2026-10-17 접속.

김지우 (Tristan Kim)

코텍시스 창업자로 AX 컨설팅과 기업 AI 교육, AI 에이전트 개발을 직접 수행합니다.

코텍시스 AI 인사이트 최신 논문 리뷰

AI 서비스 구축

MVP 설계부터 AI 내장 개발, 백오피스까지. 아이디어를 빠르고 견고하게 서비스로 만듭니다.