클로드 코드를 활용, 한 줄로 적는 가계부를 만들었습니다 — Moa AI 가계부 빌딩 일지

Spread the love

한 줄로 적는 가계부를 만들었습니다 — Moa AI 가계부 빌딩 일지

“이번엔 진짜 끝까지 가는 가계부”가 만들어진 이유

가계부 앱을 세 번 깔았다가 세 번 다 일주일 만에 지웠다는 사람을 만난 적이 있습니다. 이유를 물어보니 한결같았어요.

“카테고리 누르고, 금액 적고, 날짜 고르고… 커피 한 잔에 세 번씩 눌러야 하니까.”

“그래서 잘 적어두면 뭐가 좋은지” 물어보면 또 한결같았습니다.

“월말에 보면 그냥 숫자만 줄줄. 그래서 다음 달엔 뭘 하라는 건지 모르겠어요.”

이 두 가지가 Moa AI 가계부를 만든 출발점이었습니다. 입력을 한 줄로, 회고를 코치처럼. 그게 처음이자 끝의 목표였어요.


핵심 가치 세 가지

Moa의 첫 화면은 채팅창입니다. 카테고리 선택 버튼이 없어요.

moa365.com UI 1

사용자: 스벅 강남역 6500원
Moa: 기록 완료! 카페에 이번 달 32,400원 썼어요. 평소보다 좀 많네요 :)
      [카페] [6,500원] [강남역]

이 한 줄 입력에 백엔드에서는 Anthropic Claude Haiku가 호출되어 카테고리·금액·날짜·장소를 분리하고, 9개 고정 카테고리 중 하나로 정규화합니다. 영수증 사진을 던지면 OCR이 같은 처리를 합니다.

여기에 두 가지를 더 얹었습니다:

  • 캘린더 + 예정 지출: 월세·구독료 같은 반복지출은 한 번 등록하면 종료일까지 자동으로 캘린더에 채워집니다. “다음 결제일까지 진짜로 남은 돈”이 보입니다.
  • 월간 AI 회고 코칭: 한 달이 끝나면 AI 코치가 지난달과 비교해 “잘한 점”, “아쉬운 점”, “다음 달 한 가지 약속”을 따뜻한 톤으로 정리해줍니다. 잔소리가 아니라 친구의 톤.

대화형 가계부 ai 모아365 moa365 캘린더 대화형 가계부 ai 모아365 moa365 회고


기술 스택 — 왜 이걸 골랐나

영역 선택 이유
백엔드 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.jsxentries / 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 (빌드 시 인라인) / Render GOOGLE_MAPS_SERVER_KEY (런타임)

배운 것: API 키 하나만 쓰면 결국 노출돼서 비용 폭주. 브라우저용/서버용을 분리하고 각각 API 화이트리스트로 막는 게 표준이에요. 그리고 한국에서 Google Maps를 로드하면 지도 출처가 “TMap Mobility”로 표시되는데, Google이 한국 측량 데이터 수출 규제 때문에 SKT TMap과 라이선스 계약해서 그렇답니다. 정상 동작.

대화형 가계부 ai 모아365 moa365 map


가격 정책을 정하면서 — 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 필드가 누락돼 있었고, /planned GET이 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에서 한번 시작해보세요. 이번엔 진짜 끝까지 갈 수 있도록 만들고 있습니다.

🌱 moa365.com

AITREND.KR News Letter

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다