한 줄로 적는 가계부를 만들었습니다 — Moa AI 가계부 빌딩 일지
“이번엔 진짜 끝까지 가는 가계부”가 만들어진 이유
가계부 앱을 세 번 깔았다가 세 번 다 일주일 만에 지웠다는 사람을 만난 적이 있습니다. 이유를 물어보니 한결같았어요.
“카테고리 누르고, 금액 적고, 날짜 고르고… 커피 한 잔에 세 번씩 눌러야 하니까.”
“그래서 잘 적어두면 뭐가 좋은지” 물어보면 또 한결같았습니다.
“월말에 보면 그냥 숫자만 줄줄. 그래서 다음 달엔 뭘 하라는 건지 모르겠어요.”
이 두 가지가 Moa AI 가계부를 만든 출발점이었습니다. 입력을 한 줄로, 회고를 코치처럼. 그게 처음이자 끝의 목표였어요.
핵심 가치 세 가지
Moa의 첫 화면은 채팅창입니다. 카테고리 선택 버튼이 없어요.
이 한 줄 입력에 백엔드에서는 Anthropic Claude Haiku가 호출되어 카테고리·금액·날짜·장소를 분리하고, 9개 고정 카테고리 중 하나로 정규화합니다. 영수증 사진을 던지면 OCR이 같은 처리를 합니다.
여기에 두 가지를 더 얹었습니다:
- 캘린더 + 예정 지출: 월세·구독료 같은 반복지출은 한 번 등록하면 종료일까지 자동으로 캘린더에 채워집니다. “다음 결제일까지 진짜로 남은 돈”이 보입니다.
- 월간 AI 회고 코칭: 한 달이 끝나면 AI 코치가 지난달과 비교해 “잘한 점”, “아쉬운 점”, “다음 달 한 가지 약속”을 따뜻한 톤으로 정리해줍니다. 잔소리가 아니라 친구의 톤.
기술 스택 — 왜 이걸 골랐나
| 영역 | 선택 | 이유 |
|---|---|---|
| 백엔드 | FastAPI + SQLAlchemy 2.0 (async) | 빠른 프로토타이핑 + 타입 안정성 |
| AI | Claude Haiku 4.5 | 분류·요약이 주 작업이라 가격 효율 우선 |
| DB | PostgreSQL (Render) | Hobby 플랜에서 무료, 마이그레이션 표준화 |
| 프론트 | React 18 + Vite + TypeScript | SPA + 모바일 우선 |
| 스타일 | Tailwind CSS | 컴포넌트 일관성 확보가 빠름 |
| 다국어 | i18next, 9개 locale | 처음부터 글로벌 잠재력 염두 |
| 결제 | Toss + Paddle (예정) | 국내 + 해외 분리 처리 |
처음엔 Railway에 백엔드를 띄웠지만 광범위 장애 한 번 겪고 Render로 이전했습니다. 1인 개발자에게 인프라 단일 장애점은 치명적이라는 걸 그때 배웠어요.
가장 크게 데였던 P0 사고
런칭 초기, 어떤 사용자가 새로고침 한 번에 한 달 치 가계부가 통째로 날아갔다고 보고를 보냈습니다. 식은땀이 쫙.
원인은 단순했습니다. LedgerChat.jsx가 entries / planned / reflections / income / budget / photos 를 브라우저 useState에만 들고 있었던 것. 백엔드 CRUD 엔드포인트는 다 있었지만 프론트가 연결을 안 했어요. 새로고침 = 메모리 초기화 = 데이터 손실.
수습 작업:
- TanStack Query로 모든 데이터를 백엔드와 연결
- Optimistic update + onError rollback 패턴 도입
- 사진 업로드도 base64 in-memory → multipart upload로 전환
그 이후로 “데이터는 무조건 서버”가 1순위 원칙이 됐습니다. 1인 SaaS에서는 사용자 한 명의 데이터 손실이 평판 회복 불가 수준의 사고이기 때문이에요.
9개 언어 지원 — “글로벌”이 아니라 “한국 거주 외국인”이 시작이었다
처음엔 한국어만 만들었습니다. 그러다 베트남 사용자에게 “월급 들어오면 어디로 사라지는지 모르겠다” 메시지를 받았어요. 곰곰이 생각해보니 한국에는 한국인 사용자만 있는 게 아니더라구요. 그래서 풀어버렸습니다.
ko / en / ja / zh / es / th / vi / ms / hi — 9개 locale.
기술적으로는 i18next로 키 분리, 통화·국가·언어를 회원가입 시 한 번에 잡고 마이페이지에서 라이브 프리뷰로 바꿀 수 있게 했습니다. AI 프롬프트도 9개 locale별로 분기해서 응답이 자연스럽게 나오도록 시스템 프롬프트를 다듬었어요.
이때 깨달은 것: i18n은 처음부터 안 하면 나중에 한다는 게 거짓말이다. 한 번 한국어로 굳어진 컴포넌트를 푸는 데 든 시간이 처음부터 i18n으로 짰을 때보다 3배 정도 더 들었습니다.
갤럭시 S25 Ultra가 알려준 모바일 헤더의 진실
어느 날 사용자 스크린샷이 도착했습니다. 412dp 화면에서 H1 제목과 우상단 부동 메뉴 5개가 가로로 겹쳐 있었어요. “absolute top-3 right-4” 로 부동시킨 게 데스크탑에선 멀쩡한데, 모바일에선 폭 280-300px를 잡아먹어 제목 위에 올라타버린 거예요.
기존 패턴 (absolute 부동 5개 아이콘) → 모바일 하이브리드로 재설계:
- 상단: sticky bar (타이틀 + 3-dot)
- 3-dot 탭: 우측 슬라이드 drawer 메뉴
- 하단: BottomTabBar 4탭 (홈 / 캘린더 / 반복 / MY)
- safe-area inset 대응 (
viewport-fit=cover+env(safe-area-inset-*))
데스크탑은 기존 부동 버튼 그대로 두고, variant="absolute" / variant="inline" 두 모드로 분기했어요.
배운 것: 반응형은 width 분기 코드가 아니라 인터랙션 패턴 자체를 바꿔야 할 때가 많다.
Google Maps 통합 — 비용 vs 보안의 줄타기
“장소 후기” 기능을 만들면서 지도 연동이 필요해졌습니다. 처음엔 OpenStreetMap Nominatim 무료로 갔지만, 한국 정확도가 떨어졌어요.
최종 구성:
- 백엔드: Google Geocoding 우선 → 실패 시 Nominatim 폴백 (이중 안전망)
- 프론트: Maps JS API + Places (New) — Advanced Markers
- 키 분리: 브라우저 키 (HTTP referer 제한 + Maps JS+Places만) / 서버 키 (제한 없음 + Geocoding만)
- 환경변수: Vercel
VITE_GOOGLE_MAPS_API_KEY(빌드 시 인라인) / RenderGOOGLE_MAPS_SERVER_KEY(런타임)
배운 것: API 키 하나만 쓰면 결국 노출돼서 비용 폭주. 브라우저용/서버용을 분리하고 각각 API 화이트리스트로 막는 게 표준이에요. 그리고 한국에서 Google Maps를 로드하면 지도 출처가 “TMap Mobility”로 표시되는데, Google이 한국 측량 데이터 수출 규제 때문에 SKT TMap과 라이선스 계약해서 그렇답니다. 정상 동작.
가격 정책을 정하면서 — 1등을 따라갈 것인가, 비킬 것인가
국내 1등 가계부 앱은 월 2,900원 / 연 24,000원입니다. 매우 저렴해요. 무료 티어에 광고를 표시하는 비즈니스 모델.
Moa의 처음 가격은 월 5,400원이었습니다. 1.86배 비싸요. 경쟁력 없음 명확.
세 가지 옵션을 놓고 고민했습니다:
- A. 직접 경쟁 (월 2,900원, 광고 모델) — 광고 인프라 구축 비용 큼, Moa의 깔끔한 디자인 톤과 안 어울림
- B. AI 차별화 (월 3,900원, 광고 없음) — Moa의 강점인 AI에 가격 정당화 ✅ 선택
- C. 3-tier (Free + 일회성 + 구독) — 머니매니저 패턴, 복잡함
옵션 B 선택. 이유는 단순했어요. 광고 없는 미니멀한 UI가 Moa의 시그니처인데, 무료 티어에 광고를 끼우면 그 자체가 모순이 됩니다.
베타 유저에게는 첫 12개월 50% 할인 (월 1,950원 / 연 14,500원). 일찍 와주신 분들에 대한 작은 감사.
약관과 개인정보처리방침 — 1인 SaaS의 진짜 통과의례
여기서 가장 많이 시간이 들었습니다. 한국 「개인정보 보호법」, 「전자상거래 등에서의 소비자보호에 관한 법률」, 「통신비밀보호법」, 「신용정보법」 — 다 읽어야 했어요.
특히 AI를 쓰는 SaaS는 일반 가계부 앱이 안 다루는 영역이 있습니다:
- 외부 AI 모델 제공사에 데이터를 보내는데 그건 “위탁”인가, “국외이전”인가?
- AI 모델 학습에 우리 사용자 데이터가 들어가는가?
- 사용자가 AI 기능을 거부할 권리가 있는가?
결론적으로 약관 18조 + 개인정보처리방침 12조 구조로 정리하고, 위탁·국외이전 표에는 Anthropic, Resend, Google, Render, Vercel, Toss, Paddle, Cloudflare 8개 수탁자를 모두 명시했습니다. 각각의 수탁업무, 이전국가, 전송항목, 보유기간을 한 줄씩.
운영 주체도 명확히 했습니다:
- 상호: ㈜에이티엠스토어
- 사업자등록번호: 396-21-02113
- 통신판매업 신고: 2025-****-0174
- 주소: 경기도 **시 **로 257길
이 정보를 config/company.ts 단일 상수 파일에 모아두고, 푸터·약관·개인정보·결제 페이지에서 모두 이걸 참조하도록 했어요. 한 군데 바꾸면 전 앱이 따라옵니다.
배운 것: 법적 문서는 카피해서 쓰면 안 된다. 다른 앱의 약관 구조는 참고용일 뿐, 내 서비스의 실제 데이터 흐름에 맞춰 다시 써야 해요. 특히 AI 처리 부분은 일반 가계부 앱 약관에 없는 영역이라 직접 신설해야 했습니다.
운영하면서 깨달은 작은 디테일들
- 비밀번호 재설정 이메일이 5분 늦게 도착: 코드 문제가 아니라 Resend → Gmail 라우팅 + 신규 도메인 reputation 검사 + Render Hobby의 cold start 합쳐진 결과. 1-2분은 정상 범위.
- FAQ 안내가 실제 기능과 안 맞음: “비밀번호 잊었어요”에 “v1.5에 추가 예정”이라고 적혀 있었는데, 이미
/forgot-password라우트가 동작 중이었어요. 안내 따로, 기능 따로 굴러갈 수 있다는 것. - 반복지출이 2개월 후 멈춤: AI parse JSON 스키마에
recurrence_until필드가 누락돼 있었고,/plannedGET이 month 파라미터 없이 호출되면 expansion을 안 했고, 캘린더가 매월 fetch를 안 하고 있었어요. 3중 버그가 같은 증상으로 보였다는 것. - Render Hobby는 정적 outbound IP 없음: API 키에 IP 제한 못 검. 대신 API 화이트리스트 + 키 분리 + 결제 알람 3중 방어로 대체.
지금까지의 결과
- 8개 분리 커밋 (의미 단위로 깔끔하게)
- 9개 locale 다국어 풀 커버
- 30개국 통화 자동 매핑
- 18조 약관 + 12조 처리방침 PIPA 준수
- 2개 무료 배포 (Render + Vercel) + Cloudflare R2 (사진 저장 예정)
기능적으로는 — 채팅 입력, 영수증 OCR, 캘린더 + 반복지출, 장소 핀 + 후기 (Google Maps), 월간 AI 회고 코칭, 대차표, 9개 언어. 가격은 무료 + 프리미엄 월 3,900원.
다음 큰 과제는 비공개 문의 게시판 폼입니다. master@aitrend.kr 이메일을 노출하는 대신, 폼 제출 → Google Sheets 저장 → 관리자 검토 → AI 봇 자동 응답 초안 → 사람이 최종 수정 후 발송. 1인 운영자 효율 극대화가 목표예요.
1인 SaaS를 만들고 있는 누군가에게
가장 큰 깨달음은 이거였어요.
“잘 만든 코드보다, 사용자가 한 줄 적고 멈추지 않는 게 더 중요하다.”
P0 데이터 손실 사고, 모바일 헤더 충돌, 다국어 누락, 가격 책정, 법무 문서 — 이 모든 게 한 명의 사용자가 “어, 이거 못 쓰겠네”라고 떠나는 걸 막기 위한 디테일이었습니다. 코드가 아니라 사용자 경험을 만드는 일이라는 걸 매번 다시 배워요.
가계부를 N번째 시도하는 분이 있다면, Moa에서 한번 시작해보세요. 이번엔 진짜 끝까지 갈 수 있도록 만들고 있습니다.




