음성봇을 스텁부터 만든 이유 — 테스트로 설계를 먼저 굳히다

시리즈 · 음성봇 직접 만들기 — 실시간 음성봇의 뼈대를 바닥부터 만든 8편 기록입니다. 1. 끼어들기(barge-in) — 재생 중인 TTS를 실시간으로 끊는 법 2. "말이 끝났다"는 판단 — VAD 엔드포인터 3. 엔진을 코드 수정 없이 바꾸는 인터페이스 설계 4. 로컬 LLM 대화 두뇌와 도구 호출 루프 5. 매 턴 지연시간을 재는 이유 6. 브라우저 마이크에서 서버까지 — 오디오 프로토콜 7. 로컬 Whisper STT 연결하기 8. 스텁부터 만든 이유 — 테스트 우선 설계 (완결) — 지금 보는 글 돌아보며 시작하는 마지막 편 이 시리즈를 시작할 때부터 정해 둔 규칙이 하나 있었습니다. 진짜 음성 인식 모델과 진짜 합성 모델이 준비되기 전까지는, 그 둘을 흉내만 내는 가짜 엔진으로 전체 구조를 먼저 돌려본다는 규칙입니다. 여덟 편을 거치며 끼어들기 처리, 상태 흐름, 프로토콜, 엔진 인터페이스를 하나씩 다뤘는데, 그 모든 것이 사실 이 가짜 엔진 위에서 검증됐습니다. 지금 코드를 다시 열어 보면 그때는 별생각 없이 세운 규칙이 결과적으로 시리즈 전체의 진행 속도를 결정했다는 게 눈에 들어옵니다. 이번 편에서는 왜 굳이 가짜부터 만들었는지, 그리고 그 선택이 시리즈 전체에 어떤 영향을 남겼는지 정리하면서 이 시리즈를 마무리하려 합니다. 실제 모델을 기다리지 않기로 한 이유 음성봇의 핵심 로직은 사실 음성 인식이나 음성 합성 자체가 아닙니다. 사용자가 말을 시작했는지 끝냈는지 판단하는 상태머신, 합성 중에 끼어들기가 들어왔을 때 스트림을 멈추는 동시성 처리, 이벤트가 순서대로 오가는 흐름 제어 같은 것들이 훨씬 까다롭습니다. 반면 실제 인식·합성 모델은 무겁고 설치가 까다롭고 실행 속도도 느립니다. 이 두 가지를 한꺼번에 붙잡고 있으면 어려운 동시성 버그를 실제 모델의 로딩 시간과 뒤섞어 디버깅하게 됩니다. 모델이 느려서 반응이 늦은 건지, 상태머신이 잘못 짜여서 늦은 건지 구분이 안 되는 상황을 미...

사업자등록만 하면 되는 줄 알았다 — 개인 앱 출시 직전에 만난 행정의 여러 겹

지난 편에서는 코드를 다 짜고 나면 바로 제출 버튼을 누를 수 있을 줄 알았다가, 최소 1-2주 걸리는 사업자등록부터 다시 시작해야 한다는 사실을 확인하는 데서 끝났습니다. 그때는 사업자등록만 통과하면 제출까지 갈 수 있을 거라고 믿었습니다.

실제로는 아니었습니다. 사업자등록은 벽이 아니라 첫 번째 겹이었고, 그 뒤로 몇 겹이 더 있었습니다. 이 글은 그 겹들을 지나며 함께 처리한 리브랜딩과 구독 가격 결정까지, 제출 버튼 직전까지의 기록입니다.

사업자등록은 끝이 아니라 자물쇠 하나였다

유료 앱, 특히 구독이 들어간 앱을 App Store에 올리려면 App Store Connect에서 Paid Apps Agreement가 활성화(Active)되어야 합니다. 사업자등록은 이걸 켜기 위한 조건 중 하나일 뿐이었습니다.

Agreement가 Active로 바뀌려면 그 아래 항목들이 전부 초록불이어야 했습니다. 사업체 정보, 대금을 받을 은행 계좌, 그리고 세금 서류입니다. 저는 해외(미국) 매출에 대한 원천징수 관련 양식(W-8BEN)과 한국 쪽 세금 정보를 각각 넣어야 했고, 이것들이 하나씩 검토를 거쳐 Active가 되는 걸 기다려야 했습니다. 하나라도 "보류" 상태면 Agreement 전체가 안 켜지고, Agreement가 안 켜지면 구독 상품을 "제출 준비 완료"로 만들 수가 없습니다.

코드에서 의존성 하나가 안 풀리면 그 위의 것들이 전부 빌드가 안 되는 것과 비슷했습니다. 다른 점은, 여기서는 내가 재시도 버튼을 누르는 게 아니라 심사 쪽의 처리를 기다려야 한다는 거였습니다. 손을 쓸 수 없는 대기 시간이 있다는 게 코드와 가장 다른 부분이었습니다.

기다리는 동안 이름을 바꿨다

