Sleuth는 플레이어가 AI 용의자와 직접 대화하며 단서를 모으고 범인을 지목하는 모바일 추리 게임이다. 6명이 함께 만들고 운영하며, 나는 공동창업자로서 백엔드와 LLM 인프라, 결제, 콘텐츠 제작 도구를 주로 맡았다.
이 글은 Sleuth를 만들며 쓴 기술 글 17편을 한 곳에 모은 목차다. 1절에는 구성도로 전체 모양을 보고, 2절에 개발 과정에서 주로 고민했던 대표 작업 세 가지를 기록해두었다. 3절은 17편 전부를 팀과 서비스가 기술적으로 변화했던 순서대로 네 단계로 나눠 정리했다. 단계가 바뀔 때마다 풀어야 할 질문도 바뀌었다. 처음에는 "초기 검증을 위해 어떻게 빨리 만들까"였고, 마지막에는 "AI가 한 일을 사람과 시스템이 어떻게 검증할까"였다.
1. Sleuth 운영 개요
Sleuth를 쓰는 사람은 셋이다. 게임을 하는 플레이어, 푸시와 공지를 보내는 운영자(마케터와 PM), 사건 시나리오를 만드는 제작자다. 서비스의 구성도 이 세 사람을 기준으로 나뉜다.
Sleuth의 구성. 실선은 플레이어와 운영자의 요청이 지나는 길이고, 점선은 콘텐츠를 미리 만들어 올리는 제작 경로다. 동그라미 숫자는 3절의 단계 번호다.
플레이어의 앱(Flutter)이 호출하는 서버는 둘이다. 대화 서버는 FastAPI로 만들어 Cloud Run에서 돌리고, 용의자의 답변과, 게임이 끝나고 추리를 검증하는 수사보고서 채점을 담당한다. 결제 서버는 Node.js(Express)로 만들었고, Google Play와 App Store의 영수증을 스토어에 직접 확인한 뒤 게임 재화를 지급한다. 운영 백오피스(Express, React)는 운영자가 개발자를 거치지 않고 푸시와 알림함 메시지, 공지를 보내는 도구다. 데이터는 Firestore와 Cloud Storage에 둔다.
사건 시나리오는 플레이어의 요청과 상관없이 미리 만들어 둔다. Claude Code 기반 제작 파이프라인이 초안을 만들고, 데스크톱 제작 도구가 검증과 이미지 작업을 거쳐 데이터를 올린다. 대화 서버는 올라온 사건 데이터를 읽기만 하도록 각각의 역할을 구분해두었다.
2. 대표 작업 세 가지
아래는 17편 중 설계 판단이 가장 많이 담긴 작업 세 가지다. 각 작업을 문제, 결정, 결과 순서로 정리하고 관련 글을 읽는 순서대로 붙였다. 각 글 끝에는 측정 조건과 남은 한계, 다시 검토할 조건을 따로 적어 두었다.
① LLM 대화 서버: 앱이 부르던 LLM을 서버로
문제. 프로토타입에서는 앱에서 LLM API를 직접 불렀다. API 키와 프롬프트가 앱 안에 있었고, 호출 비용도 서버에서 통제할 수 없었다.
결정. LLM 호출을 FastAPI 서버로 옮겨 키 관리와 프롬프트 조립을 서버가 맡게 했다. 서버는 쓰지 않을 때 인스턴스가 0개까지 줄어드는 Cloud Run에 올려 고정비를 없앴고, 그 대가인 첫 요청 지연(콜드 스타트)은 시작 시 캐시 적재와 앱의 예열 요청으로 줄였다. 이후 AI 호출을 Vertex AI로 옮기면서 API 키 대신 서비스 계정 권한으로 인증하게 보안도 개선했다.
결과. 첫 메시지 응답이 예열 뒤 15.3초에서 4.7초가 됐다(각 1건 측정). Vertex AI로 옮긴 뒤 응답 지연의 중앙값은 약 1.96초였다.
- FastAPI와 클라우드 네이티브 Part 1: 클라이언트 LLM 호출의 한계와 백엔드 전환기
- FastAPI와 클라우드 네이티브 Part 2: Cloud Run 도입과 백엔드 전환기
- GCP Vertex AI: 프로덕션 환경을 위한 GeminiAPI 마이그레이션
- 부록: FastAPI의 비동기 구조 (동기/비동기, 블로킹, 이벤트 루프)
② 콘텐츠 제작 파이프라인: AI 시대를 위한 새로운 콘텐츠 저작 도구
문제. 추리 시나리오를 한 편 늘리려면 매번 외부 제작사와 새로 계약해야 했고, 받은 원고도 게임이 읽는 데이터 형식으로 다시 가공해야 했다. 팀에는 전담 작가가 없었다.
결정. 용의자 한 명을 이루는 항목을 고정된 형식(스키마)으로 정하고, Claude Code 세션 하나가 설계, 작성, 검수 역할을 차례로 맡는 10단계 제작 파이프라인을 만들었다. 사람 없이 돌던 에이전트가 검수 없이 승인 단계를 스스로 통과 처리한 일을 겪은 뒤에는, 다음 단계로 넘어가도 되는지를 LLM이 아니라 디스크의 파일을 직접 검사하는 스크립트(게이트)가 판정하게 바꿨다.
결과. 구조적인 게이트 강제를 통해 원하는 포멧의 결과물을 얻을 수 있었고, 결과적으로 자체 스튜디오를 통해 개발자 없이도 1인 저작이 가능해졌다. 이 파이프라인으로 만들고 제작 도구에서 다듬은 작품 7건이 와디즈 펀딩 캠페인으로 공개도 됐다.
③ 인앱결제: 영수증 검증을 위한 서버 도입
문제. 앱이 보내는 "결제 완료" 신호만 믿으면 위조된 요청이나 같은 영수증의 중복 지급을 막을 수 없다.
결정. 결제 서버가 영수증을 스토어에 직접 확인하고, 지급 직전 트랜잭션 안에서 한 번 더 확인하게 설계했다. App Store는 검증 방식이 달라 별도 계층으로 나누고, 재화 지급 로직만 공통으로 묶었다. 출시 뒤에는 검증을 통과한 다음에 생기는 문제를 다뤘다. 계속 실패하는 영수증이 반복해서 들어오는 것을 끊고, 영수증이 그 계정의 것인지 대조하고, 로그에는 결제 정보 원문 대신 되돌릴 수 없는 키만 남겼다.
결과. 테스트 환경에서 같은 유저가 동시에 20건을 요청하는 상황을 60번 반복해, 60번 모두 재화가 정확히 한 번만 차감되는 것을 확인했다. 유저는 안전하게 재화를 지급받고, 우리도 안전하게 결제를 확인하는 구조를 만들었다.
- Node.js & Google Play 인앱결제 Part 1: 결제 시스템 아키텍처 설계
- Node.js & App Store 인앱결제 Part 2: 결제 시스템 아키텍처
- 인앱결제 Part 3: 검증에 성공한 뒤에 시작되는 문제들
3. 17편 전체: 서비스의 기술 변천사
17편을 네 단계로 묶었다. 단계 안에서는 발행 순서로 놓았고, 괄호는 발행 시기다. 각 줄에는 그 글에서 무엇을 정했고 어떤 결과가 나왔는지를 적었다.
① 빠르게 만들기: Firebase 하나로 검증하기
처음 목표는 게임이 재미있는지 빨리 확인하는 것이었다(25년 2월 경). 서버를 직접 짜는 대신 Firebase에 인증과 데이터, 저장소를 맡겼고, 원격으로 일하는 팀의 협업 방식도 비슷한 시기에 정했다.
- [Firebase] 왜 스타트업의 빠른 프로토타이핑에 'BaaS'가 필수일까? (2025-11)
검증 속도를 가장 앞에 두고 Firebase의 인증, 데이터베이스, 스토리지에 백엔드를 맡겼다. 그 대가로 초기 구조 전체가 Firebase에 묶였고, 넉 달 뒤 LLM 호출, 이미지, 결제를 차례로 밖으로 옮겼다. - [스타트업/협업] 디스코드(Discord), 단순한 메신저를 넘어 우리 팀의 가상 오피스가 되기까지 (2026-01)
카카오톡과 노션을 오가던 원격 협업을 디스코드의 텍스트, 포럼, 음성 채널로 옮겼다. 플랫폼의 분산은 줄었지만, 어떤 대화를 어느 채널에 둘지는 규칙으로 직접 정해야 했다.
② Firebase 밖으로: LLM 호출과 이미지를 서버가 맡기
본격적으로 출시를 준비하면서, 앱이 LLM을 직접 부르는 구조로는 키와 비용을 지킬 수 없다는 점이 걸리기 시작했다. LLM 호출부터 서버로 옮겼고, 서버가 생기자 이번에는 고정비, 첫 요청 지연, 외부 API 지연이 새 문제가 됐다. 이미지 로딩도 같은 시기에 Firebase 계층을 걷어내고 다시 설계했다. 앱을 처음 만들었을 때는 콘텐츠 이미지들도 전부 클라이언트에 포함했던 시기가 있었다.
- [스타트업/기술] FastAPI와 클라우드 네이티브 | 클라이언트 LLM 호출의 한계와 백엔드 전환기 (Part 1) (2026-03)
앱이 LLM API를 직접 부르던 구조를 버리고 키 관리와 프롬프트 조립을 서버로 옮겼다. 서버 언어로 Python을 택하면서, Node.js로 짠 결제 서버와 두 런타임을 함께 운영하게 됐다. - [스타트업/기술] Firebase Storage 이미지 로딩 최적화 | GCP Bucket 직접 호출을 통한 Latency 개선 (2026-03)
보안 규칙과 토큰 검사를 거치는 Firebase 계층 대신 Cloud Storage를 직접 호출해, 이미지 1장 기준 첫 로딩을 2.03초에서 1.39초로, 캐시 재검증을 895ms에서 16ms로 줄였다. 썸네일은 공개 데이터라는 판단을 전제로 한 선택이다. - [스타트업/기술] FastAPI와 클라우드 네이티브 | Cloud Run 도입과 백엔드 전환기 (Part 2) (2026-03)
고정비를 없애려고 인스턴스를 0개까지 줄이는 설정을 택하고, 콜드 스타트는 시작 시 캐시 적재와 앱의 예열 요청으로 줄였다. 첫 메시지 응답이 15.3초에서 4.7초가 됐다(각 1건). - [스타트업/기술] GCP Vertex AI | 프로덕션 환경을 위한 GeminiAPI 마이그레이션 (2026-03)
AI Studio의 Gemini API에서 응답이 27초까지 늦어지는 문제를 겪고 Vertex AI로 옮겼다. 원인을 확정하지 못한 채 내린 결정이라, 이전 뒤 같은 로그 지점에서 지연을 다시 재 중앙값 약 1.96초를 확인했다. - [스타트업/기술] GitHub Actions CI 파이프라인 | 프로덕션 배포 전 런타임 에러를 차단하는 테스트 자동화 (2026-03)
로컬에서는 보이지 않던 운영 환경 오류를 겪은 뒤, 외부 의존성을 흉내 낸(mocking) 테스트를 PR마다 돌리는 CI를 붙였다. 이런 테스트가 잡을 수 있는 오류와 잡을 수 없는 오류의 경계도 함께 정리했다. - [스타트업/기술] WebP 변환을 통한 이미지 로딩 최적화 및 GCS 업로드 자동화 | PNG 대비 용량 89%, 지연 시간 81% 개선 (2026-05)
이미지를 PNG에서 WebP로 바꾸고 변환과 업로드를 스크립트로 자동화해, 이미지 1장 기준 용량을 89%, 로딩 시간을 81% 줄였다. (글에는 없지만, 후에 Cloud Function화하여 저작 파이프라인과도 통합했다) - [스타트업/기술] FastAPI와 클라우드 네이티브 | 부록: FastAPI의 비동기 구조 (동기/비동기, 블로킹, 이벤트 루프) (2026-05)
대화 서버가 요청 하나를 처리하는 단계에서 출발해, 동기와 비동기, 블로킹, 이벤트 루프, FastAPI가 코드를 어디서 실행하는지를 해설했다. 로컬 시뮬레이션으로 비동기 함수 안의 동기 호출이 다른 요청까지 늦추는 모습을 보였다.
③ 돈을 다루는 서버: 결제는 철저하게
결제는 틀리면 돈이 잘못 나가는 영역이라 출발점부터 달랐다. 앱의 신호를 믿지 않고 서버가 스토어에 직접 확인한다는 원칙을 세우고, 흩어져 있던 가격 데이터를 한곳으로 모은 뒤, 출시 후에는 검증을 통과한 다음에 생기는 실패까지 다뤘다.
- [스타트업/기술] Node.js & Google Play 인앱결제 | 결제 시스템 아키텍처 설계 (Part 1) (2026-04)
앱의 결제 완료 신호를 믿지 않고, 서버가 구매 토큰을 구글 서버에 직접 확인한 뒤 트랜잭션 밖과 안에서 두 번 확인해 중복 지급을 막았다. 그 대가로 결제 흐름에 스토어 API 왕복이 한 번 늘었다. - [스타트업/기술] Node.js & App Store 인앱결제 | 결제 시스템 아키텍처 (Part 2) (2026-04)
서명된 토큰(JWS)으로 오는 App Store의 검증 흐름을 별도 계층으로 나누고, 재화 지급 로직만 플랫폼과 무관한 공통 함수로 합쳤다. 유지보수할 검증 계층이 하나 늘었다. - [스타트업/이슈] Google Play 인앱 결제 API | "insufficient permissions" 오류 (2026-04)
결제 API의 권한 오류를 서비스 계정 권한을 늘려 풀려다 실패했고, 인앱 상품을 새로 등록해 해결했다. 원인은 두 가지 가설로 남겼고, 에러 처리 계층이 구글의 응답 상태를 지워 추적이 어려웠던 점도 적었다. - [스타트업/기술] SSoT 기반 카탈로그 시스템 설계 | 파편화된 가격 데이터의 비효율성을 극복하고 프로모션 구조 구축하기 (2026-04)
스토리 문서마다 흩어져 있던 가격을 카탈로그 한곳에서 참조하게 모으고, 그 위에 기간과 대상을 지정하는 프로모션 구조를 올렸다. 구매 시점의 가격은 구매 기록에 함께 남긴다. - [스타트업/기술] 인앱결제 Part 3 | 검증에 성공한 뒤에 시작되는 문제들 (2026-06)
검증은 통과했지만 지급하면 안 되는 경우를 다뤘다. 계속 실패하는 영수증의 반복 재전송을 끊고, 영수증과 계정의 소유 관계를 대조하고, 로그에는 원문 대신 지문만 남겼다. 막힌 정당한 결제자는 절차서를 따라 사람이 구제한다.
④ 팀 생산성 높이기: 운영과 콘텐츠 제작
서비스가 어느정도 돌아가기 시작하자 다음 과제는 개발자와 외부 작가에게 묶여 있던 일을 줄이는 것이라고 생각했다. 공지와 알림은 운영자가 직접 보내게 했고, 시나리오는 외주 대신 LLM 파이프라인으로 만들었다. AI native를 지향하되, 사람의 승인과 스크립트의 게이트를 통과하도록 만들었다.
- [스타트업/기술] 알림함 아키텍처: Fan-out on Write vs Fan-out on Read | 비용을 '구독자 수'가 아닌 '활성 유저 수'에 비례시키기 (2026-06)
수신자를 서버가 알 수 있는 발송은 보낼 때 수신자마다 알림을 미리 써 두고, 구독자를 알 수 없는 언어별 전체 공지만 유저가 앱을 열 때 채우게 나눴다. 비용이 구독자 수가 아니라 실제로 앱을 여는 유저 수에 비례하게 된 대신, 중복 방지와 수신 동의 재확인, 읽은 위치 관리를 앱이 맡았다. - 추리 게임 시나리오, 외주에서 자체 파이프라인으로 (2026-08)
용의자를 고정된 형식으로 구조화하고, Claude Code 세션 하나가 설계, 작성, 검수 역할을 차례로 맡는 10단계 제작 파이프라인을 만들었다. 궁극적으로는 추리의 몰입 경험을 제공하고자하는 팀으로서, 빠르고 양질의 콘텐츠를 공급하는게 중요했다. - [스타트업/기술] 에이전트의 Self-approval 문제 | LLM 저작 파이프라인을 결정론적 게이트로 막다 (2026-09)
사람 없이 돌던 에이전트가 승인 단계를 스스로 통과 처리한 일을 계기로, 승인 판정을 프롬프트 지시에서 디스크의 파일을 직접 검사하는 게이트로 옮겼다. 이 게이트가 막는 것과 아직 막지 못하는 것을 나눠 적었다.
새 글을 발행하면 이 목록에 이어서 업데이트해보도록 하겠다.