인앱 결제 — 제일 무서운 걸 제일 편하게
구독은 사는 순간의 문제가 아니라 매달 갱신되는지 계속 지켜보는 문제다. 그리고 진실은 서버에만 있다.
구독 결제는 1인 팀에게 가장 무서운 영역이다. 돈이 오가는 곳이라 실수가 곧 사고이고, 애플과 구글의 결제 체계가 서로 다르며, "결제했는데 안 돼요"라는 문의는 반드시 온다.
무서운 이유가 하나 더 있다. 다른 기능은 버그가 나도 사용자가 불편해하고 끝이지만, 결제는 신뢰의 문제가 된다. 돈을 냈는데 기능이 안 열리면 사용자는 앱이 고장났다고 생각하지 않고 자기가 속았다고 느낀다. 한 번 그렇게 느낀 사용자는 버그가 고쳐져도 돌아오지 않는다.
핵심 통찰은 이거였다: 구독은 "구매하는 순간"의 문제가 아니라 "매달 갱신되는가"를 계속 지켜보는 문제다. 갱신 실패, 환불, 유예 기간, 재구독 — 이 복잡한 상태 관리를 직접 만들 이유가 없다. RevenueCat이라는 전문 서비스에 맡겼다. 양쪽 스토어의 결제를 한 곳에서 받아서, 우리 서버에 "이 사용자는 Pro입니다"라고 알려주는 구조.
직접 만들지 않기로 한 결정에 대해 조금 더 적어두고 싶다. AI에게 시키면 뭐든 만들 수 있으니, 이번 프로젝트에서 나는 오히려 무엇을 안 만들 것인가를 자주 고민했다. 만드는 비용이 싸졌다고 해서 유지하는 비용까지 싸진 건 아니다. 결제 상태 관리는 내가 만드는 순간 앞으로 몇 년간 애플과 구글의 정책 변경을 내가 따라가야 한다는 뜻이다. 열흘짜리 개발은 가능해도 몇 년짜리 유지보수는 1인 팀에 없다. 그래서 남이 계속 고쳐줄 부분은 남에게 맡겼다.
그리고 원칙 하나: 진실은 서버에만 있다. 앱 화면도 구독 상태를 보여주지만 그건 표시용일 뿐, 실제로 유료 기능을 허용할지는 서버가 자기 장부를 보고 판단한다. 누군가 앱을 조작해서 화면상 Pro로 바꿔도 서버는 속지 않는다. 데이터 연동에서 "의견이 갈리면 서버가 이긴다"고 정해둔 것과 똑같은 원칙이 여기서 또 나왔다. 판단하는 곳을 하나로 정해두면, 나중에 이상한 상태를 만났을 때 어디를 봐야 하는지 고민하지 않아도 된다.
그래도 사고는 있었다.
- 결제했는데 무료 회원 : 결제한 신규 구매자가 웹에서는 무료로 표시됐다. 원인은 내부 등급 이름이 두 갈래로 갈라져 있던 것
- 유령 회원 : 로그아웃할 때마다 결제 시스템에 빈 회원이 하나씩 쌓였다
둘 다 "결제 자체"의 버그가 아니라 결제와 다른 기능이 만나는 경계에서 났다는 게 인상적이었다. 결제는 혼자 있을 때보다 로그인, 등급 표시, 웹 뷰어 같은 것들과 만날 때 깨진다.
결제가 밀린 사용자에게 화내지 않고 "스토어에서 결제 수단을 확인해 주세요"라고 안내하는 배너, 너무 낡은 버전 앱을 스토어로 안내하는 강제 업데이트 장치까지 붙이고 나서야 "붙어 있는 결제"가 아니라 "운영할 수 있는 결제"가 됐다. 결제는 붙이는 게 절반, 그 후의 시나리오가 절반이다.