Code › tail-villain

결제 전에 필요한 운영 경계

결제 기능보다 먼저 크레딧과 동의, 운영 구조를 갖춘 과정

돈을 받기 전에 먼저 정해야 할 것들이 있었다. 가격표보다 먼저, 사용량을 어떻게 세고 어디서 막을지 정해야 했다.

tail-villain은 AI 인터뷰 제품이라서 사용자의 행동이 곧 비용으로 이어진다. 로드맵 분석, 인터뷰 턴, 학습 세션, 리포트가 모두 모델 호출과 연결된다. 결제 화면만 붙인다고 유료 제품이 되는 게 아니라, 사용량과 권한과 동의가 같은 방향으로 움직여야 했다.

5월 초 작업은 그래서 charging 자체보다 결제 전에 필요한 운영 경계를 구현하는 작업이었다. frontend와 backend를 나누고, session이 끊기지 않게 만들고, credit을 차감하고 환불할 수 있게 만들고, 가입 과정에서 필요한 동의와 사용자 상태를 제대로 남기는 일들이 이어졌다.


먼저 배포 구조를 분리했다. frontend는 tailvillain.com, backend는 api.tailvillain.com으로 나누고, backend 쪽의 불필요한 API prefix를 제거했다. URL이 짧아지는 것보다 중요한 건 책임이 분리되는 쪽이었다. 사용자가 보는 화면과 API 서버가 다른 도메인에서 움직일수록 CORS, cookie, token refresh 같은 경계도 더 분명해진다.

그 과정에서 세션 유지 버그가 드러났다. refresh token cookie의 path가 예전 API prefix에 묶여 있었고, prefix를 제거한 뒤에는 refresh 요청에 cookie가 실리지 않았다. 사용자 입장에서는 밤새 세션이 끊긴 것처럼 보였고, 서버 재시작 뒤에도 다시 로그인해야 했다.

결제나 credit보다 먼저 이런 문제가 잡혀야 했다. 유료 사용자가 생긴 뒤에 session persistence가 흔들리면, 그건 단순 로그인 버그가 아니라 신뢰 문제이기 때문이다. cookie path를 현재 API 구조에 맞게 고치고, JWT secret이 환경에서 안정적으로 로드되는지도 확인했다.

배포 빌드도 같은 범주였다. TypeScript incremental cache가 깨져서 build output이 꼬이면 코드가 맞아도 배포된 서버는 다르게 동작할 수 있어서, build cache를 비우고 pnpm 기준으로 script를 정리했다. Prisma schema 경로도 명시해서 배포가 같은 순서로 반복되도록 만들었다.


그다음은 credit이었다. 처음에는 plan이나 role처럼 등급 중심으로 생각하기 쉽지만, AI 제품에서는 결국 사용량 단위가 필요하다. 사용자가 어떤 등급인지보다, 어떤 작업이 얼마만큼의 비용을 발생시키는지가 더 직접적인 기준이기 때문이다.

그래서 UserSubscription에 remainingCredits와 monthlyCredits를 두고, CreditTransaction으로 모든 이동을 남겼다. 차감, 환불, 충전, 보너스가 각각 기록으로 남아야 나중에 사용자가 왜 잔액이 줄었는지 설명할 수 있다. 잔액이 음수가 되지 않도록 데이터베이스 제약도 넣었는데, 돈과 가까운 값은 애플리케이션 코드만 믿기보다 저장소에서도 막아야 하기 때문이다.

차감도 기능별로 나눴다. 로드맵 생성은 분석과 토픽 생성이 들어가므로 무겁고, 인터뷰와 학습은 턴 단위로 반복된다. 리포트는 별도의 종합 평가라서, 같은 AI 호출이라고 묶어버리면 가격과 운영이 흐려진다.

