토스인앱 결제, 왜 개발자들이 선택하나
토스인앱은 토스페이먼츠가 제공하는 모바일 앱 내 결제 SDK로, 별도 PG사 계약 없이 신용카드·간편결제·계좌이체를 한 번에 붙일 수 있다. 기존 PG 연동은 각 결제수단마다 계약서 쓰고 API 문서 뒤지며 며칠씩 걸렸지만, 토스인앱은 SDK 임포트 한 줄로 시작해서 당일 테스트까지 돌릴 수 있다. 다만 실제 운영 배포 전까지 검증 로직과 웹훅 처리를 제대로 안 하면 결제는 성공했는데 상품이 안 나가거나, 중복 결제가 터지는 사고가 생긴다. 이 글은 토스인앱을 처음 붙이는 개발자가 SDK 설치부터 서버 검증까지 놓치기 쉬운 순서를 실무 기준으로 정리한다.
SDK 설치와 초기 설정 — 5분이면 끝나는 이유
토스인앱 SDK는 iOS는 CocoaPods, Android는 Gradle로 한 줄만 추가하면 된다. iOS 예시는 pod 'TossPayments', Android는 implementation 'com.tosspayments:payment-sdk-android:latest' 형태다. 설치 후 앱 초기화 코드에서 클라이언트 키를 등록하는데, 이 키는 토스페이먼츠 개발자센터에서 테스트용과 운영용을 따로 발급받는다. 테스트 키로 실제 돈이 빠지지 않으니 처음엔 샌드박스 키를 쓰고, 나중에 환경 변수로 분리해서 배포 빌드에만 운영 키를 주입하면 된다.
초기화 코드는 보통 AppDelegate나 Application 클래스 onCreate에 넣는다. 여기서 실수가 하나 있는데, 키를 하드코딩으로 박아두면 깃허브에 올렸다가 키가 노출되는 경우가 많다. 환경 변수나 .env 파일로 빼고 .gitignore에 추가하는 습관을 들여야 한다. SDK 초기화가 끝나면 결제 요청 함수 하나만 호출하면 토스 결제창이 뜨는데, 이 단계까지는 공식 문서 따라 10분이면 된다. 문제는 그다음 서버 검증부터다.
결제 요청 파라미터 — 필수 값과 선택 값 구분

결제 요청 시 넘기는 파라미터는 크게 세 가지다. amount(금액), orderId(주문 고유 ID), orderName(주문명). 여기서 orderId는 절대 중복되면 안 되는 값이라서 UUID나 타임스탬프 조합으로 만든다. 같은 orderId로 두 번 요청하면 토스 서버가 중복 결제로 막아주긴 하지만, 앱 재시작이나 네트워크 재시도 로직에서 같은 ID를 재사용하면 결제 실패 팝업만 뜨고 원인 파악이 어렵다.
orderName은 결제창에 표시되는 상품명인데, 여기에 이모지나 특수문자를 넣으면 일부 카드사 화면에서 깨지는 경우가 있다. 한글·영문·숫자만 쓰거나, 최소한 테스트 결제 후 실제 카드 영수증을 확인해봐야 한다. 선택 파라미터로는 customerEmail, customerName, taxFreeAmount(면세 금액) 등이 있는데, 면세 처리가 필요한 전자책이나 디지털 콘텐츠 판매 앱이라면 taxFreeAmount를 빼먹으면 부가세를 이중으로 내는 셈이 된다.
결제 결과 수신과 클라이언트 검증 한계
결제창에서 사용자가 카드 번호 입력하고 확인을 누르면 토스 SDK가 콜백으로 결과를 돌려준다. 성공 시 paymentKey와 orderId, amount가 넘어오는데, 이 시점에서 많은 개발자가 "결제 성공했으니 상품 지급"을 바로 처리한다. 이게 가장 위험한 실수다. 클라이언트 콜백은 조작 가능하다. 루팅된 안드로이드나 탈옥 iOS에서 후킹 도구로 콜백 파라미터를 위조해서 amount를 1원으로 바꾸고도 성공 응답을 받을 수 있다.
그래서 클라이언트에서 받은 paymentKey를 서버로 보내고, 서버가 토스페이먼츠 API를 직접 호출해서 실제 결제 금액과 상태를 재확인하는 과정이 필수다. 이 과정을 "결제 승인(confirm)"이라고 부른다. 클라이언트 콜백은 "결제 시도 완료" 정도로만 보고, 진짜 돈이 들어왔는지는 서버가 토스 API /v1/payments/{paymentKey}로 조회해서 status: DONE, totalAmount가 주문 DB와 일치하는지 대조해야 한다.
서버 웹훅 처리 — 결제 승인 후 상품 지급 타이밍

