Code › tail-villain
테일빌런 첫 베타 출시
로컬에서 동작하던 테일빌런을 Vercel에 배포하고 첫 사용자를 받을 준비를 마친 베타 출시 기록
5월 1일, 테일빌런의 첫 출시를 준비했다. 기능은 이미 로컬에서 동작했지만 Vercel에 배포하자 백엔드 엔트리포인트를 찾지 못했고, 그다음에는 모듈 형식이 충돌했다. 배포 버튼을 누르는 것과 실제로 제품을 출시하는 것은 다른 일이었다.
로드맵을 만들고, Study와 모의 인터뷰를 진행하고, 마지막에 리포트를 받는 흐름은 이미 이어져 있었다. 그런데 나 혼자 쓰던 환경을 벗어나 사용자를 받으려면 기능만 확인해서는 부족했다. 누가 요청을 보냈는지 식별하고, 비싼 AI 요청이 무제한으로 실행되지 않게 막고, 가입한 사용자가 실제 이메일 소유자인지 확인해야 했다.
첫 출시 직전의 이틀은 새 기능을 추가하는 대신, 제품을 계속 열어 두기 위한 준비에 사용했다.
출시 전에 먼저 막아야 할 것들
테일빌런은 로드맵 생성과 인터뷰 턴, Study 응답과 평가에서 AI를 계속 호출한다. 일반적인 데이터 조회보다 요청 한 번의 비용이 크기 때문에, 제한 없이 열어 두면 사용량과 보안 문제가 함께 커진다.
처음에는 IP를 기준으로 요청 횟수를 제한하는 방식이 단순해 보였다. 하지만 회사나 학교처럼 같은 네트워크를 공유하는 환경에서는 한 사람의 사용량 때문에 다른 사용자까지 막힐 수 있다. 로그인한 사용자의 요청과 익명 요청을 같은 기준으로 다루는 것도 맞지 않았다.
인증된 요청은 userId를 기준으로 추적하고, 익명 요청만 IP를 사용하도록 요청 제한 기준을 바꿨다. 모든 API에 같은 숫자를 적용하지 않고 짧은 요청, 중간 요청, 긴 요청, 무거운 요청으로 나눠 비용과 위험도에 따라 제한을 달리했다. 로드맵 생성과 인터뷰 메시지처럼 모델 비용이 직접 발생하는 요청은 더 엄격하게 다뤘다.
여기서 guard 실행 순서도 문제가 됐다. 사용자 기준으로 제한하려면 인증이 먼저 끝나서 요청 안에 사용자 정보가 있어야 하는데, 전역 throttling guard가 인증보다 먼저 실행되면 userId를 알 수 없다. 결국 요청 흐름을 IP 보호, 인증, 이메일 확인, 사용자와 credit 확인 순서로 다시 정리했다.
내부 데이터베이스 ID도 그대로 외부에 노출하지 않았다. URL과 API에서는 user, roadmap 같은 리소스 종류를 알아볼 수 있는 public ID를 사용하고, 내부 primary key는 저장소 안에 남겼다. 동시에 LLM 사용 로그를 추가해 어떤 기능이 어떤 모델을 얼마나 호출하는지 확인할 수 있게 했다. 비용을 볼 수 있어야 요청 제한과 credit 정책도 근거를 가지고 정할 수 있기 때문이다.
이메일 인증도 운영 기능으로 만들었다
계정이 만들어졌다는 사실만으로 민감한 기능까지 바로 열어 둘 수는 없었다. OTP 기반 이메일 인증을 추가하고, 인증하지 않은 사용자는 보호된 API에 접근하지 못하도록 막았다.
화면에서는 요청이 실패한 이유만 보여 주는 대신 인증 배너와 모달을 통해 다음 행동을 안내했다. 당시 테일빌런의 분위기에 맞춰 Raven Message 형태를 사용했지만, 중요한 건 장식보다 상태를 분명하게 전달하는 일이었다.
이메일 인증을 항상 강제할지 여부는 운영 중에 바꿀 수 있도록 admin 설정에 넣었다. 문제가 생길 때마다 코드를 수정하고 다시 배포하지 않아도, 관리 화면에서 정책을 조정할 수 있어야 첫 사용자를 받으면서 대응할 수 있었다.
로컬에서는 없던 429가 나타났다
요청 제한을 강화한 뒤 로컬 화면에서 갑자기 429 Too Many Requests가 발생했다. 실제로 API를 과도하게 호출한 것이 아니라, React Strict Mode가 개발 환경에서 effect를 두 번 실행하면서 짧은 시간에 같은 요청이 반복된 결과였다.
개발 환경의 localhost 요청은 제한 대상에서 제외해 문제를 해결했다. 단순히 로컬에서 불편한 현상을 없앤 것처럼 보이지만, 이 오류 덕분에 요청 제한이 얼마나 민감하게 동작하는지 출시 전에 확인할 수 있었다. 정책이 너무 느슨하면 보호가 되지 않고, 너무 엄격하면 정상적인 사용자 흐름까지 막는다.
Vercel에서는 서버가 시작되지 않았다
요청 제한과 이메일 인증을 준비한 뒤에도 배포는 바로 끝나지 않았다. 로컬에서 정상적으로 빌드되던 모노레포를 Vercel에 올리자 백엔드 엔트리포인트를 찾지 못했다.
NestJS 빌드 결과가 Vercel이 기대하는 dist 루트가 아니라 dist/src/main.js에 생성되고 있었고, Vercel의 Output Directory 설정에는 눈에 잘 띄지 않는 공백도 들어가 있었다. tsconfig의 rootDir과 include, exclude 범위를 다시 정리해 main.js가 올바른 위치에 나오도록 했고, 설정에 남은 공백도 제거했다.
shared types와 Prisma client가 준비되기 전에 frontend나 backend 빌드가 시작되는 문제도 있었다. 각 프로젝트 설정에 긴 명령을 따로 넣는 대신 root package script에서 생성과 빌드 순서를 고정했다. 로컬에서 우연히 남아 있던 결과물에 기대지 않고, 빈 환경에서도 같은 순서로 빌드돼야 했기 때문이다.
엔트리포인트를 고친 다음에는 nanoid에서 ERR_REQUIRE_ESM 오류가 발생했다. 사용 중이던 nanoid v5는 ESM 전용이었지만, 컴파일된 NestJS 서버는 CommonJS 방식으로 패키지를 불러오고 있었다. 모듈 시스템 전체를 출시 직전에 바꾸는 대신 CommonJS와 호환되는 nanoid v3.3.8로 내렸다.
한 문제를 고치면 다음 문제가 나왔다.
v1.0.0에서 v1.0.2까지
이 과정을 거쳐 테일빌런 v1.0.0을 배포했고, 배포 설정과 모듈 호환 문제를 수정하면서 v1.0.1과 v1.0.2까지 이어졌다. 로컬 PostgreSQL도 운영 환경인 Neon DB와 같은 17 버전으로 맞춰 인증과 세션, credit 데이터가 환경 차이 때문에 다르게 동작할 가능성을 줄였다.
첫 베타 출시 때 갖춘 것은 완성된 제품이 아니었다. 사용자 기준 요청 제한, 이메일 인증, public ID, LLM 사용 기록, 운영 설정, 반복 가능한 모노레포 빌드처럼 제품을 계속 운영하기 위한 최소한의 조건이었다.
출시는 하나의 버전을 붙이는 것으로 끝나지 않았다. 로컬에서 동작하던 가정이 운영 환경에서도 성립하는지 확인하고, 실패할 때 원인을 찾을 수 있으며, 사용자가 들어온 뒤 제품이 스스로를 보호할 수 있어야 했다.
테일빌런의 첫 베타는 그렇게 시작됐다.