승인을 기다리는 시간이 아깝기도 했고, 출시를 코앞에 두니 이름이 다시 눈에 들어왔습니다. 그래서 앱 이름을 JustBlurt에서 Talktion으로 바꿨습니다. 서브타이틀은 앱이 하는 일을 그대로 드러내는 Talk2Notion으로 잡았습니다.

출시 전에 이름을 바꾸는 건 생각보다 손이 많이 가는 작업입니다. App Store Connect의 앱 이름과 부제만 바꾸는 게 아니라, 저장 시 이름 중복 판정에 걸리는지 확인해야 하고, 앱 안에 박혀 있던 표시 이름, 구독 상품의 표시명, 그리고 결제 연동 쪽 설정까지 새 이름과 어긋나지 않는지 하나씩 맞춰야 했습니다. 이름은 문자열 하나가 아니라 여러 곳에 흩어져 있는 참조였습니다.

그래도 출시 직전이 이걸 할 수 있는 마지막 타이밍이었습니다. 사용자가 생기고 나면 이름을 바꾸는 비용은 훨씬 커지니까요. 어차피 대기 중이라 시간도 있었습니다.

구독 가격을 정하는 일

가격은 코드가 아니라 판단의 문제라 오히려 더 오래 붙들었습니다. 최종적으로 월 $4.99, 연 $39.99로 정했습니다. 연간을 월 환산하면 약 $3.33이라 월 구독 대비 33% 싼 셈이고, 연간 플랜에는 7일 무료 체험을 붙였습니다. 오래 쓸 사람은 연간으로 유도하고, 부담스러운 사람은 체험으로 먼저 써 보게 하려는 구성입니다.

고민하다 접은 선택지도 있습니다. 한 번 결제로 평생 쓰는 라이프타임 딜(LTD)입니다. 초기 사용자를 빠르게 모으는 데는 LTD가 매력적이지만, 이 앱은 쓸 때마다 클라우드 STT와 정제에 원가가 계속 드는 구조입니다. 한 번 받은 돈으로 무한히 발생하는 비용을 감당하는 모델은, 사용자가 많아질수록 저를 조여 옵니다. 그래서 LTD는 기본 상품에서 빼고, 나중에 출시 기념 한정 프로모션으로만 열어 둘지 검토하기로 했습니다.

가격을 정하고 나서야 알았습니다. 가격은 숫자를 고르는 일이 아니라, 이 앱을 앞으로 어떤 비용 구조로 운영할지를 정하는 일이었습니다.

이제 제출 버튼 앞에서

지금 남은 건 순수한 실행 목록입니다. App Store Connect에서 구독 상품을 최종 설정하고, 결제 연동 대시보드에 연간 상품을 등록하고, 클라우드로 오디오가 전송되는 만큼 개인정보 라벨에 "Audio Data"를 다시 넣고, 실기기에서 마지막으로 한 번 돌려 본 뒤, 빌드를 올려 제출하는 순서입니다.

이 목록에서 실제로 발을 헛디디기 쉬운 자리가 하나 있습니다. 월 상품은 예전에 만들어 뒀는데, 이번에 새로 만든 연간 상품을 결제 연동 도구(RevenueCat) 쪽에도 등록하고 판매 묶음(offering)에 넣어 주지 않으면, App Store Connect에는 연간 상품이 멀쩡히 있어도 앱의 결제 화면에는 연간 옵션이 안 뜹니다. 두 시스템에 각각 존재를 알려 줘야 비로소 사용자 화면에 나타나는 구조라, "분명히 만들었는데 앱에서 안 보인다"고 한참 헤매기 좋은 지점입니다. 무료 사용자는 하루 몇 번까지만 클라우드로 처리하고 그 뒤엔 온디바이스로 조용히 넘어가게 해 뒀는데, 이 경계가 결제 화면의 업그레이드 유도와 맞물려 있어서, 실기기 검증 때 무료·유료 두 계정으로 각각 확인해야 할 것도 여기서 나옵니다.

돌아보면 지난 편의 교훈이 반쯤만 맞았습니다. "제출하면 되겠다"고 생각했다가 사업자등록을 만난 게 지난 편이었다면, 이번엔 "사업자등록만 하면 되겠다"고 생각했다가 그 위의 여러 겹을 만났습니다. 행정은 코드처럼 한 번에 끝나는 대신, 열고 보면 그 안에 또 문이 있는 구조였습니다.

그래도 이번엔 남은 게 전부 내가 누를 수 있는 버튼들입니다. 기다려야 하는 자물쇠는 풀렸습니다. 다음 편은 아마 "드디어 제출했다", 혹은 "제출하고 또 뭔가에 걸렸다"가 될 텐데, 어느 쪽이든 이 사이드 프로젝트의 다음 한 걸음입니다.

댓글

이 블로그의 인기 게시물

디스코드 구글캘린더 연동 봇 만들기: 카톡 캡처 OCR 자동 등록 시스템 구축

클로드 코드 비용 줄이는 법 — ollama 로컬 LLM 연동 (M3 맥북)

유튜브 노션 자동 저장 봇 만들기 — Claude CLI MCP + Discord 233일 운영기