포트원 가입부터 전자결제 신청, PG 계약, 실연동 채널 추가, V2 결제 조회 검증까지의 순서를 공식 문서 기준(2026-10-17 확인)으로 정리합니다. Next.js App Router 라우트 핸들러에서 @portone/server-sdk의 Webhook.verify로 Standard Webhooks 서명을 검증하는 코드를 함께 제공합니다.
포트원(PortOne)은 여러 PG사의 결제창과 API를 하나의 SDK로 호출하게 해 주는 연동 계층이고, 결제대행 계약은 PG사와 따로 맺습니다. 이 글은 포트원 개발자센터 문서(2026-10-17 확인)를 기준으로 가입부터 PG 계약, 채널 추가, V2 결제, Standard Webhooks 서명 검증까지의 순서를 Next.js App Router 코드와 함께 정리합니다.
핵심 요약
- 포트원 콘솔 가입은 별도 계약 없이 할 수 있고, 테스트 채널로 전자결제 신청 전에 개발을 시작할 수 있습니다.
- 전자결제 신청에는 사업자등록증이 필요합니다. 문서는 사업자등록증이 없으면 신청할 수 없다고 명시합니다.
- 실연동 채널은 PG사와 계약한 뒤 PG 가맹점 관리자에서 받은 상점아이디(MID)와 키를 포트원 콘솔에 입력해 만듭니다.
- V2 결제 완료는 서버가
GET https://api.portone.io/payments/{paymentId}로 조회해amount.total과 주문 금액을 대조한 뒤 처리합니다. - V2 웹훅은 Standard Webhooks 서명을 붙이며,
@portone/server-sdk의Webhook.verify에 원문 문자열 본문을 넘겨 검증합니다.
가입부터 실연동까지의 순서
포트원 문서의 연동 준비 가이드와 전자결제 신청 가이드를 합치면 순서는 아래와 같습니다.
| 순서 | 할 일 | 위치 | 받는 값 |
|---|---|---|---|
| 1 | 콘솔 회원가입, 이메일 인증 | admin.portone.io | 콘솔 계정 |
| 2 | 테스트 채널 추가 | 결제 연동 → 연동 관리 → 채널 관리(테스트 모드) | 테스트 채널 키 |
| 3 | 비즈니스 인증 | 전자결제 신청 화면 | 사업자등록증 사본, 담당자, 서비스 URL 제출 |
| 4 | PG사·결제수단 선택 | 추천 패키지 또는 직접 선택 | 신청 내역 |
| 5 | PG 계약 후 실연동 채널 추가 | PG 가맹점 관리자 → 포트원 채널 관리(실연동 모드) | PG의 MID와 PG별 키 |
| 6 | 연동 정보 발급 | 연동 관리 → 식별코드·API Keys, 결제알림(Webhook) 관리 | Store ID, V2 API Secret, 웹훅 시크릿 |
3단계의 서비스 URL은 운영 중인 웹이나 앱 주소여야 하며, 신청 가이드는 PG 계약을 위한 서비스 기본 구성 요건을 먼저 반영하라고 안내합니다. 5단계에서 콘솔에 입력할 값은 PG사마다 다릅니다. 예를 들어 나이스정보통신은 MID·Key·결제취소 비밀번호를, 스마트로는 MID·Merchant Key·결제취소 비밀번호를 입력합니다. 이미 계약한 PG가 있으면 신청 화면에서 고객센터로 연결됩니다.
식별값과 비밀값 관리
V2 연동에는 공개해도 되는 값과 서버에만 두는 값이 섞여 있습니다.
| 값 | 발급 위치 | 사용처 | 환경변수(예시) |
|---|---|---|---|
| Store ID | 연동 관리 화면 | 브라우저 requestPayment의 storeId | NEXT_PUBLIC_PORTONE_STORE_ID |
| 채널 키 | 채널 관리(테스트·실연동 각각) | 브라우저 channelKey | NEXT_PUBLIC_PORTONE_CHANNEL_KEY |
| V2 API Secret | 식별코드·API Keys → V2 API | 서버 REST 호출 | PORTONE_API_SECRET |
| 웹훅 시크릿 | 결제알림(Webhook) 관리(테스트·실연동 각각) | 서버 웹훅 검증 | PORTONE_WEBHOOK_SECRET |
V2 API Secret은 Owner, Admin 계정만 발급할 수 있고 발급 직후에만 값을 확인할 수 있습니다. 문서는 이 값을 고객사 서버에서만 쓰고 브라우저에 노출하지 말라고 적고 있습니다. 웹훅 시크릿은 테스트와 실연동 환경별로 동시에 최대 2개까지 발급할 수 있어, 새 시크릿을 발급해 서버에 반영한 뒤 기존 시크릿을 만료시키는 방식으로 무중단 교체를 할 수 있습니다.
V2 결제 요청과 서버 검증
브라우저는 @portone/browser-sdk를 설치하고 @portone/browser-sdk/v2에서 가져온 PortOne.requestPayment()로 결제창을 엽니다. 결제 금액은 주문 생성 시 서버가 정해 저장해 둔 값을 씁니다.
"use client";
import * as PortOne from "@portone/browser-sdk/v2";
export async function pay(order: { paymentId: string; orderName: string; amount: number }) {
await PortOne.requestPayment({
storeId: process.env.NEXT_PUBLIC_PORTONE_STORE_ID!,
channelKey: process.env.NEXT_PUBLIC_PORTONE_CHANNEL_KEY!,
paymentId: order.paymentId, // 서버가 주문 생성 때 만든 값
orderName: order.orderName,
totalAmount: order.amount,
currency: "CURRENCY_KRW",
payMethod: "CARD",
redirectUrl: `${window.location.origin}/payments/redirect`,
forceRedirect: true,
});
}
결제 연동 문서는 모바일을 지원하려면 redirectUrl을 반드시 지정하라고 안내합니다. 리다이렉트 쿼리에는 paymentId가 항상 붙고, 실패 시 code, message, pgCode, pgMessage가 더 붙습니다. 같은 paymentId로 여러 번 결제를 시도할 수 있지만 최종 성공은 한 번뿐이므로 paymentId가 중복 결제 방지의 기준이 됩니다.
리다이렉트 페이지는 paymentId만 서버로 보냅니다. 서버는 결제 조회 API로 상태와 금액을 확인합니다. 문서의 REST 예시는 Authorization: PortOne {API Secret} 헤더를 쓰고, 서버 SDK는 같은 호출을 PortOneClient({ secret })로 감쌉니다.
// lib/portone.ts (서버 전용)
import { PortOneClient } from "@portone/server-sdk";
import { getOrderByPaymentId, saveStatus } from "@/lib/orders"; // 프로젝트의 주문 저장소
const portone = PortOneClient({ secret: process.env.PORTONE_API_SECRET! });
export async function syncPayment(paymentId: string) {
const order = await getOrderByPaymentId(paymentId);
if (!order) return "UNKNOWN_ORDER";
const payment: any = await portone.payment.getPayment({ paymentId }).catch(() => null);
if (!payment) return "NOT_FOUND"; // 조회 실패(GetPaymentError)는 미결제로 둔다
if (payment.amount?.total !== order.amount) {
await saveStatus(paymentId, "AMOUNT_MISMATCH"); // 위변조 의심, 사람이 확인
return "AMOUNT_MISMATCH";
}
await saveStatus(paymentId, payment.status); // PAID일 때만 권한 부여
return payment.status;
}
// app/api/payments/complete/route.ts
import { syncPayment } from "@/lib/portone";
export async function POST(req: Request) {
const { paymentId } = await req.json();
return Response.json({ status: await syncPayment(paymentId) });
}
조회 결과의 status가 VIRTUAL_ACCOUNT_ISSUED이면 가상계좌만 발급된 상태이므로 완료 처리하지 않고, PAID일 때 권한을 부여합니다. saveStatus는 이미 PAID인 주문을 같은 값으로 다시 써도 결과가 같도록 만듭니다. 리다이렉트 경로와 웹훅 경로가 같은 syncPayment를 부르기 때문입니다. 서버 SDK는 Node.js v20 이상이 필요합니다.
V2 웹훅 서명 검증
웹훅 URL과 시크릿은 콘솔의 결제알림(Webhook) 관리 탭에서 웹훅 버전 "결제모듈 V2"를 고른 뒤 설정합니다. 최신 웹훅 버전 2024-04-25는 본문이 Standard Webhooks 형식이고 Content Type은 application/json만 지원합니다.
type | 의미 | 처리 |
|---|---|---|
Transaction.Paid | 결제(예약 결제 포함) 승인 | syncPayment 후 PAID면 권한 부여 |
Transaction.VirtualAccountIssued | 가상계좌 발급 | 입금 안내만, 완료 처리 안 함 |
Transaction.Cancelled, Transaction.PartialCancelled | 전체·부분 취소 | syncPayment로 상태와 권한 갱신 |
Transaction.Failed | 결제 실패 | 주문 상태만 갱신 |
알지 못하는 type | 문서 기준 예고 없이 추가될 수 있음 | 오류 없이 200으로 무시 |
서버 SDK의 verify 함수는 헤더를 일반 객체(Record<string, string>)로 받고 키를 Object.entries로 찾습니다. Next.js 라우트 핸들러의 req.headers는 Headers 인스턴스이므로 Object.fromEntries(req.headers)로 바꿔 넘깁니다. 그대로 넘기면 필수 헤더(webhook-id, webhook-timestamp, webhook-signature)를 찾지 못해 검증이 실패합니다. 본문은 JSON으로 파싱하지 않고 문자열 그대로 넘깁니다.
// app/api/webhooks/portone/route.ts
import { Webhook } from "@portone/server-sdk";
import { syncPayment } from "@/lib/portone";
export async function POST(req: Request) {
const raw = await req.text();
let webhook;
try {
webhook = await Webhook.verify(
process.env.PORTONE_WEBHOOK_SECRET!,
raw,
Object.fromEntries(req.headers),
);
} catch (e) {
if (e instanceof Webhook.WebhookVerificationError)
return new Response("invalid signature", { status: 400 });
throw e;
}
if (Webhook.isUnrecognizedWebhook(webhook)) return new Response("ignored", { status: 200 });
if ("paymentId" in webhook.data) await syncPayment(webhook.data.paymentId);
return new Response("ok", { status: 200 });
}
서명이 맞아도 웹훅 본문의 상태를 그대로 믿지 않고 syncPayment로 조회합니다. 웹훅 문서의 예제도 서명 검증 뒤 결제 단건 조회와 금액 대조를 거칩니다. SDK 소스(webhook.ts)는 webhook-timestamp가 현재 시각과 5분 넘게 차이 나면 TIMESTAMP_TOO_OLD 또는 TIMESTAMP_TOO_NEW로 거부하므로 서버 시계가 맞는지도 확인합니다.
운영 조건은 다음과 같습니다. 연결과 읽기 타임아웃은 각각 30초입니다. 실패하면 최대 5회 재전송하며 대기 시간은 0·1·4·16·64·256분에 지터가 더해집니다. 재전송되어도 본문의 timestamp는 같습니다. 포트원 서버는 웹훅 응답을 기다리지 않고 브라우저를 리다이렉트하므로 둘의 도착 순서는 보장되지 않습니다. 순서 보장이 필요하면 storeId를 적어 tech.support@portone.io에 요청할 수 있습니다. IP 필터를 쓰는 경우 V2 웹훅 IP는 52.78.5.241입니다.
AI 도구로 연동할 때
포트원은 Claude Code 플러그인 AI Codemate를 제공합니다. Node.js 22.6.0 이상에서 npx @portone/cli@latest setup을 실행하면 플러그인이 등록되고, Claude Code에서 /portone-integration:start로 대화형 연동 코드 생성을 시작합니다. MCP 서버는 npx -y @portone/mcp-server@latest로 등록하며, 문서 검색에 더해 콘솔 로그인 후 채널 목록 조회·테스트 채널 추가·결제 내역 조회를 수행할 수 있습니다. 문서는 MCP로 조회한 API 응답이 제3자 AI 서비스로 전달되어 그 서비스 정책에 따라 저장될 수 있다고 경고합니다. 운영 계정에서는 이 점을 검토한 뒤 씁니다. PG 선택이나 기존 서비스와의 권한 연동 설계는 서비스 구축 문의하기로 요청할 수 있습니다.
함께 읽을 글
- 바이브코딩 결제 연동 가이드: 국내 PG·해외 MoR 선택 기준
- 토스페이먼츠 Next.js 연동: App Router 결제 승인·금액 검증·웹훅까지
- 정기결제 구현: 빌링키 발급부터 결제 실패 재시도·해지까지(토스·포트원)
- 결제 붙이기 전 법적 준비: 사업자등록·통신판매업 신고·환불 고지 체크리스트
- 코텍시스 서비스 구축 사업부
자주 묻는 질문
포트원만 가입하면 결제를 받을 수 있나요?
받을 수 없습니다. 포트원 가입과 테스트 채널로는 개발과 테스트만 할 수 있습니다. 실제 결제는 전자결제 신청을 거쳐 PG사와 계약하고, PG에서 받은 MID와 키로 실연동 채널을 추가해야 시작됩니다. 전자결제 신청에는 사업자등록증이 필요합니다.
V1과 V2는 무엇이 다른가요?
포트원 문서는 신규 프로젝트에 V2를 권장하고 V1은 레거시 유지보수 용도로 지원한다고 적고 있습니다. V1은 고객사 식별코드와 REST API Key·Secret을 쓰고, V2는 Store ID, 채널 키, 별도로 발급하는 V2 API Secret을 씁니다. V1 웹훅 본문은 imp_uid, merchant_uid, status이고 단건 조회 API로 검증합니다. V2 웹훅은 Standard Webhooks 서명을 붙이며 서버 SDK로 검증할 수 있습니다. 문서를 볼 때는 URL에 ?v=v2를 붙여 버전을 고정합니다.
수수료는 어디서 정해지나요?
결제 수수료는 계약한 PG사와의 계약에서 정해집니다. 포트원 개발자센터와 포트원 웹사이트에서 공개 요금표는 찾지 못했습니다(공식 문서에 기재 없음, 2026-10-17 확인). 포트원 이용 조건은 포트원 고객센터에, 결제 수수료는 신청한 PG사에 확인합니다.
출처
포트원 공식 문서, 모두 2026-10-17 접속.
- 결제 연동 준비하기, https://developers.portone.io/opi/ko/integration/ready/readme?v=v2
- 전자결제 신청, https://developers.portone.io/opi/ko/console/guide/reg
- 연동 정보(채널 관리), https://developers.portone.io/opi/ko/console/guide/channel-manage
- 인증 결제 연동하기, https://developers.portone.io/opi/ko/integration/start/v2/checkout?v=v2
- 웹훅 연동하기, https://developers.portone.io/opi/ko/integration/webhook/readme-v2?v=v2
- 웹훅 연동하기(V1), https://developers.portone.io/opi/ko/integration/webhook/readme-v1?v=v1
- 서버 SDK 레퍼런스, https://developers.portone.io/sdk/ko/v2-server-sdk/readme?v=v2
- server-sdk JavaScript README 및 webhook.ts, https://github.com/portone-io/server-sdk/tree/main/javascript
- AI 도구 활용하기, https://developers.portone.io/opi/ko/integration/using-ai-tools
- Next.js 16.3 문서, route.js 레퍼런스(Webhooks)
코텍시스 창업자로 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로 서빙해야 하는 기업에게 정확도 손실 없이 메모리와 비용을 동시에 줄이는 사실상의 표준 기법입니다.

