1인 스튜디오의 배포: Cloudflare Workers, Neon, Expo

서버가 필요한 앱 하나를 혼자서 운영 가능한 상태로 만들기. 월 5달러의 인프라로 staging과 production을 나누고, 비밀은 어디에도 적지 않는 방법.

배포cloudflareexpo

WhenWeave는 서버가 있는 앱입니다. 혼자 만드는 앱에 서버가 있다는 건, 만드는 시간의 상당 부분이 “굴러가게 하는 일”에 들어간다는 뜻입니다. 그 시간을 최소로 줄이면서도 나중에 부끄럽지 않을 구성을 찾는 데 몇 주가 걸렸습니다. 지금의 구성과, 왜 이렇게 됐는지를 적습니다.

구성

휴대폰 (Expo / React Native)
   │ HTTPS
   ▼
Cloudflare Workers (Hono, TypeScript)
   │ Hyperdrive (커넥션 풀)
   ▼
Neon PostgreSQL (싱가포르)

세 층이 전부입니다. Redis도, 큐도, 컨테이너도 없습니다. 비용은 Workers Paid 플랜 월 5달러와 도메인 값이 전부이고, 데이터베이스는 무료 플랜 안에 있습니다.

왜 Workers인가, 그리고 한 번 실패한 이야기

처음엔 무료 플랜으로 배포했습니다. 헬스체크도, 일정 목록도, Space도 전부 잘 동작했습니다. 그런데 회원가입만 실패했습니다. 로그를 보니 exceededCpu cpu=10ms. 무료 플랜의 요청당 CPU 한도가 10ms인데, 비밀번호 해시(scrypt)는 그 안에 끝날 수 없습니다. 해시 비용을 낮추면 되지만 그건 보안을 깎아 요금을 아끼는 일이라 하지 않았습니다. Paid로 올리고 나서, 830개 요청의 CPU 시간을 재서 한도를 1000ms로 잡았습니다. 측정된 최대치(약 185ms)의 5배, 기본값(30초)의 30분의 1. 폭주하는 코드를 잡을 만큼은 낮고, 느린 날에도 로그인을 끊지 않을 만큼은 높습니다.

데이터베이스: 읽은 직후에 보여야 한다

Hyperdrive는 기본으로 읽기 쿼리를 캐시합니다. 캘린더 앱에서는 치명적입니다. 방금 만든 일정이 다음 목록 요청에 안 보이면 사용자는 두 번 만듭니다. 그래서 캐시를 끄고(--caching-disabled) 만들었고, 이건 배포 검증 스크립트에 들어 있습니다. 만들고 → 바로 읽고 → 있는지 확인. 이 검사가 실패하면 캐시 설정이 잘못된 것이지 앱이 잘못된 게 아닙니다.

데이터베이스에는 역할이 둘입니다. 마이그레이션을 실행하는 관리자 역할과, 서버가 쓰는 런타임 역할. 런타임 역할은 테이블을 만들거나 지울 수 없고, 다른 역할의 비밀번호를 볼 수 없습니다. 서버 코드에 구멍이 나도 스키마는 지킬 수 있습니다.

잠금은 풀러를 통과해도 잠금이어야 한다

초대 코드 수락, 일정 공유, 빈 시간 공유 켜기. 이 셋은 “구성원 행을 잠그고 → 검사하고 → 쓰기”를 한 트랜잭션 안에서 합니다. 로컬 테스트는 PostgreSQL에 직접 붙어서 통과했지만, 배포된 서버는 Hyperdrive라는 풀러를 거칩니다. 풀러가 트랜잭션을 나눠 버리면 잠금은 의미가 없습니다. 그래서 배포된 API를 상대로 실제 경쟁 상황을 만들어 검사합니다. 초대 코드 하나에 두 사람이 동시에 수락 → 정확히 한 명만 들어와야 합니다. 열두 번 반복해서 한 번도 어긋나지 않는 걸 확인하고서야 “됐다”고 적었습니다.

비밀은 어디에도 적지 않는다

데이터베이스 비밀번호, 인증 시크릿, 스토어 자격증명. 이것들은 채팅에도, 소스에도, 커밋 메시지에도, 문서에도 없습니다. 규칙은 단순합니다. 비밀은 대화식으로만 넣는다. wrangler secret put은 프롬프트에서 받고, 데이터베이스 접속 문자열은 CLI가 셸 변수로 넘기며, 문서에는 자리표시자만 남깁니다. 배포 검증 스크립트조차 쿠키나 접속 문자열을 출력하지 않습니다. 혼자 운영해도 이 규칙은 지킵니다. 오히려 혼자라서, 나중에 실수하면 잡아줄 사람이 없기 때문입니다.

staging과 production

두 환경은 같은 코드, 다른 모든 것입니다. 다른 데이터베이스 프로젝트, 다른 커넥션 풀, 다른 시크릿, 다른 요청 제한 카운터. Worker 이름에 환경 접미사를 일부러 남겨서, 환경을 지정하지 않은 배포 명령이 production을 덮어쓸 수 없게 했습니다. 마이그레이션은 언제나 서버보다 먼저, 앞으로만 갑니다. 되돌리는 건 코드뿐이고, 스키마 실수는 새 마이그레이션으로 고칩니다.

앱

앱은 Expo로 만들고 EAS로 빌드합니다. staging 빌드는 staging API 주소를 빌드 시점에 박아 넣습니다. 런타임에 서버 주소를 바꾸는 기능은 개발용으로만 두고, 배포 빌드에서는 주소가 없으면 그냥 실패하게 했습니다. 잘못된 서버를 조용히 가리키는 앱보다 안 켜지는 앱이 낫습니다.


전부 합쳐서, 혼자서 하루에 한 번 배포하고 잠들 수 있는 구성입니다. 더 필요해지면 그때 더합니다. 지금까지 더 필요했던 적은 없습니다.

Back to the blog