# 토스페이먼츠 연동을 문서대로 따라가봤습니다: 회원가입 없이 승인 API 까지 갔습니다

> 토스페이먼츠 결제위젯 문서의 예제를 한 글자도 고치지 않고 올려서 결제까지 통과시키고 승인 API 를 호출했습니다. 회원가입도 계약도 없이 문서에 적힌 테스트 키만으로 승인 200 까지 갔고, 막히는 지점과 헷갈리는 에러 메시지를 그대로 적었습니다.

- 출처: https://dec27.dev/ko/toss-payments-integration-teardown/
- 저자: dec27
- 매체: dec27
- 발행일: 2026-08-21
- 최종 수정(UTC): 2026-08-25
- 카테고리: 웹·앱 개발
- 키워드: 토스페이먼츠 연동, 결제 연동 개발, PG 연동 절차, 결제위젯, 결제 승인 API

---

## 핵심 요약

- **토스페이먼츠 연동 문서의 예제 코드를 한 글자도 고치지 않고 올렸더니 결제위젯이 떴습니다.** 회원가입도 계약도 하지 않았고, 문서 코드 블록에 그대로 적혀 있는 테스트 키만 썼습니다. (확인일 2026년 8월 21일)
- **결제를 끝까지 통과시키고 승인 API 까지 성공했습니다.** 승인 응답은 HTTP 200 에 `status=DONE`, 필드가 37개였습니다.
- **금액을 1원만 틀리게 보내면 403으로 막힙니다.** 금액 위변조 방어가 실제로 동작합니다. 다만 응답은 "허용되지 않은 요청" 이라고만 하고 금액이 안 맞는다고 말해 주지 않습니다.
- **처음 붙일 때 헛다리를 짚기 쉬운 에러가 하나 있습니다.** 존재하지 않는 paymentKey 를 보내면 "결제 시간이 만료되어 결제 진행 데이터가 존재하지 않습니다" 가 옵니다. 키를 잘못 만든 것과 시간이 지난 것이 같은 응답으로 옵니다.
- **비밀번호를 두 번 입력합니다.** 계좌 확인용 한 번, 결제 승인용 한 번입니다. 보안 키패드의 숫자 배열은 두 화면에서 다르고, 다시 밟을 때마다 새로 섞입니다.
- 이 글은 테스트 환경 기준입니다. 결제창에 "테스트 환경이에요. 실제로 결제되지 않아요" 가 떠 있습니다.

## 문제 정의: 붙이기 전에 어디까지 해볼 수 있는가

결제를 붙이는 일은 대개 계약부터 시작합니다. 전자결제를 신청하고, 심사를 받고, 상점이 열리고, 그다음에 키를 받습니다. 그런데 개발 일정을 잡으려면 그 전에 알아야 하는 것이 있습니다. 코드가 얼마나 붙는지, 서버가 무엇을 해야 하는지, 어디서 막히는지입니다.

토스페이먼츠 문서는 이 순서를 뒤집을 수 있게 해 둡니다. API 키 문서에 이렇게 적혀 있습니다. "주문서형, 결제창형 연동 키는 토스페이먼츠 전자결제 신청 이후에만 확인할 수 있어요. 신청 전에는 문서에 있는 테스트 키로 결제를 연동해보세요."

그래서 그 말대로 해봤습니다. **회원가입도 하지 않고, 계약도 하지 않고, 문서에 박힌 키만으로 어디까지 가는지** 확인했습니다.

## 어떻게 따라갔는지 먼저 적습니다

