사이드 프로젝트에 결제를 붙이려는 사람이 가장 먼저 검색하는 질문은 하나다. “사업자 등록 없이도 되나?”
답부터 말하면 테스트는 되고, 실결제는 안 된다. 그런데 이걸 그냥 “안 된다”로 줄이면 절반은 틀린 말이 된다. 등록 없이 할 수 있는 일이 생각보다 훨씬 많다. 결제위젯을 붙이고, 카드 인증 화면을 띄우고, 승인 API까지 호출해서 응답을 받아보는 것 — 여기까지가 사업자등록증 없이 오늘 당장 되는 범위다.
결론부터: 이건 이분법이 아니라 3단계다
“된다/안 된다”로 나누면 계속 헷갈린다. 실제로는 관문이 세 개고, 각 관문에서 열리는 것이 다르다.
| 단계 | 필요한 것 | 되는 것 | 안 되는 것 |
|---|---|---|---|
| 1. 문서용 테스트 키 | 없음 | SDK 연동, 결제창 호출, 승인 API 응답 확인 | 가상계좌 테스트, 결제내역 조회, 웹훅 |
| 2. 개발자센터 회원가입 | 이메일 계정 | 체험 상점 테스트 키(일부), 테스트 결제내역, 웹훅 설정 | 결제위젯 전용 내 키, 실결제 (라이브 키) |
| 3. 전자결제 계약 | 사업자등록증 | 라이브 키, 실제 정산 | — |
토스페이먼츠 공식 문서는 1단계를 이렇게 안내한다.
회원가입이나 사업자 등록 없이도 토스페이먼츠의 온라인 결제를 테스트해볼 수 있다는 사실, 알고 계셨나요?
반대로 3단계를 두고는 이렇게 못 박는다.
주문서형, 결제창형 연동 키는 토스페이먼츠 전자결제 신청 이후에만 확인할 수 있어요. 신청 전이라면 개발 연동 체험 상점의 일부 테스트 키만 확인할 수 있어요.
즉 사업자 등록을 면제해주는 경로는 없다. PG 계약은 사업자 단위로만 체결된다. 토스페이먼츠가 비사업자에게 ’사업자등록 바로신청’을 따로 제공하는 것도 같은 이유다 — 등록을 건너뛰게 해주는 게 아니라, 등록을 빨리 끝내게 도와주는 서비스다.
그래서 이 글의 순서는 이렇다. 등록 없이 오늘 끝낼 수 있는 연동을 먼저 다 한다. 그다음 실결제가 필요해지는 시점에 무엇을 준비해야 하는지 보고, 그 등록이 실제로 요구하는 비용은 마지막에 계산한다.
1단계: 등록 없이 결제 승인까지 — 결제위젯 v2 연동 4단계
토스페이먼츠 결제위젯 v2는 네 단계로 끝난다. SDK 설치 → 금액 설정 → UI 렌더링 → 승인 API. 아래 코드는 토스페이먼츠 공식 샘플 저장소(tosspayments/tosspayments-sample)의 express-javascript 예제를 기준으로 했다.
SDK 설치
스크립트 태그 한 줄이면 된다.
<script src="https://js.tosspayments.com/v2/standard"></script>
번들러를 쓴다면 npm 패키지를 설치한다.
npm install @tosspayments/tosspayments-sdk
위젯 초기화와 금액 설정
clientKey에 들어간 값이 바로 문서용 테스트 키다. 가입 없이 공개된 키라서 복사해서 그대로 돌려도 동작한다.
const clientKey = "test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm";
const customerKey = generateRandomString();
const tossPayments = TossPayments(clientKey);
// 회원 결제
const widgets = tossPayments.widgets({ customerKey });
// 비회원 결제라면
// const widgets = tossPayments.widgets({ customerKey: TossPayments.ANONYMOUS });
await widgets.setAmount({ currency: "KRW", value: 50000 });
customerKey는 구매자 식별자인데, 공식 주석이 경고하듯 이메일이나 전화번호처럼 유추 가능한 값을 쓰면 안 된다. 브랜드페이로 확장할 때 이 키가 결제수단 소유자를 가리키므로, 남이 추측할 수 있는 값이면 그대로 취약점이 된다.
setAmount()는 렌더링과 결제 요청보다 반드시 먼저 호출해야 한다. 쿠폰이나 할인으로 금액이 바뀌면 그때마다 다시 호출한다.
UI 렌더링
결제수단 UI와 약관 UI를 각각 그린다.
await Promise.all([
widgets.renderPaymentMethods({
selector: "#payment-method",
variantKey: "DEFAULT",
}),
widgets.renderAgreement({
selector: "#agreement",
variantKey: "AGREEMENT",
}),
]);

