본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 31. 23:10

결제 웹훅(Webhook)은 실행되었지만 사용자는 아무것도 받지 못했다 — 조용한 매출 킬러 디버깅하기

요약

결제는 성공했지만 사용자 권한이 부여되지 않는 웹훅(Webhook) 오류의 원인과 해결책을 다룹니다. 데이터 불일치 문제를 방지하기 위한 인증 프로세스 개선 및 사용자 역조회 전략을 제시합니다.

핵심 포인트

  • 웹훅 실행 시 사용자 ID를 반드시 전달하여 데이터 연결 보장
  • 결제 전 사용자 인증(Auth)을 먼저 생성하여 데이터 누락 방지
  • 실패 시나리오를 대비한 매직 링크 등 사용자 경험 보완
  • 해피 패스(Happy Path)를 엣지 케이스처럼 철저히 테스트할 것

지난주에 나는 사람들이 내 제품에 비용을 지불하고도 아무런 보상을 받지 못하고 있다는 사실을 발견했다.

결제가 실패했다는 의미의 "아무것도 없음"이 아니다. 결제는 성공했다. 돈은 내 계좌로 들어왔다. 하지만 결제한 사용자는 접근 권한도, 계정도, 로그인 상태도 얻지 못했다. 그저 성공 페이지와 혼란의 벽만을 마주했을 뿐이다.

이것은 200줄짜리 웹훅 (Webhook) 핸들러 안에 숨어 있던 조용한 매출 킬러에 관한 이야기다.

증상은 보이지 않았다

에러 로그도 없었다. 실패한 트랜잭션도 없었다. (아직까지는) 화난 고객 지원 이메일도 없었다. 문제는 구조적인 것이었다:

돈은 실제였지만, 접근 권한은 실제가 아니었다.

근본 원인: 세 가지 잘못된 가정

나의 원래 흐름은 새벽 2시에 보기에는 완벽하게 합리적으로 보이는 세 가지 가정을 바탕으로 설계되었다:

  1. [원문 누락 부분 - 문맥상 결제 프로세스 관련 가정]
  2. 웹훅 (Webhook)은 항상 사용자를 찾아낼 것이다. 나의 checkout/route.ts는 사용자의 Supabase ID를 Paddle의 customData로 전달하지 않았다. 그래서 웹훅이 실행되었을 때, 결제를 사용자에게 다시 연결할 방법이 없었다.
  3. syncUserPlan(null)은 아무 작업도 하지 않는 함수(no-op)이다. 그렇지 않았다. 그것은 에러도, 경고도, 재시도 큐(retry queue)도 없이 조용히 아무것도 하지 않았다.
// 한 줄로 요약된 버그:
const userId = await findUserByPaddleId(subscription.userId);
// userId = null → syncUserPlan(null) → 아무 일도 일어나지 않음 → 돈은 수거됨, 접근은 거부됨

해결책: 루프를 완성하는 네 가지 변경 사항

1. 결제 전에 인증(Auth) 생성하기

// checkout/page.tsx
// 이전: Paddle로 바로 리다이렉트
// 이후: 먼저 Supabase 사용자를 생성한 후 결제 진행
...

2. 웹훅 (Webhook)에서 사용자 역조회(Reverse-Lookup)하기

// webhooks/paddle/route.ts
async function findUserIdBySupabaseId(supabaseId: string) {
  const { data } = await supabase
...

3. 성공 페이지에 매직 링크(Magic Link) 제공

결제 후, 사용자는 자신의 이메일과 "로그인 링크 보내기" 버튼을 보게 된다. 기억해야 할 비밀번호도, 고객 지원 티켓을 보낼 필요도 없다.

// checkout/success/page.tsx
// 표시 내용: "user@email.com의 편지함을 확인하세요"
// 버튼: "로그인 링크 보내기" → Supabase 매직 링크 (magic link)

4. 전환 퍼널 (Conversion Funnel) 단축하기

새로운 경로: 아티클 (Article) → 페이월 (Paywall) → /checkout → 인증 (Auth) 생성 → Paddle → 로그인 링크 전송

단계가 하나 줄었습니다. 사용자가 이탈할 지점도 하나 줄었습니다.

진짜 교훈: 해피 패스 (Happy Path)를 엣지 케이스 (Edge Case)처럼 테스트하라

우리는 실패할 '수도 있는' 일들에 대한 에러 핸들링 (error handling)에 집착합니다. 하지만 가장 큰 위험은 종종 '작동해야 하는 모든 것이 실제로 작동할 것'이라는 가정에서 비롯됩니다.

결제 흐름 (payment flow)을 라이브로 배포하기 전, 제가 스스로에게 던지는 질문들:

  • 사용자가 존재하기 전에 웹훅 (webhook)이 실행되면 어떻게 되는가?
  • 사용자는 존재하지만 이메일이 없다면 어떻게 되는가?
  • 결제는 성공했지만 데이터베이스 (database) 쓰기에 실패하면 어떻게 되는가?
  • 사용자가 결제한 제품을 받지 못한 채 돈만 잃게 될 가능성이 있는가?

만약 이 질문들에 대한 답변이 "잘 모르겠다" 또는 "그런 일은 일어날 수 없다"라면, 당신의 프로덕션 (production) 환경에는 소리 없는 매출 킬러가 존재할 가능성이 높습니다.

중요한 수치들

이 수정 사항을 적용하기 전:

  • 처리된 결제 건수: 0보다 큰 특정 수치
  • 실제로 액세스 권한을 얻은 사용자: 마땅히 있어야 할 인원보다 적음
  • 고객 지원 티켓 (Support tickets): 0건 (아무도 불만을 제기해야 한다는 사실을 몰랐기 때문)

이 수정 사항을 적용한 후:

  • 모든 결제가 사용자를 생성함
  • 모든 사용자가 로그인 링크를 받음
  • 모든 로그인 시도가 추적됨

가장 무서운 버그는 앱을 충돌시키는 버그가 아닙니다. 고객의 돈을 조용히 가져가면서 아무것도 돌려주지 않는 버그입니다.

여러분의 결제 흐름에서 소리 없는 실패를 발견한 적이 있나요? 어떻게 찾아냈는지, 혹은 아직 찾는 중인지 이야기를 들려주세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
1

댓글

0