Bubble.io와 Stripe API 연동 방법|구독형 결제 SaaS 구축과 Webhook 처리
온라인 강의, 업무 자동화 도구, 회원제 콘텐츠처럼 매달 이용료를 받는 서비스를 만들려면 일반 결제와 다른 구조가 필요합니다. 사용자가 한 번 결제하고 끝나는 것이 아니라 구독 생성, 정기 결제, 결제 실패, 요금제 변경, 해지까지 지속적으로 관리해야 하기 때문입니다.
Bubble.io와 Stripe를 연동하면 복잡한 서버를 직접 개발하지 않고도 구독형 SaaS 결제 시스템을 구축할 수 있습니다. 다만 결제 완료 페이지에 도착했다는 이유만으로 사용자의 유료 권한을 활성화하면 안 됩니다. 실제 결제 상태는 Stripe가 전송하는 Webhook을 기준으로 확인해야 합니다.
이 글에서는 Bubble.io와 Stripe API를 연결해 구독 결제를 생성하고, Webhook으로 회원 권한을 관리하는 전체 구조를 알아보겠습니다.
Bubble.io와 Stripe의 역할
Bubble.io는 회원가입, 요금제 선택 화면, 사용자 데이터 및 서비스 이용 권한을 관리합니다. Stripe는 카드 결제와 구독 청구, 결제 실패, 환불 및 해지를 처리합니다.
두 서비스의 역할을 나누면 다음과 같습니다.
| 구분 | 담당 기능 |
|---|---|
| Bubble.io | 회원 관리, 요금제 화면, 이용 권한, 데이터 저장 |
| Stripe Checkout | 카드 정보 입력과 결제 인증 |
| Stripe Billing | 구독 생성, 갱신, 청구서 및 결제 주기 관리 |
| Webhook | 결제 결과와 구독 상태를 Bubble에 전달 |
| Customer Portal | 결제수단 변경, 요금제 관리, 구독 해지 |
Stripe Checkout Session은 일회성 결제뿐 아니라 구독 결제도 지원합니다. 구독형 서비스에서는 Checkout Session을 생성할 때 결제 모드를 subscription으로 지정합니다.
전체 결제 흐름 이해하기
구독 결제는 다음과 같은 순서로 진행됩니다.
- 사용자가 Bubble에서 원하는 요금제를 선택합니다.
- Bubble의 서버 측 Workflow가 Stripe Checkout Session을 생성합니다.
- 사용자는 Stripe가 제공하는 안전한 결제 화면으로 이동합니다.
- 결제가 완료되면 Stripe가 Bubble의 Webhook 주소로 이벤트를 전송합니다.
- Bubble은 전달받은 고객 및 구독 정보를 데이터베이스에 저장합니다.
- 결제 상태가 정상일 때만 사용자의 유료 기능을 활성화합니다.
- 이후 갱신, 실패 또는 해지 이벤트가 발생하면 권한을 다시 조정합니다.
Stripe 구독에는 생성, 체험 기간, 정기 청구, 결제 실패, 변경 및 해지까지 여러 단계가 존재합니다. 따라서 단순히 ‘결제 성공’ 한 번만 처리하는 구조로는 안정적인 SaaS 서비스를 운영하기 어렵습니다.
1. Stripe에서 상품과 요금제 만들기
먼저 Stripe Dashboard에서 SaaS 상품을 등록합니다.
예를 들어 다음과 같이 구성할 수 있습니다.
- 상품명: Pro Plan
- 결제 방식: 정기 결제
- 결제 주기: 월간
- 가격: 월 19,000원
- 통화: KRW
상품을 생성하면 Stripe에서 price_로 시작하는 Price ID를 발급합니다.
예시:
price_1ABCDEFxxxxxxxx
결제 금액을 Bubble 화면에서 직접 전달하는 방식은 피하는 것이 좋습니다. 사용자가 브라우저 요청을 조작할 가능성이 있기 때문입니다. Bubble에는 검증된 Price ID만 저장하고, 실제 금액은 Stripe에 등록된 가격 정보를 기준으로 처리하는 것이 안전합니다.
테스트 과정에서는 Stripe의 테스트 모드를 사용하고, 실제 서비스 공개 전에 라이브 모드의 상품과 Price ID를 별도로 등록해야 합니다.
2. Bubble 데이터베이스 설계하기
안정적인 구독 상태 관리를 위해서는 User 데이터에 결제 관련 필드를 추가해야 합니다.
User 데이터 권장 필드
| 필드 이름 | 형식 | 용도 |
|---|---|---|
stripe_customer_id | text | Stripe 고객 식별값 |
stripe_subscription_id | text | 현재 구독 식별값 |
subscription_status | text | 구독 상태 |
plan_name | text | 이용 중인 요금제 |
price_id | text | Stripe Price ID |
current_period_end | date | 현재 이용기간 종료일 |
cancel_at_period_end | yes/no | 기간 종료 후 해지 여부 |
access_enabled | yes/no | 유료 기능 이용 가능 여부 |
결제 이력을 별도로 보관하려면 PaymentEvent 또는 WebhookEvent 데이터 타입을 만드는 것도 좋습니다.
WebhookEvent 권장 필드
- Stripe Event ID
- Event Type
- Customer ID
- Subscription ID
- 처리 결과
- 수신 시간
- 원본 데이터 또는 필요한 일부 값
Stripe Event ID를 저장하면 같은 Webhook이 여러 번 도착했을 때 중복 처리를 막을 수 있습니다.
3. Bubble과 Stripe 연결하기
Bubble에서는 공식 Stripe 플러그인을 이용하거나 API Connector로 Stripe API를 직접 호출할 수 있습니다.
공식 플러그인은 비교적 설정이 간단하며 카드 결제와 구독 기능을 지원합니다. Stripe 공식 기능을 더 세밀하게 제어하려면 API Connector 또는 별도의 서버 측 API를 사용할 수 있습니다. Bubble의 API Connector는 Bubble에서 외부 서비스로 요청을 전송하는 용도이며, 설정한 호출을 Workflow Action으로 실행할 수 있습니다.
Stripe 비밀키는 다음과 같이 구분됩니다.
pk_test_... 테스트용 공개키
sk_test_... 테스트용 비밀키
pk_live_... 운영용 공개키
sk_live_... 운영용 비밀키
sk_로 시작하는 Secret Key는 절대 페이지 요소나 브라우저에서 실행되는 코드에 노출하면 안 됩니다. Bubble의 플러그인 설정 또는 서버 측 비공개 값으로 저장해야 합니다.
4. Checkout Session 생성하기
사용자가 ‘구독 시작하기’ 버튼을 누르면 Bubble의 Backend Workflow에서 Stripe Checkout Session을 생성합니다.
주요 요청값은 다음과 같습니다.
mode=subscription
line_items[0][price]=price_발급받은_ID
line_items[0][quantity]=1
success_url=https://example.com/payment-success
cancel_url=https://example.com/pricing
customer=현재_사용자의_Stripe_Customer_ID
client_reference_id=Bubble_User_ID
client_reference_id나 Metadata에는 Bubble 사용자를 식별할 수 있는 내부 ID를 담을 수 있습니다. 다만 이메일, 주민등록번호, 전화번호처럼 불필요한 개인정보는 Metadata에 저장하지 않는 것이 좋습니다.
기존 Stripe Customer ID가 없는 사용자라면 고객을 먼저 만들거나 Checkout에서 고객이 생성되도록 설정합니다. 결제가 끝난 후에는 반환된 Customer ID를 Bubble의 User 데이터에 저장합니다.
중요한 주의사항
success_url 페이지가 열렸다는 사실만으로 구독 권한을 활성화해서는 안 됩니다.
사용자가 성공 페이지 주소를 직접 입력할 수도 있고, 결제 완료 후 브라우저를 닫을 수도 있습니다. 결제 결과는 화면 이동이 아니라 Stripe Webhook을 통해 최종 확인해야 합니다.
5. Bubble Backend Workflow로 Webhook 받기
Bubble에서 다음 경로로 이동합니다.
Settings → API
그다음 Workflow API와 Backend Workflows 기능을 활성화합니다.
Backend Workflows 화면에서 새로운 API Workflow를 만들고 이름을 다음과 같이 지정합니다.
stripe_webhook
일반적인 Bubble Workflow API 주소는 다음 구조를 사용합니다.
https://도메인/api/1.1/wf/stripe_webhook
개발 버전에서는 주소에 /version-test/가 포함될 수 있으므로 테스트 URL과 운영 URL을 구분해야 합니다. Bubble의 API Workflow는 페이지가 열려 있지 않아도 서버 측에서 실행되며, 외부 서비스가 보낸 요청으로 실행할 수 있습니다.
이 주소를 Stripe Dashboard의 Webhook Endpoint로 등록합니다.
6. 처리해야 할 핵심 Stripe 이벤트
모든 Stripe 이벤트를 받을 필요는 없습니다. 서비스 운영에 필요한 이벤트만 선택하는 것이 관리하기 쉽습니다.
checkout.session.completed
최초 Checkout 절차가 완료되었을 때 발생합니다.
처리할 작업:
- Stripe Customer ID 저장
- Subscription ID 저장
- 사용자와 결제 정보 연결
- 결제 처리 기록 생성
단, 결제수단이나 결제 방식에 따라 최종 결제 성공 시점이 다를 수 있으므로 이 이벤트 하나만으로 모든 상태를 확정하지 않는 것이 안전합니다.
invoice.paid
구독 청구서 결제가 정상적으로 처리됐을 때 발생합니다.
처리할 작업:
subscription_status = active
access_enabled = yes
최초 결제뿐 아니라 매월 구독이 갱신될 때도 전달되므로 유료 이용 권한을 유지하는 기준으로 사용할 수 있습니다.
invoice.payment_failed
정기 결제가 실패했을 때 발생합니다.
처리할 작업:
- 결제 실패 상태 기록
- 사용자에게 결제수단 변경 안내
- 필요한 경우 일부 기능 제한
- 재결제 기간 종료 후 권한 중지
결제가 한 번 실패했다고 즉시 모든 데이터를 삭제하는 것은 좋지 않습니다. Stripe의 재결제 설정과 서비스의 유예기간 정책을 함께 적용해야 합니다.
customer.subscription.updated
구독 요금제, 상태 또는 해지 예약이 변경될 때 발생합니다.
확인할 항목:
- 구독 상태
- 변경된 Price ID
- 현재 이용기간 종료일
- 기간 종료 후 해지 여부
customer.subscription.deleted
구독이 최종적으로 종료됐을 때 발생합니다.
처리 예시:
subscription_status = canceled
access_enabled = no
Stripe도 구독 상태, 결제 실패 및 고객 인증이 필요한 상황을 Webhook 이벤트로 처리하도록 안내하고 있습니다.
7. Webhook 보안과 서명 검증
Webhook 주소는 외부 요청을 받아야 하므로 공개될 수밖에 없습니다. 문제는 공격자가 Stripe인 것처럼 가짜 결제 완료 요청을 보낼 수 있다는 점입니다.
Stripe는 Webhook 요청에 Stripe-Signature 헤더를 포함합니다. 서버에서는 Endpoint Secret을 이용해 이 서명을 검증해야 합니다.
whsec_xxxxxxxxxxxxx
검증 과정은 다음과 같습니다.
- 요청의 원본 Body를 받습니다.
Stripe-Signature헤더를 확인합니다.- Endpoint Secret으로 서명을 검증합니다.
- 검증에 성공한 요청만 처리합니다.
- 처리 후 빠르게 2xx 응답을 반환합니다.
Bubble만으로 요청의 원본 Body와 헤더를 이용한 Stripe 서명 검증이 어려운 구성이라면 Cloudflare Workers, 서버리스 함수 또는 별도 백엔드를 중간 검증 계층으로 사용하는 방법이 안전합니다.
Stripe → 서명 검증 서버 → Bubble Backend Workflow
중간 서버가 Stripe 서명을 확인한 뒤 검증된 데이터만 Bubble로 전달하도록 구성하는 방식입니다. 단순히 Webhook URL을 숨기는 것은 보안 대책이 될 수 없습니다.
Stripe는 등록된 Webhook Endpoint로 실시간 이벤트 데이터를 전송하며, 수신 서버에서 이벤트를 안전하게 검증하고 처리하도록 안내합니다.
8. 중복 이벤트를 방지하는 방법
Stripe는 네트워크 오류나 응답 지연이 발생하면 동일한 이벤트를 다시 보낼 수 있습니다. 따라서 하나의 Webhook이 반드시 한 번만 전달된다고 가정하면 안 됩니다.
Webhook을 받으면 먼저 Stripe Event ID를 검색합니다.
이미 저장된 Event ID가 없음
→ 결제 상태 업데이트
→ Event ID 저장
이미 저장된 Event ID가 있음
→ 추가 작업 없이 종료
이러한 처리를 멱등성이라고 합니다. 중복 수신에도 결과가 한 번만 반영되도록 만드는 방식입니다.
이 처리가 없으면 같은 이벤트로 포인트가 두 번 지급되거나 이메일이 반복 발송될 수 있습니다.
9. 구독 상태와 이용 권한 분리하기
구독 상태를 단순한 yes/no 값 하나로 관리하면 예외 상황에 대응하기 어렵습니다.
Stripe에서 자주 확인하게 되는 상태는 다음과 같습니다.
| 상태 | 의미 | 권한 처리 예시 |
|---|---|---|
trialing | 무료 체험 중 | 이용 허용 |
active | 정상 구독 중 | 이용 허용 |
past_due | 결제 지연 | 유예기간 적용 |
unpaid | 미납 상태 | 이용 제한 |
canceled | 구독 종료 | 이용 중지 |
incomplete | 최초 결제 미완료 | 이용 제한 |
Bubble에는 Stripe 원본 상태를 저장하는 subscription_status와 실제 서비스 접근 여부를 나타내는 access_enabled를 분리하는 것이 좋습니다.
이렇게 구성하면 결제 실패 후 3일간 읽기 기능만 제공하는 것과 같은 운영 정책도 적용할 수 있습니다.
10. 테스트할 항목
라이브 결제를 시작하기 전에 다음 상황을 반드시 확인해야 합니다.
- 신규 월간 구독
- 연간 요금제 구독
- 결제 완료 후 권한 활성화
- 결제 도중 취소
- 카드 결제 실패
- 정기 결제 성공
- 정기 결제 실패
- 요금제 업그레이드와 다운그레이드
- 즉시 해지
- 이용기간 종료 후 해지
- 같은 Webhook의 중복 전송
- 순서가 바뀐 이벤트 수신
- 잘못된 서명의 Webhook 요청
- 테스트 환경과 운영 환경의 데이터 분리
Stripe Dashboard의 이벤트 기록과 Bubble의 서버 로그를 함께 확인하면 오류가 발생한 지점을 찾기 쉽습니다.
자주 발생하는 실수
결제 성공 페이지에서 권한을 바로 부여하는 경우
성공 페이지는 사용자 화면일 뿐 결제 상태를 보증하지 않습니다. 권한 변경은 Webhook의 결제 결과를 기준으로 처리해야 합니다.
Secret Key를 브라우저에 노출하는 경우
Stripe Secret Key는 서버 측에서만 사용해야 합니다. 페이지의 JavaScript나 공개 API 요청에 포함하면 안 됩니다.
이메일만으로 사용자를 찾는 경우
이메일은 변경되거나 중복될 수 있습니다. Stripe Customer ID와 Subscription ID를 Bubble 사용자에게 연결하는 것이 안전합니다.
결제 실패와 구독 해지를 같은 상태로 처리하는 경우
결제 실패는 재시도 후 복구될 수 있지만, 해지는 구독 종료를 의미합니다. 두 상태를 분리해서 관리해야 합니다.
Webhook 중복 수신을 고려하지 않는 경우
Stripe Event ID를 저장해 이미 처리한 요청인지 확인해야 합니다.
마무리
Bubble.io와 Stripe를 연동하면 코드를 많이 작성하지 않고도 구독형 SaaS 결제 시스템을 구축할 수 있습니다. 그러나 실제 운영 안정성은 결제 버튼보다 Webhook 처리 구조에서 결정됩니다.
Checkout은 결제 화면을 제공하고, Stripe Billing은 구독 주기를 관리하며, Bubble은 회원 데이터와 서비스 이용 권한을 관리합니다. 여기에 Webhook 서명 검증, 중복 이벤트 방지, 결제 실패 처리까지 추가해야 안정적인 구독 서비스가 완성됩니다.
처음에는 신규 구독과 정상 결제만 구현하기 쉽지만, 실제 서비스에서는 결제 실패, 해지 예약, 요금제 변경 및 Webhook 재전송까지 고려해야 합니다. 테스트 모드에서 전체 구독 흐름을 충분히 검증한 후 라이브 키로 전환하는 것이 중요합니다.
FAQ
Bubble 공식 Stripe 플러그인만으로 구독 결제가 가능한가요?
기본적인 카드 결제와 구독 기능은 구현할 수 있습니다. 다만 복잡한 요금제 변경, 세밀한 Webhook 검증 또는 Stripe의 최신 API 기능이 필요하면 API Connector나 별도 서버 연동이 필요할 수 있습니다.
Webhook 없이도 SaaS를 운영할 수 있나요?
권장하지 않습니다. 브라우저가 종료되거나 결제 상태가 나중에 변경되면 Bubble이 그 결과를 알 수 없기 때문입니다.
구독 해지 즉시 권한을 차단해야 하나요?
서비스 정책에 따라 다릅니다. 일반적으로 cancel_at_period_end 방식은 이미 결제한 이용기간이 끝날 때까지 서비스를 제공한 뒤 권한을 종료합니다.
Stripe API 키는 어디에 저장해야 하나요?
Secret Key는 Bubble 플러그인 설정이나 서버 측 비공개 값에 보관해야 합니다. 페이지 요소, URL, JavaScript 및 공개 데이터 필드에는 넣지 않아야 합니다.




댓글 0
첫 댓글을 남겨보세요.