
Next.js + Supabase + Cloudflare Workers로 '조용한 AI 동반자'를 만든 이야기
요약
Next.js, Supabase, Cloudflare Workers를 활용하여 부업과 다이어트 기록을 하나의 캐릭터 성장 시스템으로 통합한 'ALTER' 앱 개발 사례를 소개합니다. 압박감을 주는 기존 습관 관리 앱과 달리, AI가 사용자의 행동을 관찰하여 직업을 판정하는 '재촉하지 않는' 설계가 특징입니다.
핵심 포인트
- Next.js, Supabase, Cloudflare Workers 기반의 PWA 구현
- 부업과 다이어트 데이터를 캐릭터의 HP/MP/EXP로 통합 설계
- AI를 활용한 동적인 직업 판정 시스템 도입
- 랭킹과 스트릭 페널티를 제거한 사용자 친화적 UX 설계
1년 몇 개월 전, 저는 이 포스트를 보았습니다.
정말 살이 빠질 것 같은 게임 × 부업 × 피트니스 앱 같은 것을 AI로 만들고 싶다
— Sako Yuki (@sako_brain) https://x.com/sako_brain/status/2054790607076499585
부업 엔지니어에게 다이어트와 부업은 '둘 다 지속하기 어려운 것'의 대표격이었습니다. 식사는 식단 앱, 운동은 운동 앱, 부업의 진척도는 노트에 수기로 기록. 기록이 제각각이면 어디선가 "오늘은 이만하자"라는 마음이 섞이게 되고, 정신을 차려보면 양쪽 모두 멈춰 있습니다. 저 또한 몇 번이고 같은 일을 반복해 왔습니다.
Sako 씨의 이 한마디가, 흩어져 있던 기록을 "하나의 캐릭터로 집약한다"라는 발상으로 바꾸어 주었습니다. 거기서부터 진지하게 설계하여 만든 것이 본 기사에서 소개하는 ALTER라는 앱입니다. 포스트 인용에 대해서는 개발 초기에 본인에게 답글(Reply)로 한마디 전해 두었습니다.
ALTER는 부업의 commit과 다이어트의 걸음 수를 동일한 캐릭터의 HP / MP / EXP로 올리는 Web 앱입니다. Next.js + Supabase + Cloudflare Workers 기반의 PWA로, iPhone 홈 화면에 두면 iOS 앱처럼 작동합니다.
기능을 한마디로 요약하면 다음과 같습니다.
식사는 MP, 운동은 HP, 흐름 시간(Flow time)은 EXP, 부업의 움직임은 CHA.
있을 법하면서도 없는 것이, 부업 × 다이어트라는 두 축을 하나의 캐릭터로 집약하는 설계였습니다. Asuken은 식단만, Strava는 운동만, Habitica는 습관만 관리합니다. 두 축을 서로 다른 앱으로 관리하는 고통은 부업 엔지니어라면 익숙한 감각일 것입니다.
차별화의 핵심은 3가지입니다.
- 부업 × 다이어트를 하나의 캐릭터로 (경쟁자가 거의 없는 영역)
- 행동이 직업을 결정하는 동적인 AI 판정 (스스로 "선택"하는 것이 아니라, 지속한 행동으로부터 "보여지는" 것)
- Habitica에서 좌절한 사람들을 위한, 재촉하지 않는 트래커
직업은 6가지입니다. 검사 / 마법사 / 검사(檢士) / 상인 / 연금술사 / 비서. 최근 7일간의 행동을 AI가 관찰하여 판정하며, 월마다 바뀌어도 괜찮은 설계로 만들었습니다. 판정이 확정되기 전까지는 '견습생'으로서, 중립적인 동반자가 곁에 있습니다.
ALTER의 근간에 두는 4줄의 문장이 있습니다.
"조용한 미래"
- 전투보다 발전
- 태스크보다 성장
- 승패보다 지속
기술 선정도 코드 설계도, 모두 이 4줄에서 파생되었습니다. 이것은 추상론이 아니라, Habitica나 Duolingo에서 제가 좌절했던 경험에 대한 직접적인 응답입니다.
Habitica에서는 퀘스트의 산에 짓눌려 결국 앱을 열지 않게 되었습니다. "하지 않으면 HP가 줄어든다", "스트릭(Streak)이 끊기면 쌓아온 것이 제로로 돌아간다"라는 압박감은, 계속하고 싶은 사람일수록 계속할 수 없게 만듭니다. 원인과 결과가 역전되어 있다고 느꼈습니다.
그래서 ALTER의 브랜드 프로미스(Brand Promise)는 "재촉하지 않는다, 경쟁시키지 않는다, 서두르게 하지 않는다"로 정했습니다. 구체적으로는 다음과 같은 설계입니다.
- 랭킹 없음
- 스트릭 종료에 따른 페널티 없음
- 리그 강등 없음
- "LEVEL UP!"의 화려한 플래시 효과 없음
대신, 이어온 발걸음을 조용히 기록합니다. 레벨이 올라도 새싹이 살짝 피어나는 정도의 연출에 그치며, 다이어트의 체중 변화도 "달성"이 아니라 "건강한 범위의 유지"를 찬양하는 방향으로 했습니다.
솔직히 말하면, 이것은 운영 측면에서 리스크가 큰 선택이었습니다. 랭킹도 스트릭도 지속률을 높이기 위해 가장 저렴하게 효과를 볼 수 있는 장치입니다. 그것을 전부 제외한다면, 대신 사람을 살며시 붙잡아둘 "무언가"를 직접 준비해야 합니다. 제가 건 것은 숫자로 재촉하는 대신 배치한, 조용히 쌓여가는 기록과 재촉하지 않는 동반자였습니다. 따라서 "재촉하지 않는다"는 친절함을 가장한 포즈가 아니라, 외압을 일절 사용하지 않고 지속을 지탱할 수 있는가라는 설계상의 도전 그 자체입니다.
이후의 기술적인 이야기 — 아키텍처, AI 모델 선정 방법, 동반자의 말투 제어 — 는 모두 이 4줄을 어떻게 코드로 구현할 것인가에 대한 궁리들의 집합입니다. 그중에서도 AI 동반자 레이어가 이 사상을 가장 짙게 반영하고 있습니다. 다음 장부터 구현 내용을 살펴보겠습니다.
ALTER는 "개인 개발이 월 0엔부터 시작할 수 있는 구성"을 최우선으로 구축했습니다. 전체 모습은 다음과 같습니다.
역할 분담은 심플합니다.
- Next.js 16 (App Router / Server Actions) on Vercel: UI와 API의 본체. Server Actions를 통해 DB 쓰기까지 완결하며, Serwist로 PWA(Progressive Web App)화하여 iPhone 홈 화면에 배치할 수 있도록 했습니다.
- Supabase (PostgreSQL + Auth + RLS): 데이터와 인증. Row Level Security (RLS)를 통해 '자신의 행만 읽을 수 있음'을 DB 계층에서 보장하여, 앱 측의 인가(Authorization) 실수가 사고로 직결되지 않도록 했습니다.
- Cloudflare Workers Cron: 아침/저녁 메시지 생성 및 외부 데이터 가져오기를 위한 정기 실행 역할. Vercel이 아닌 Cloudflare (CF)를 선택한 이유는 제7장에서 다루겠습니다.
- 외부 AI: 일간 메시지는 Groq의 Llama 3.3 70B, 핵심적인 장면은 Claude를 사용합니다. 이 구분법이 다음 장의 주제입니다.
왜 이 조합인가? Vercel Hobby, Supabase Free, Cloudflare Workers Free 모두 개인 개발 규모라면 무료 범위 내에 들어오며, 고정비 제로로 프로덕션을 운영할 수 있기 때문입니다. 과금이 발생하는 것은 Claude / Groq의 종량제 비용과 Stripe 수수료 정도이며, 사용자가 늘어나기 전까지는 거의 사비가 들지 않습니다. 기술 선정 단계부터 '서두르지 않고 계속할 수 있음'을 운영하는 개발자 자신에게도 적용한 형태입니다.
AI를 내세우면 원가가 그대로 적자로 직결됩니다. ALTER는 'Free 플랜에서도 AI가 제대로 대화할 수 있음'과 'Pro 플랜에서도 원가가 폭주하지 않음'을 양립시키기 위해, tier × 용도 × 월간 예산이라는 3가지 축으로 모델을 해결하고 있습니다. 그 중심에는 이 순수 함수(Pure Function)가 있습니다.
// lib/ai/provider.ts (발췌)
export function resolveProvider(args: {
tier: SubscriptionTier;
...
흐름을 정리하면 다음과 같습니다.
판단의 배경 3가지:
- Free 플랜의 일간 메시지는 Groq (Llama 3.3 70B). 무료 범위가 넓고 빠릅니다. 장애가 발생할 때만 Claude Haiku로 전환하여, 무료 사용자에게도 동반자가 침묵하는 일이 없도록 합니다.
- Pro / Founders 플랜의 일간 메시지도 Haiku. 아침 코멘트나 일간 스코어는 빈도가 높기 때문에, 이를 Sonnet으로 설정하면 원가 부담이 커집니다. Haiku의 입출력 단가는 Sonnet의 약 1/3입니다 (구현된 원가표 기준 Haiku = $1 / $5, Sonnet = $3 / $15 per 1M tokens).
- 핵심적인 장면(주간 리포트, 월간 사용 설명서, 직업 판정)에만 Sonnet 4.6 사용. 빈도가 낮으면서 품질이 곧 경험이 되는 곳에 예산을 집중합니다.
폭주 방지 대책도 마련되어 있습니다. Pro / Founders 플랜에는 **월간 원가 소프트 캡(Soft Cap, 사용자당 600엔)**이 있어, 이를 초과하면 리포트(report) 기능조차 자동으로 Haiku로 강등됩니다. 통상적인 이용(주 4회 + 월 1회)이라면 수십 엔 수준에서 끝나므로 도달하기 어려운 안전망이지만, 예상치 못한 사용 패턴이 나타나더라도 적자가 무한정 커지지 않는 구조입니다. '서두르지 않게 한다'는 원칙을 비용 관리 측면의 안심 장치로도 가져가고 싶었습니다.
이 방침이 탁상공론이 되지 않도록, AI 호출 시마다 모델, 입출력 토큰, 예상 원가를 api_usage_logs와 PostHog에 이중으로 기록합니다. 소프트 캡 판정 역시 당월의 실제 비용 합계를 DB에서 가져와 수행하는 실측치 기반입니다. resolveProvider()를 부작용(Side Effect)이 없는 순수 함수로 만든 이유도 이 때문이며, 이를 통해 '어떤 tier의, 어떤 용도에서, 예산 초과 시, 어떤 모델이 출력되는지'를 네트워크나 DB를 건드리지 않고도 유닛 테스트(Unit Test)로 모든 패턴을 고정할 수 있습니다. 원가를 예측할 수 있기에 Free 플랜을 망설임 없이 무료로 제공할 수 있습니다. 비용 설계와 '서두르지 않게 한다'는 철학은 여기서 하나로 이어집니다.
'서두르지 않게, 경쟁시키지 않게, 재촉하지 않게'를 동반자의 말투로 녹여내는 것이 이 계층의 역할입니다. 세 가지 장치를 통해 구현했습니다.
① 7가지 인격을 정적인 TS(TypeScript) 모듈로 고정한다. 내비(수습 기간)를 포함한 6가지 직업의 동반자를 각각 시스템 프롬프트(System Prompt)가 포함된 모듈로 보유합니다. 런타임에 생성하지 않고 고정되어 있으므로, 리뷰와 차분 관리(Diff Management)를 코드와 동일한 선상에서 수행할 수 있습니다. 대표적으로 내비(Navi)의 정의는 다음과 같습니다.
// lib/ai/companions/navi.ts (발췌)
export const NAVI_COMPANION: CompanionConfig = {
characterClass: null,
...
② 날씨 × 진화 단계에 따라 말투를 동적으로 변경한다. 동일한 인격이라도, 진화 단계(견습 / 숙련 / 달인)와 '오늘의 컨디션(날씨)'에 따라 system prompt의 끝에 수식어를 추가합니다.
이 부분이 철학이 반영되는 지점입니다. 예를 들어 '비가 계속되는(며칠간 피로가 쌓인)' 날에는 말투를 한 단계 더 부드럽게 하고, 전진을 재촉하는 자극적인 말을 하지 않도록 명시적으로 제한합니다. 전환점을 맞이한 '무지개'가 뜨는 날이라 할지라도, 축하는 평소 말투의 범위 내로 유지합니다.
③ tone-check로 자극적인 표현을 기계적으로 차단한다. 생성 결과는 사용자에게 전달하기 전에 순수 함수 (Pure Function)로 검사합니다.
// lib/ai/tone-check.ts (발췌)
const NG_WORD_RE = /頑張|最高|素晴/;
const IMPERATIVE_RE = /(しろ|なさい|すべき|なければ)/;
...
프롬프트에서 금지하고, 출력 단계에서 한 번 더 검사하는 이중 구조입니다. 체중을 칭찬하는 말이나, 불안·초조함을 "극복해라"라며 부정하는 말까지 걸러냅니다. 결과적으로 동일한 사용자가 아침에 접속하더라도, 맑은 날과 비 오는 날에 돌아오는 말의 온도가 조용히 변합니다. 화려한 연출이 아닌, 이 '온도의 제어'야말로 ALTER 동반자의 핵심이었습니다.
ALTER는 아침저녁 메시지 생성이나 외부 데이터 가져오기 등, 정기적으로 호출하고 싶은 API가 십여 개 있습니다. 그런데 Cloudflare Workers Free tier의 Cron Triggers는 계정 (Account) 단위로 5개까지입니다. 제 계정은 다른 Worker에서 3개를 사용하고 있어, ALTER에 할당할 수 있는 것은 2개뿐이었습니다.
그래서 2개의 스케줄 (Schedule)로 모든 엔드포인트 (Endpoint)를 팬아웃 (Fan-out) 하는 디스패처 (Dispatcher)를 하나의 함수로 집약했습니다.
// cloudflare/cron-worker/src/index.ts (발췌)
function resolvePaths(cron: string, now: Date): string[] {
if (cron === '*/5 * * * *') {
...
핵심은 요일·월초 판정을 resolvePaths() 내부에만 닫아두었다는 점입니다. Cron 할당량은 2개로 고정한 채, 엔드포인트의 증감이나 주간 / 월간 구분은 이 단일 함수를 편집하는 것만으로 완결됩니다. 제약(5개까지)을 역이용하여 오히려 Cron 구성이 단순해졌습니다. Worker 측은 받은 경로 (Path)를 순차적으로 fetch하며, 각 엔드포인트에는 Authorization: Bearer ${CRON_SECRET}를 붙여 Vercel의 API를 보호하고 있습니다.
ALTER는 2026-06-03에 일반 공개합니다.
Free: ¥0— 6개 직업 판정 + AI 동반 (Groq). 이것만으로도 충분히 사용할 수 있습니다 -
Pro: ¥980/월— AI 이미지 생성 + 상세 인사이트 (Insight) + 90일 수라의 길 -
Founders: ¥500/월 (영구 고정, 50 슬롯 한정)— Pro의 모든 기능을 반값에 -
Founders 슬롯이 모두 차면 자동으로 Pro (¥980/월)로 전환됩니다. 한 번 가입하면 요금이 인상될 기한은 없습니다. 공개일에는 "남은 슬롯이 몇 개인지"를 /pricing에서 실시간으로 확인할 수 있도록 준비해 두었습니다.
- ALTER LP: https://alter.ponfreelance.com
- 모티브가 된 사코(Sako) 님의 포스트: https://x.com/sako_brain/status/2054790607076499585
사코 님, 감사합니다.
저자: 폰 (@pon_freelance)
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기