# 테스트는 됩니다, 실결제는 안 됩니다 — 사업자 등록 없이 토스페이먼츠 어디까지 연동되나 사업자 등록 없이 토스페이먼츠 테스트 연동은 되고 실결제는 안 됩니다. 결제위젯 v2 코드와 계약 조건. - source: https://polroute.com/posts/toss-payments-side-project/ - category: 아키텍처 - published: 2026-09-18 --- 사이드 프로젝트에 결제를 붙이려는 사람이 가장 먼저 검색하는 질문은 하나다. "사업자 등록 없이도 되나?" 답부터 말하면 **테스트는 되고, 실결제는 안 된다.** 그런데 이걸 그냥 "안 된다"로 줄이면 절반은 틀린 말이 된다. 등록 없이 할 수 있는 일이 생각보다 훨씬 많다. 결제위젯을 붙이고, 카드 인증 화면을 띄우고, 승인 API까지 호출해서 응답을 받아보는 것 — 여기까지가 사업자등록증 없이 오늘 당장 되는 범위다. ## 결론부터: 이건 이분법이 아니라 3단계다 "된다/안 된다"로 나누면 계속 헷갈린다. 실제로는 관문이 세 개고, 각 관문에서 열리는 것이 다르다. | 단계 | 필요한 것 | 되는 것 | 안 되는 것 | |---|---|---|---| | **1. 문서용 테스트 키** | 없음 | SDK 연동, 결제창 호출, 승인 API 응답 확인 | 가상계좌 테스트, 결제내역 조회, 웹훅 | | **2. 개발자센터 회원가입** | 이메일 계정 | 체험 상점 테스트 키(일부), 테스트 결제내역, 웹훅 설정 | 결제위젯 전용 내 키, 실결제 (라이브 키) | | **3. 전자결제 계약** | **사업자등록증** | 라이브 키, 실제 정산 | — | 토스페이먼츠 공식 문서는 1단계를 이렇게 안내한다. > 회원가입이나 사업자 등록 없이도 토스페이먼츠의 온라인 결제를 테스트해볼 수 있다는 사실, 알고 계셨나요? 반대로 3단계를 두고는 이렇게 못 박는다. > 주문서형, 결제창형 연동 키는 토스페이먼츠 전자결제 신청 이후에만 확인할 수 있어요. 신청 전이라면 개발 연동 체험 상점의 일부 테스트 키만 확인할 수 있어요. 즉 사업자 등록을 면제해주는 경로는 없다. PG 계약은 사업자 단위로만 체결된다. 토스페이먼츠가 비사업자에게 '사업자등록 바로신청'을 따로 제공하는 것도 같은 이유다 — 등록을 건너뛰게 해주는 게 아니라, 등록을 빨리 끝내게 도와주는 서비스다. 그래서 이 글의 순서는 이렇다. 등록 없이 오늘 끝낼 수 있는 연동을 먼저 다 한다. 그다음 실결제가 필요해지는 시점에 무엇을 준비해야 하는지 보고, 그 등록이 실제로 요구하는 비용은 마지막에 계산한다. ## 1단계: 등록 없이 결제 승인까지 — 결제위젯 v2 연동 4단계 토스페이먼츠 결제위젯 v2는 네 단계로 끝난다. SDK 설치 → 금액 설정 → UI 렌더링 → 승인 API. 아래 코드는 토스페이먼츠 공식 샘플 저장소(`tosspayments/tosspayments-sample`)의 `express-javascript` 예제를 기준으로 했다. ### SDK 설치 스크립트 태그 한 줄이면 된다. ```html ``` 번들러를 쓴다면 npm 패키지를 설치한다. ```bash npm install @tosspayments/tosspayments-sdk ``` ### 위젯 초기화와 금액 설정 `clientKey`에 들어간 값이 바로 **문서용 테스트 키**다. 가입 없이 공개된 키라서 복사해서 그대로 돌려도 동작한다. ```javascript 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를 각각 그린다. ```javascript await Promise.all([ widgets.renderPaymentMethods({ selector: "#payment-method", variantKey: "DEFAULT", }), widgets.renderAgreement({ selector: "#agreement", variantKey: "AGREEMENT", }), ]); ``` ![토스페이먼츠 개발자센터 샌드박스에서 렌더링된 결제위젯. 상단에 "테스트 환경이에요. 실제로 결제되지 않아요." 배너가 떠 있고 결제수단 타일과 결제하기 버튼이 보인다](/images/toss-payments-side-project-1.webp) 위 코드가 실제로 그리는 화면이다. 토스페이먼츠 개발자센터 샌드박스(`developers.tosspayments.com/sandbox`)에서 가입 없이 바로 확인할 수 있고, 상단 배너가 이 글의 결론을 그대로 보여준다 — 화면은 완전히 동작하지만 실제로 결제되지는 않는다. `variantKey`로 결제수단 구성이 다른 멀티 UI를 쓸 수 있는데, 이건 계약 이후 어드민에서 만드는 기능이다. 테스트 단계에서는 `DEFAULT`로 충분하다. ### 결제 요청 ```javascript 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` 세 개를 쿼리 파라미터로 붙여준다. **여기까지는 아직 결제가 아니다.** 인증만 끝난 상태고, 실제 승인은 다음 단계다. ### 승인은 서버에서 ```javascript 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자 이하, 영문 대소문자·숫자·`-`·`_`만 허용.** 그런데 공식 샘플의 주문번호 생성기는 이렇게 생겼다. ```javascript 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에서도 안 잡힌다. 서비스가 자리를 잡고 주문이 만 건쯤 쌓였을 때, 어느 실제 고객의 실제 결제가 이유 없이 실패하면서 처음 드러난다. 해결은 단순하다. **주문번호는 클라이언트에서 만들지 말고 서버에서 만든다.** ```javascript import { randomUUID } from "crypto"; const orderId = `order_${randomUUID()}`; ``` UUID v4는 16진수와 하이픈만 쓰므로 허용 문자 안에 들어오고, 길이도 42자라 6~64자 범위를 만족한다. 서버에서 만들어야 하는 더 중요한 이유는 따로 있다. 결제를 요청하기 **전에** `orderId`와 금액을 서버 DB에 먼저 저장해둬야 다음 함정을 막을 수 있다. ## 실전 함정 2: 금액은 반드시 서버에서 대조한다 승인 API를 프론트엔드에서 직접 호출하면 안 되는 이유는 시크릿 키 노출만이 아니다. 더 직접적인 위험은 **금액 조작**이다. `successUrl`로 돌아올 때 `amount`는 쿼리 파라미터로 온다. 브라우저 주소창에 그대로 노출되는 값이다. 50,000원짜리 주문을 100원으로 바꿔서 승인 요청을 보내는 건 개발자도구를 열 줄 아는 사람이면 누구나 할 수 있다. 그래서 승인 직전에 서버가 대조해야 한다. ```javascript 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 최소 인프라 구성](/posts/minimal-infra-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 번호 발급 전 과정](/posts/duns-number-registration/)에 정리해뒀다. ## 등록 전까지 쓸 수 있는 대안 당장 등록할 생각이 없다면 선택지는 두 갈래다. **앱스토어·플레이스토어 인앱결제.** 개인 자격으로도 개발자 계정을 만들 수 있고, 결제·정산·세금 처리를 플랫폼이 판매자로서 떠안는다. 국내 PG 계약이 필요 없다는 게 가장 큰 장점이다. 대가는 수수료다. 15~30%로 PG의 3.4%와는 자릿수가 다르다. 대신 앱 심사라는 관문이 새로 생긴다 — 심사 리젝 사유는 [앱스토어 심사 리젝 사유별 체크리스트](/posts/app-store-rejection-checklist/)에 정리해뒀다. 참고로 인앱결제 의무는 디지털 재화에 적용되고, 실물 상품이나 오프라인 서비스는 외부 결제를 쓸 수 있다. **후원 플랫폼.** 개인 계정으로 받을 수 있지만 '결제'가 아니라 후원이라 구독·환불·영수증 같은 상거래 기능이 없다. 수익 검증용으로는 쓸 만해도 제품의 결제 수단으로는 한계가 뚜렷하다. 정리하면 이렇다. **제품이 팔릴지 아직 모르는 단계면 등록하지 말고 테스트 키로 연동만 끝내 둔다.** 결제 흐름은 어차피 실결제로 바뀔 때 키 두 개만 교체하면 되는 구조다. 첫 결제가 실제로 일어날 근거가 생겼을 때 등록하면 된다 — 카드사 심사에 2주가 걸린다는 것만 일정에 넣어두고. ## 정리 - 사업자 등록 없이 **결제위젯 연동부터 승인 API 응답 확인까지** 끝낼 수 있다. 문서용 테스트 키가 공개돼 있다. - 실결제는 예외 없이 사업자 등록이 필요하다. 우회 경로는 없고, 개인사업자 기준 서류는 사업자등록증 + 대표자 본인인증, 카드사 심사까지 약 2주다. - 공식 샘플의 `btoa` 기반 주문번호는 약 **1만 1천 건에 한 번** 허용 문자 위반으로 실패한다. 주문번호는 서버에서 UUID로 만든다. - 승인과 금액 대조는 반드시 서버에서 한다. 쿼리 파라미터로 오는 `amount`는 신뢰할 수 없는 값이다. - 등록의 진짜 비용은 수수료가 아니라 건강보험이다. 피부양자라면 사업소득 1원부터 자격을 잃고, 직장가입자라면 해당되지 않는다. 수수료율·정산 주기·계약 요건은 개정된다. 이 글의 수치는 2026년 9월 기준이고, 실제로 계약을 진행하기 전에 공식 문서에서 한 번 더 확인하는 편이 좋다.