토스페이먼츠는 결제가 최종 완료되면 서버로 웹훅(Webhook)을 쏜다. 이 웹훅 URL은 개발자센터에 미리 등록해둬야 하고, POST 요청으로 paymentKey, orderId, status 등이 JSON으로 날아온다. 웹훅 수신 시 해야 할 일은 세 가지다. 첫째, orderId로 내 DB에서 주문을 찾는다. 둘째, 이미 처리된 주문인지 체크(중복 웹훅 방지). 셋째, 토스 API로 paymentKey 재조회해서 금액·상태 일치 확인 후 상품 지급.
웹훅은 네트워크 장애로 여러 번 올 수 있다. 같은 paymentKey로 두 번째 웹훅이 오면 "이미 처리됨" 상태를 DB에서 확인하고 200 OK만 응답해야 한다. 200이 아닌 응답을 주면 토스 서버가 "실패"로 판단해서 몇 분 뒤 재전송을 시도한다. 또 웹훅 수신 후 상품 지급 로직이 5초 이상 걸리면 토스 서버가 타임아웃으로 재시도를 보낼 수 있으니, 웹훅 핸들러는 빠르게 DB 상태만 업데이트하고 200을 먼저 돌려준 뒤, 실제 지급 로직은 백그라운드 큐로 돌리는 게 안전하다.
환불과 부분 취소 — API 호출 시 주의점
환불은 /v1/payments/{paymentKey}/cancel 엔드포인트로 POST 요청을 보낸다. 전액 환불이면 cancelAmount 파라미터 없이 보내고, 부분 취소면 취소할 금액을 명시한다. 여기서 실수는 부분 취소 금액이 원래 결제 금액을 초과하거나, 이미 취소된 금액을 빼지 않고 또 요청하는 경우다. 예를 들어 10,000원 결제 후 3,000원 부분 취소했으면 남은 금액은 7,000원인데, 다시 5,000원 취소 요청을 보내면 성공하지만, 8,000원을 요청하면 잔액 초과로 에러가 난다.
환불 API 응답에는 cancels 배열이 있어서 지금까지 취소된 내역이 모두 들어온다. 이 배열을 파싱해서 총 취소액을 계산하고, 남은 취소 가능 금액을 UI에 표시해주면 사용자 실수를 줄일 수 있다. 또 환불 사유(cancelReason)는 필수 파라미터는 아니지만, CS 대응이나 정산 시 추적을 위해 "고객 요청", "재고 부족" 같은 간단한 문구라도 넣어두는 게 좋다.
테스트 카드와 실제 운영 전환 체크리스트

토스페이먼츠는 테스트 환경에서 쓸 수 있는 가상 카드 번호를 제공한다. 대표적으로 4000-0000-0000-0008(승인), 4000-0000-0000-0051(한도 초과 실패) 등이 있고, CVC는 아무 숫자나 넣어도 된다. 이 카드로 결제 플로우를 반복 테스트하면서 성공·실패·취소·웹훅 재시도 시나리오를 모두 돌려봐야 한다. 특히 네트워크 끊김 시뮬레이션은 Charles나 Proxyman 같은 프록시 도구로 웹훅 응답을 지연시켜서 재시도 로직이 제대로 동작하는지 확인한다.
운영 전환 전 체크리스트는 다음과 같다.
-
클라이언트 키를 운영 키로 교체했는지
-
서버 API 호출 시 시크릿 키를 환경 변수로 분리했는지
-
웹훅 URL이 HTTPS이고, 외부에서 접근 가능한지
-
orderId 생성 로직이 충돌 없이 고유한지

-
결제 금액과 DB 주문 금액 대조 로직이 있는지
-
웹훅 중복 수신 시 멱등성 보장되는지
-
환불 API 호출 시 취소 가능 금액 체크하는지
이 중 하나라도 빠지면 운영 초기에 고객 문의가 폭주하거나, 정산 시 금액 불일치로 회계팀과 싸우게 된다.
자주 묻는 질문 — 실무에서 막히는 지점
결제창이 뜨지 않고 바로 앱이 종료돼요

iOS에서 Info.plist에 LSApplicationQueriesSchemes 배열에 tosspayments를 추가 안 하면 URL 스킴 호출이 막힌다. Android는 AndroidManifest.xml에 인텐트 필터를 등록해야 하는데, 공식 문서 예제 코드를 그대로 복사하면 보통 해결된다. 그래도 안 되면 SDK 버전이 최신인지, 초기화 코드가 실제로 실행됐는지 로그를 찍어 확인한다.
웹훅이 안 와요
개발자센터에 등록한 웹훅 URL이 로컬호스트(localhost)면 토스 서버가 접근할 수 없다. ngrok 같은 터널 서비스로 로컬 서버를 외부에 노출하거나, 개발 서버를 클라우드에 띄워서 공인 IP로 등록해야 한다. 또 웹훅 핸들러가 200 OK를 5초 안에 응답 안 하면 토스가 "실패"로 보고 재시도를 멈출 수 있으니, 핸들러 로직을 최대한 가볍게 만든다.
결제는 성공했는데 상품이 안 나가요
클라이언트 콜백만 믿고 상품 지급했거나, 웹훅 처리 로직에 예외가 터져서 DB 업데이트가 안 된 경우다. 웹훅 핸들러에 try-catch를 걸고, 에러 발생 시 슬랙이나 센트리(Sentry)로 알림을 보내서 즉시 대응할 수 있게 만든다. 또 주문 상태를 "결제 대기 → 결제 완료 → 상품 지급 완료"처럼 세분화하면 어느 단계에서 멈췄는지 추적이 쉽다.
마무리 — 다음 단계로 넘어가기
토스인앱 SDK 자체는 간단하지만, 결제 시스템은 돈이 오가는 만큼 검증과 예외 처리가 핵심이다. 클라이언트 콜백을 절대 신뢰하지 말고, 서버에서 토스 API로 재확인하는 흐름을 기본으로 깔아야 한다. 웹훅 중복 수신과 네트워크 재시도는 반드시 일어난다고 가정하고, 멱등성을 보장하는 로직을 처음부터 넣어두면 나중에 장애 대응 시간을 크게 줄일 수 있다. 지금 당장 해야 할 일은 테스트 환경에서 결제 성공·실패·환불 시나리오를 각각 10번씩 돌려보고, 웹훅 로그가 DB에 제대로 쌓이는지 확인하는 것이다. 그다음 운영 키로 전환하고, 첫 실 결제를 직접 테스트한 뒤 환불까지 해보면 실전 배포 준비가 끝난다.