- 결제위젯 연동 문서의 전체 예제 코드를 **그대로 복사**해 파일 하나로 저장했습니다. 2,795바이트입니다.
- 로컬 웹서버에 올려 브라우저로 열었습니다. **코드를 한 글자도 고치지 않았습니다.**
- 결제하기를 눌러 결제를 끝까지 통과시켰습니다.
- 돌아온 주소의 값으로 승인 API 를 실제로 호출했습니다.
- 승인 API 에 일부러 잘못된 요청을 넣어 어떤 응답이 오는지 확인했습니다.
쓴 키는 문서 코드 블록에 그대로 적혀 있는 공개 테스트 키 두 개입니다. 클라이언트 키가 `test_gck_docs_` 로 시작하고 시크릿 키가 `test_gsk_docs_` 로 시작합니다.

이 글이 답하지 못하는 것도 먼저 적습니다.

- **테스트 환경 기준입니다.** 실제 상점 키의 동작이 같다고 단정할 수 없습니다.
- 결제수단은 계좌이체 하나만 끝까지 갔습니다. 카드는 카드사 선택 화면까지만 갔습니다.
- 웹훅, 취소 API, 정산은 확인하지 않았습니다.
- 확인일은 2026년 8월 21일 하루입니다. 문서와 SDK 는 바뀝니다.

## 1단계: 예제를 그대로 올리면 위젯이 뜹니다

문서의 예제는 HTML 파일 한 장입니다. 스크립트 태그로 SDK 를 불러오고, 결제 UI 와 약관 UI 를 넣을 빈 요소 두 개를 두고, 스크립트에서 위젯을 렌더합니다.

이 파일을 그대로 올려 열었더니 위젯이 렌더됐습니다. **콘솔 오류는 없었습니다.**

눈에 띈 것은 시간이었습니다.

| 시점 | 걸린 시간 |
| --- | --- |
| 결제 UI 자리에 요소가 붙음 | 800ms |
| 위젯이 실제 높이를 확보하고 결제수단이 보임 | 2,408ms |

출처: 저희가 직접 측정한 값입니다. 확인일 2026년 8월 21일. 같은 기계에서 두 번 재었고 첫 실행에서는 요소가 붙는 데 1,611ms 였습니다. 회선과 기계에 따라 달라집니다.

**이 두 시점이 다르다는 것이 실무에서 걸립니다.** 그 사이 화면에는 회색 뼈대만 보입니다. 로딩 표시를 직접 만들 생각이라면 요소가 붙는 시점이 아니라 높이가 잡히는 시점을 기준으로 잡으셔야 합니다.

## 2단계: 결제창은 새 창이 아니라 같은 페이지에서 뜹니다

결제하기를 눌렀더니 결제창이 **같은 페이지 안의 프레임**으로 떴습니다. 새 창은 열리지 않았습니다. 팝업 차단을 걱정할 필요가 없다는 뜻입니다. 함께 붙는 프레임 중에는 reCAPTCHA 도 있었습니다.

기본으로 선택된 결제수단은 퀵계좌이체였습니다. 그 자리에 "3% 즉시 할인" 배지가 붙어 있었고, 결제수단 목록 아래에는 "신한카드 최대 3개월 무이자 할부" 안내가 따로 있었습니다. 혜택 문구가 결제수단마다 하나씩 붙는 구조는 아니었습니다.

테스트 값은 화면이 알려줍니다. 휴대폰번호 칸에는 `010-0000-0000` 이 이미 입력된 채로 뜨고 "테스트용 휴대폰번호가 입력되어 있어요" 가 적혀 있습니다. 비밀번호 화면에는 "테스트용 비밀번호는 '000000' 이에요" 가 적혀 있습니다. **문서를 다시 뒤져 테스트 값을 찾을 필요가 없습니다.**

## 3단계: 비밀번호를 두 번 입력합니다

여기가 처음 밟을 때 예상과 달랐던 지점입니다. 비밀번호를 한 번이 아니라 **두 번** 입력합니다.