실패 시 환불도 같이 필요했다. AI 생성이 중간에 실패했는데 credit만 빠져나가면, 사용자는 시스템 비용 구조를 이해해주지 않는다. 제품이 비용을 세기로 했다면 실패도 같은 장부 안에서 처리해야 한다.


pricing은 숫자를 붙이는 일이 아니라 기대치를 조정하는 일이었다. beta 단계에서는 무료 또는 낮은 진입 비용으로 들어오는 사용자가 많고, 동시에 나중에 어떤 유료 기능이 열릴지도 보여줘야 한다.

그래서 beta pricing은 credit을 중심으로 설명했다. Novice plan에는 beta 기간의 추가 credit과 일부 premium 기능 접근을 포함하고, 상위 tier는 아직 잠겨 있더라도 어떤 기능을 포함할지 보여주는 식으로 정리했다. 흐릿하게 숨기기보다 지금 쓸 수 있는 것과 앞으로 열릴 것을 구분했다.

여기서 중요한 건 과장하지 않는 것이었다. 아직 결제까지 완전히 열린 상태가 아니라면, 마치 모든 tier가 즉시 구매 가능한 것처럼 쓰면 안 된다. 사용자가 이해해야 하는 건 지금 beta에서 무엇을 얻고, credit이 어떤 기준으로 줄어드는지였다.

검색 노출을 위한 기본 설정도 이 시점에 들어갔다. robots와 sitemap 같은 작업은 눈에 띄는 기능은 아니지만, public product라면 제품의 바깥쪽 입구를 만드는 일이기도 하다.


가입 과정도 줄였다. 결제 전에 사용자를 더 많이 통과시키려면 처음부터 너무 많은 값을 묻지 않는 편이 나아서, nickname과 language 입력을 제거했다. nickname은 자동 생성하고, language는 현재 UI locale을 기준으로 잡게 했다.

이건 단순히 form field를 줄인 일이 아니었다. 사용자는 아직 제품을 써보기도 전에 이름과 설정을 고민하고 싶어 하지 않는 반면, 제품은 사용자를 구분할 최소한의 identity가 필요하다. 자동 nickname은 그 사이의 타협이었고, 사용자는 바로 시작하고 시스템은 고유한 표시 이름을 가진다.

동의 모델은 반대로 더 명확해야 했다. Terms, Privacy Policy, Marketing consent를 UserConsent로 남기고, 해외 데이터 이전과 AI 처리 파트너에 대한 안내를 가입 흐름 안에 넣었다. 법적 문구를 예쁘게 숨기기보다, 사용자가 어떤 데이터 흐름에 동의하는지 알 수 있게 만드는 쪽이 중요했다.

체크박스가 늘어나면 가입 전환율이 떨어질 수 있다. 하지만 결제를 붙일 제품이라면 동의 상태가 나중에 어딘가에 남아 있어야 하고, friction을 줄이는 것과 필요한 동의를 흐리는 것은 다른 문제다.


5월 3일에는 asset 최적화도 있었다. persona avatar와 landing screenshot을 WebP로 바꾸고, 오래된 favicon 충돌을 정리했다. 이 글의 중심은 시각 polish가 아니지만, public product에서는 초기 로딩과 asset 경로도 운영 품질에 들어간다. 사용자가 가입하고 결제를 고민하기 전에, 첫 화면이 불필요하게 무거우면 이미 한 번 밀리는 셈이다.

하지만 더 큰 축은 그대로였다. 배포는 frontend와 backend의 경계를 분명히 해야 하고, session은 도메인이 나뉘어도 이어져야 한다. credit은 차감과 환불을 남겨야 하며, onboarding은 가볍지만 consent는 정확해야 했다.

결제는 버튼 하나가 아니다. 사용량을 세는 장부, 실패를 되돌리는 규칙, 사용자를 식별하는 방식, 데이터 동의를 남기는 기록이 같이 있어야 한다.

돈을 받기 전에 제품은 먼저 운영 가능한 모양이 되어야 했다.