위 코드가 실제로 그리는 화면이다. 토스페이먼츠 개발자센터 샌드박스(developers.tosspayments.com/sandbox)에서 가입 없이 바로 확인할 수 있고, 상단 배너가 이 글의 결론을 그대로 보여준다 — 화면은 완전히 동작하지만 실제로 결제되지는 않는다.
variantKey로 결제수단 구성이 다른 멀티 UI를 쓸 수 있는데, 이건 계약 이후 어드민에서 만드는 기능이다. 테스트 단계에서는 DEFAULT로 충분하다.
결제 요청
await widgets.requestPayment({
orderId: generateOrderId(),
orderName: "토스 티셔츠 외 2건",
successUrl: window.location.origin + "/success.html",
failUrl: window.location.origin + "/fail.html",
customerEmail: "customer123@gmail.com",
customerName: "김토스",
});
인증이 끝나면 토스가 successUrl로 리다이렉트하면서 paymentKey, orderId, amount 세 개를 쿼리 파라미터로 붙여준다. 여기까지는 아직 결제가 아니다. 인증만 끝난 상태고, 실제 승인은 다음 단계다.
승인은 서버에서
const widgetSecretKey = "test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6";
const encryptedSecretKey =
"Basic " + Buffer.from(widgetSecretKey + ":").toString("base64");
app.post("/confirm", function (req, res) {
const { paymentKey, orderId, amount } = req.body;
fetch("https://api.tosspayments.com/v1/payments/confirm", {
method: "POST",
headers: {
Authorization: encryptedSecretKey,
"Content-Type": "application/json",
},
body: JSON.stringify({ paymentKey, orderId, amount }),
});
});
시크릿 키 뒤의 콜론이 눈에 걸린다면 정확히 본 거다. 토스페이먼츠 API는 시크릿 키를 HTTP Basic 인증의 사용자 ID로 쓰고 비밀번호는 쓰지 않는다. 비밀번호가 없다는 걸 알리려고 콜론만 붙여서 인코딩한다.
이 승인 API가 200을 돌려주면 연동은 끝이다. 테스트 환경에서는 실제 카드번호를 넣어도 승인이 가상으로 처리되고 돈이 빠져나가지 않는다.
문서용 테스트 키로 안 되는 것
공짜 키인 만큼 경계가 있다. 가상계좌는 문서용 테스트 키로 테스트할 수 없다. 전자결제 신청 이후 발급받는 본인 테스트 키가 필요하다. 개발자센터의 테스트 결제내역 조회와 웹훅 설정도 마찬가지로 회원가입부터 해야 한다. 다만 회원가입 자체는 사업자와 무관하니, 여기까지는 등록 없이 올라갈 수 있는 2단계다.
실전 함정 1: 공식 샘플의 orderId는 1만 건에 한 번 터진다
이 글에서 가장 값어치 있는 부분이다.
토스페이먼츠 API의 orderId에는 제약이 있다. 6자 이상 64자 이하, 영문 대소문자·숫자·-·_만 허용. 그런데 공식 샘플의 주문번호 생성기는 이렇게 생겼다.
function generateRandomString() {
return window.btoa(Math.random()).slice(0, 20);
}
Base64 출력 문자에는 +, /, =가 포함된다. 전부 orderId 허용 문자가 아니다. 실제로 이 코드를 그대로 쓴 개발자들이 같은 에러를 만난다.
orderId는 영문 대소문자, 숫자, 특수문자(-, _) 만 허용합니다
여기서 대충 “Base64는 위험하다” 정도로 넘어가면 진짜 문제를 놓친다. 이 버그가 얼마나 자주 터지는지가 핵심이다. 그래서 직접 재봤다. Node v22.17.0에서 위 생성기를 200만 번 돌린 결과다.
N = 2,000,000
= 포함: 171회 (0.0086%) → 약 1/11,700
+ 포함: 0회
/ 포함: 0회
+와 /는 한 번도 나오지 않았다. 입력이 Math.random() 문자열이라 숫자와 점뿐이고, 해당 비트 패턴이 생기지 않는다. 문제는 = 하나이고, 원인은 길이다. Math.random()이 평소보다 짧은 문자열(끝자리 0이 잘린 경우)을 반환하면 Base64 결과가 20자 이하로 짧아지고, 그러면 끝의 패딩 =가 slice(0, 20) 안으로 들어온다.
약 1만 1천 건에 한 번. 이게 이 버그의 진짜 얼굴이다. 개발 중에 몇십 번 결제해서는 절대 안 잡힌다. QA에서도 안 잡힌다. 서비스가 자리를 잡고 주문이 만 건쯤 쌓였을 때, 어느 실제 고객의 실제 결제가 이유 없이 실패하면서 처음 드러난다.
해결은 단순하다. 주문번호는 클라이언트에서 만들지 말고 서버에서 만든다.
import { randomUUID } from "crypto";
const orderId = `order_${randomUUID()}`;
UUID v4는 16진수와 하이픈만 쓰므로 허용 문자 안에 들어오고, 길이도 42자라 6~64자 범위를 만족한다. 서버에서 만들어야 하는 더 중요한 이유는 따로 있다. 결제를 요청하기 전에 orderId와 금액을 서버 DB에 먼저 저장해둬야 다음 함정을 막을 수 있다.
실전 함정 2: 금액은 반드시 서버에서 대조한다
승인 API를 프론트엔드에서 직접 호출하면 안 되는 이유는 시크릿 키 노출만이 아니다. 더 직접적인 위험은 금액 조작이다.
successUrl로 돌아올 때 amount는 쿼리 파라미터로 온다. 브라우저 주소창에 그대로 노출되는 값이다. 50,000원짜리 주문을 100원으로 바꿔서 승인 요청을 보내는 건 개발자도구를 열 줄 아는 사람이면 누구나 할 수 있다.
그래서 승인 직전에 서버가 대조해야 한다.
const order = await db.orders.findByOrderId(orderId);
if (order.totalPrice !== Number(amount)) {
throw new Error("PAYMENT_AMOUNT_MISMATCH");
}
// 여기를 통과한 뒤에야 승인 API를 호출한다
이 검사가 성립하려면 결제 요청 전에 orderId와 금액이 이미 DB에 있어야 한다. 앞 절에서 주문번호를 서버에서 만들라고 한 이유가 이것이다. 두 함정은 사실 같은 문제의 앞뒤다.
같은 맥락에서 중복 승인 방어도 서버 몫이다. 같은 orderId로 이미 승인된 결제가 있는지는 애초에 클라이언트가 알 수 없다.
백엔드를 어디에 둘지 고민 중이라면 1인 개발자 Vercel + Supabase 최소 인프라 구성에 정리해둔 조합이 이 정도 요구사항에는 충분하다.
실전 함정 3: PCI-DSS는 이미 해결돼 있다
결제를 처음 붙이는 개발자가 과하게 걱정하는 지점이 카드정보 보안 인증이다. 결론은 신경 쓸 필요 없다.
결제위젯 방식에서는 카드번호가 토스페이먼츠 화면에서 입력되고, 우리 서버를 거치지 않는다. 우리가 받는 건 paymentKey 같은 참조값뿐이다. PCI-DSS 인증 의무는 카드 데이터를 저장·처리·전송하는 쪽에 붙는데, 그 경로에서 우리는 애초에 빠져 있다.
다만 이건 카드정보를 직접 받지 않을 때만 성립한다. 자체 폼에 카드번호를 입력받아 서버로 보내는 순간 이야기가 완전히 달라진다. 하지 말아야 할 설계다.
실결제로 넘어갈 때: 계약 체크리스트
여기서부터는 사업자등록증이 필요하다.
필요 서류. 개인사업자는 사업자등록증이 기본이고, 여기에 대표자 본인인증이 붙는다. 서류는 휴대폰으로 찍어 제출해도 되지만 페이지 전체가 잘리지 않게 담아야 한다. 통신판매업 신고증은 거래 규모에 따라 추가로 요구된다.
심사 기간. 신청 후 2~3영업일 안에 계약 담당자 연락이 오고, 카드사 심사까지 모두 끝나는 데는 대체로 2주 정도 잡아야 한다. 출시일이 정해져 있다면 개발이 끝난 뒤에 신청해서는 늦는다. 개발과 병행해서 넣어야 한다.
수수료. 토스페이먼츠 공시 기준(2026년 9월 기준)이다.
| 결제수단 | 수수료 |
|---|---|
| 신용·체크카드 | 3.4% |
| 간편결제 | 3.4% |
| 계좌이체 | 2.0% (최저 건당 200원) |
| 가상계좌 | 건당 400원 |
| 휴대폰 (실물) | 3.5% |
| 휴대폰 (디지털) | 7.0% |
| 휴대폰 (앱) | 8.0% |
| 상품권 | 9.0% |
모두 부가세 별도다. 카드 수수료는 영세·중소 가맹점 우대가 적용되면 0.63%(영세)에서 1.74%(중소3)까지 내려간다. 다만 신규 사업자는 가입 시점에 일단 ‘일반’ 등급으로 시작하고, 국세청이 상·하반기 명단을 넘겨주면 등급이 자동 반영되면서 차액이 환급된다. 첫 몇 달 정산액이 예상보다 적어도 계산이 틀린 게 아니다.
정산 주기. 공시 기준 “평균 5일 이내”다. 결제일에 바로 돈이 들어오지는 않으니, 원가가 즉시 나가는 구조라면 이 시차만큼 운전자금이 필요하다.
등록의 진짜 비용: 수수료가 아니라 건강보험이다
“그럼 사업자등록 하면 되지”에서 대부분 멈추는데, 여기가 실제 의사결정 지점이다. 등록 자체는 무료고 온라인으로 신청하면 며칠 안에 끝난다. 문제는 그 뒤에 딸려오는 것들이다.
본인이 지금 건강보험 피부양자라면 — 여기가 제일 크다. 학생, 취업 준비 중, 전업으로 일하지 않는 상태에서 가족 밑에 피부양자로 등록돼 있다면 기준이 이렇다.
- 사업자등록을 한 경우: 필요경비를 뺀 사업소득이 1원이라도 있으면 피부양자 자격 상실
- 사업자등록을 안 한 경우: 연 사업소득 500만원 이하까지는 피부양자 유지
월 3만원짜리 서비스에 구독자 하나만 붙어도 지역가입자로 전환되고 보험료가 새로 부과된다. 지역가입자 보험료는 소득뿐 아니라 재산과 자동차까지 점수로 환산해 매기기 때문에, 소득이 적어도 금액이 작다는 보장이 없다.
직장에 다니면서 사이드로 하는 경우라면 이 조항은 해당되지 않는다. 이미 직장가입자이므로 사업자등록을 해도 자격이 바뀌지 않는다. 보수 외 소득이 일정 기준(연 2,000만원)을 넘을 때 초과분에만 소득월액보험료가 추가로 붙는다. 사이드 프로젝트 초기 매출로는 닿기 어려운 선이다.
즉 같은 “사업자등록 하기”가 두 사람에게 완전히 다른 비용이다. 본인이 어느 쪽인지부터 확인해야 한다.
세금. 등록하면 매년 5월 종합소득세 신고 의무가 생긴다. 매출이 0원이어도 신고는 해야 한다. 부가가치세 신고도 별도로 붙는데, 간이과세자는 연 1회, 일반과세자는 연 2회다.
정확한 기준은 개인 상황에 따라 갈리므로, 금액이 커질 것 같으면 세무 상담 한 번이 가장 싼 투자다. 사업자 등록과 관련해 실제로 서류를 떼어본 경험은 개인사업자 D-U-N-S 번호 발급 전 과정에 정리해뒀다.
등록 전까지 쓸 수 있는 대안
당장 등록할 생각이 없다면 선택지는 두 갈래다.
앱스토어·플레이스토어 인앱결제. 개인 자격으로도 개발자 계정을 만들 수 있고, 결제·정산·세금 처리를 플랫폼이 판매자로서 떠안는다. 국내 PG 계약이 필요 없다는 게 가장 큰 장점이다. 대가는 수수료다. 15~30%로 PG의 3.4%와는 자릿수가 다르다. 대신 앱 심사라는 관문이 새로 생긴다 — 심사 리젝 사유는 앱스토어 심사 리젝 사유별 체크리스트에 정리해뒀다. 참고로 인앱결제 의무는 디지털 재화에 적용되고, 실물 상품이나 오프라인 서비스는 외부 결제를 쓸 수 있다.
후원 플랫폼. 개인 계정으로 받을 수 있지만 ’결제’가 아니라 후원이라 구독·환불·영수증 같은 상거래 기능이 없다. 수익 검증용으로는 쓸 만해도 제품의 결제 수단으로는 한계가 뚜렷하다.
정리하면 이렇다. 제품이 팔릴지 아직 모르는 단계면 등록하지 말고 테스트 키로 연동만 끝내 둔다. 결제 흐름은 어차피 실결제로 바뀔 때 키 두 개만 교체하면 되는 구조다. 첫 결제가 실제로 일어날 근거가 생겼을 때 등록하면 된다 — 카드사 심사에 2주가 걸린다는 것만 일정에 넣어두고.
정리
- 사업자 등록 없이 결제위젯 연동부터 승인 API 응답 확인까지 끝낼 수 있다. 문서용 테스트 키가 공개돼 있다.
- 실결제는 예외 없이 사업자 등록이 필요하다. 우회 경로는 없고, 개인사업자 기준 서류는 사업자등록증 + 대표자 본인인증, 카드사 심사까지 약 2주다.
- 공식 샘플의
btoa기반 주문번호는 약 1만 1천 건에 한 번 허용 문자 위반으로 실패한다. 주문번호는 서버에서 UUID로 만든다. - 승인과 금액 대조는 반드시 서버에서 한다. 쿼리 파라미터로 오는
amount는 신뢰할 수 없는 값이다. - 등록의 진짜 비용은 수수료가 아니라 건강보험이다. 피부양자라면 사업소득 1원부터 자격을 잃고, 직장가입자라면 해당되지 않는다.
수수료율·정산 주기·계약 요건은 개정된다. 이 글의 수치는 2026년 9월 기준이고, 실제로 계약을 진행하기 전에 공식 문서에서 한 번 더 확인하는 편이 좋다.