1. "계좌 확인을 위해 비밀번호를 입력해주세요"
2. "결제를 위해 비밀번호를 입력해주세요"
그리고 **보안 키패드의 숫자 배열이 두 화면에서 다릅니다.** 한 번의 결제 안에서만 다른 것이 아니라, 결제를 다시 밟을 때마다 배열이 새로 섞였습니다. 숫자가 놓이는 자리를 외워 두는 것이 의미가 없다는 뜻입니다. 테스트 환경인데도 실제와 같은 방식으로 동작합니다.

결제가 끝나자 문서에 적어 둔 successUrl 로 돌아왔고, 주소에 값 네 개가 붙어 있었습니다.

| 값 | 내용 |
| --- | --- |
| paymentType | NORMAL |
| orderId | 예제에 적혀 있던 주문번호 그대로 |
| paymentKey | `tgen_` 으로 시작하는 문자열 |
| amount | 50000 |

출처: 저희가 실제로 결제를 통과시키고 돌아온 주소입니다. 확인일 2026년 8월 21일.

결제하기를 누른 시점부터 돌아오기까지는 두 번 밟아 26초와 58초가 나왔습니다. **다만 이 값은 화면마다 넉넉한 대기를 넣고 밟은 것이라 사람이 손으로 하면 더 빠릅니다.** 속도 지표로 쓰실 값이 아닙니다.

![차단봉이 달린 게이트 네 개가 한 줄로 늘어선 일러스트. 앞의 세 개는 차단봉이 올라가 열려 있고 맨 끝 하나만 내려와 막혀 있습니다. 계약 전에도 앞 단계는 통과되고 마지막 문턱만 남는다는 것을 나타냅니다.](/media/articles/toss-payments-integration-teardown/four-gates.png)

## 4단계: 승인 API 를 부르지 않으면 결제가 끝난 것이 아닙니다

돌아온 값으로 승인 API 를 호출했습니다. 문서대로 paymentKey, orderId, amount 세 개를 보내고 시크릿 키를 인증 헤더에 넣었습니다.

한 번에 성공했습니다.

| 항목 | 값 |
| --- | --- |
| 응답 | HTTP 200, 914ms 와 1,040ms |
| status | DONE |
| method | 계좌이체 |
| totalAmount | 50000 |
| 응답 필드 수 | 37개 |

출처: 저희가 직접 호출한 결과입니다. 확인일 2026년 8월 21일. 응답 시간은 결제를 두 번 통과시켜 각각 잰 값입니다.

승인 시각이 결제창을 통과한 시각보다 뒤였습니다. **결제창에서 비밀번호를 다 눌렀다고 결제가 끝난 것이 아닙니다.** 서버가 승인 API 를 불러야 끝납니다. 문서가 4단계로 나눠 둔 이유가 이것입니다.

## 일부러 틀리게 보내봤습니다

붙이는 사람이 실제로 만나는 것은 성공 응답이 아니라 에러입니다. 그래서 일부러 잘못된 요청을 넣었습니다.

| 보낸 것 | 응답 | 코드와 메시지 |
| --- | --- | --- |
| 인증 헤더 없음 | 401 | `UNAUTHORIZED_KEY` / 인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다. |
| 잘못된 시크릿 키 | 401 | `UNAUTHORIZED_KEY` / 같은 메시지 |
| 존재하지 않는 paymentKey | 404 | `NOT_FOUND_PAYMENT_SESSION` / 결제 시간이 만료되어 결제 진행 데이터가 존재하지 않습니다. |
| 필수 필드 누락 | 400 | `INVALID_REQUEST` / 필수 파라미터가 누락되었습니다. |
| 금액을 1원 틀리게 | 403 | `FORBIDDEN_REQUEST` / 허용되지 않은 요청입니다. |
| 성공한 요청을 한 번 더 | 400 | `ALREADY_PROCESSED_PAYMENT` / 이미 처리된 결제 입니다. |

출처: 저희가 직접 호출한 결과입니다. 확인일 2026년 8월 21일. 같은 요청을 두 번 재어 응답 시간은 231ms 에서 511ms 사이였습니다.

여기서 세 가지가 실무에 직접 걸립니다.

**첫째, 금액 위변조 방어가 실제로 막습니다.** 같은 paymentKey 와 orderId 에 금액만 49,999원으로 보냈더니 403 이 왔습니다. 클라이언트에서 넘어온 금액을 그대로 믿지 말고 주문서에 저장한 금액으로 승인을 부르라는 문서 안내가 빈말이 아닙니다. **다만 응답은 금액이 안 맞는다고 말해 주지 않습니다.** "허용되지 않은 요청" 뿐입니다.

**둘째, 존재하지 않는 키의 메시지가 오해를 부릅니다.** 아무렇게나 만든 paymentKey 를 보냈더니 "결제 시간이 만료되어 결제 진행 데이터가 존재하지 않습니다" 가 왔습니다. 키를 잘못 만든 경우와 시간이 지난 경우가 같은 응답으로 옵니다. 처음 붙일 때 이 메시지를 보고 만료 쪽을 파면 시간을 버립니다. **키가 제대로 넘어왔는지부터 보시는 편이 빠릅니다.**

**셋째, 필수 필드 누락은 어느 필드인지 알려주지 않습니다.** amount 만 뺀 경우와 paymentKey 까지 함께 뺀 경우의 응답이 같았습니다. 요청 본문을 찍어 보는 수밖에 없습니다.

이 셋을 결제 API 의 결함으로 읽으실 필요는 없습니다. 결제는 내부 상태를 덜 드러내는 편이 안전한 영역입니다. 다만 **처음 붙이는 사람이 어디서 헤매게 되는지**는 알고 시작하시는 편이 낫습니다.

![열쇠 두 개가 각각 열쇠구멍 판 옆에 걸린 일러스트. 왼쪽 열쇠는 구멍에 맞아 들어가고 오른쪽 열쇠는 휘어 있어 같은 구멍에 들어가지 않습니다. 같은 실패로 보여도 원인이 키가 틀린 것과 시간이 지난 것으로 갈린다는 것을 나타냅니다.](/media/articles/toss-payments-integration-teardown/two-keys.png)

## 카드로 결제하려면 카드사를 먼저 골라야 합니다

결제수단에서 신용·체크카드를 골랐더니 카드사 목록이 펼쳐졌습니다. 카드사를 고르지 않은 채로 결제하기를 눌렀더니 결제창이 열리지 않고 그 자리에 "카드사를 선택해주세요" 가 떴습니다.

목록에는 국민, 카카오뱅크, 하나, 토스뱅크, 산업, 비씨, 우리, 전북, 씨티, 우체국, 새마을이 있었고 더보기가 붙어 있었습니다.

**저희는 카드 결제를 끝까지 가지 않았습니다.** 계좌이체만 완주했습니다. 카드 흐름의 뒷단은 이 글에서 말하지 않겠습니다.

## 실무 관점: 계약 전에 할 수 있는 것과 없는 것

문서 키로 확인한 결과를 정리하면 경계가 뚜렷합니다.

| 계약 전에 되는 것 | 계약과 계정이 필요한 것 |
| --- | --- |
| 위젯 렌더와 화면 배치 | 실제 상점 키 |
| 결제창 흐름 확인 | 어드민의 결제 UI 설정 |
| successUrl, failUrl 처리 | 정산 |
| 승인 API 호출과 에러 처리 | 실제 결제 |

붙이는 순서로 옮기면 이렇습니다.

1. **문서 예제를 그대로 올려 위젯부터 띄우십시오.** 코드를 고치는 것은 그다음입니다. 예제가 한 블록에 다 들어 있어서 화면을 오갈 일이 없습니다.
2. **승인 API 를 성공시키는 것까지 먼저 해보십시오.** 결제창을 통과하는 것과 결제가 끝나는 것은 다릅니다. 서버 쪽 작업이 남아 있다는 것을 일정에 넣어야 합니다.
3. **에러 경로를 먼저 짜십시오.** 금액 불일치 403, 이미 처리됨 400, 인증 실패 401 세 가지는 코드로 갈라 두는 편이 낫습니다. 특히 이미 처리됨은 네트워크 오류로 재시도했을 때 앞의 요청이 성공했다는 신호입니다.
4. **금액은 서버에 저장한 값으로 승인을 부르십시오.** 1원만 달라도 막히는 것을 확인했습니다.

## 확인하지 못한 것

- **실제 상점 키의 동작**을 확인하지 않았습니다. 회원가입과 계약이 필요합니다. 이 글의 값은 전부 문서 테스트 키 기준입니다.
- **카드 결제 완주**를 하지 않았습니다. 카드사 선택 화면까지만 갔습니다.
- **웹훅과 취소 API** 를 확인하지 않았습니다.
- **정산 흐름**을 확인하지 않았습니다. 승인 응답에 정산 상태가 미완료로 담겨 있다는 것만 봤습니다.
- **서버 없이 파일을 바로 열었을 때** 어떻게 되는지 확인하지 않았습니다. 처음부터 로컬 서버에 올렸습니다.

## 자주 묻는 질문

### 회원가입 없이 정말 되나요

저희가 확인한 범위에서는 위젯 렌더부터 승인 API 성공까지 됐습니다. 문서 코드 블록에 테스트 키가 그대로 적혀 있습니다. 다만 실제 상점 키와 정산은 계약이 필요합니다.

### 결제창을 통과하면 결제가 끝난 건가요

아닙니다. 서버에서 승인 API 를 호출해야 끝납니다. 저희 실행에서도 승인 시각이 결제창 통과 시각보다 뒤였습니다.

## 결론

토스페이먼츠 연동에서 계약 전에 확인할 수 있는 범위는 생각보다 넓습니다. **문서 예제를 고치지 않고 올리는 것만으로 위젯이 뜨고, 결제를 통과할 수 있고, 승인 API 까지 성공합니다.**

일정을 잡으실 때 기억하실 것은 셋입니다.

- 위젯이 뜨는 데 걸리는 시간과 결제수단이 보이는 데 걸리는 시간이 다릅니다.
- 결제창 통과와 결제 완료가 다릅니다. 서버 작업이 남아 있습니다.
- 에러 메시지가 원인을 정확히 짚어 주지 않는 구간이 있습니다. 특히 존재하지 않는 키에 만료 메시지가 옵니다.
모든 값은 2026년 8월 21일에 직접 실행해 확인했습니다. 문서와 SDK 는 바뀌므로 붙이기 전에 다시 확인하시기를 권장합니다.

## 관련 글

같은 결제 계열의 다른 항목입니다.

- [결제 수수료 분해: 가상계좌가 카드보다 비싸지는 지점은 객단가 11,765원입니다](https://dec27.dev/ko/pg-fee-structure-korea/)
- [인증 서비스 MAU 요금 분해: 사용자 한 명 값이 스무 배 넘게 벌어집니다](https://dec27.dev/ko/auth-service-mau-pricing/)
- [국내 SaaS 가격 공개 실태를 21곳 직접 확인했습니다: 18곳은 어딘가에 금액을 적어 뒀습니다](https://dec27.dev/ko/korea-saas-pricing-disclosure-survey/)
- [노션 API 로 사이트를 만들며 부딪힌 상한: 한 번에 보낼 수 있는 블록은 100개입니다](https://dec27.dev/ko/notion-api-teardown/)
---

## 출처

- [주문서형 결제 연동하기 (결제위젯 연동 가이드) (토스페이먼츠 개발자센터, 확인 2026-08-21)](https://docs.tosspayments.com/guides/v2/payment-widget/integration)
- [API 키 (토스페이먼츠 개발자센터, 확인 2026-08-21)](https://docs.tosspayments.com/reference/using-api/api-keys)
