이유식 큐브를 관리하는 앱, '큡큡'을 만들었습니다
냉동실에 쌓여가는 이유식 큐브의 재고·유통기한·급여 기록·알레르기 테스트를 한 곳에서. 부부가 함께 쓰는 앱의 기획부터 배포까지
냉동실에 쌓여가는 이유식 큐브, 언제 만들었고 몇 개 남았는지 매번 헷갈리시죠. 그 불편을 풀어보려고 직접 앱을 만들었습니다. 기획부터 배포까지 그 과정을 정리합니다.
큐브 이유식이란?
이유식을 만들 때 매 끼니 새로 조리하긴 어렵습니다. 그래서 많은 부모가 재료(소고기·애호박·당근 등)를 미리 갈아 익힌 뒤 얼음틀에 소분해 얼립니다. 이 얼린 한 칸이 바로 '큐브' 입니다. 끼니마다 필요한 큐브를 몇 개씩 꺼내 데우고 조합해 한 그릇을 만듭니다.
문제는 이 큐브들의 관리입니다.
- 이 큐브, 만든 지 며칠 됐지? (냉동도 보관 기한이 있습니다)
- 소고기 큐브 몇 개 남았더라?
- 오늘 처음 준 재료, 알레르기 반응은 없었나?
- 한 끼에 뭘 몇 그램 먹였는지 기록이 안 남는다
큡큡은 이 큐브의 재고·유통기한·급여 기록·알레르기 테스트를 한곳에서 관리하는 앱입니다.
왜 만들었나
큡큡은 우리 집의 실제 상황에서 출발했습니다. 이유식을 만드는 사람과 먹이는 사람이 달랐거든요.
- 나(엄마) : 주말, 퇴근 후 몰아서 큐브를 만드는 사람
- 남편(아빠, 육아휴직 중) : 평일에 그 큐브를 조합해 먹이는 사람
역할이 나뉘면 두 사람의 정보가 어긋납니다.
- 재고는 먹이는 사람만 안다 : "냉동실에 큐브가 몇 개 남았는지" 는 만드는 내가 모르고 먹이는 남편이 압니다.
- 식사 이력도 먹이는 사람만 안다 : "오늘 아기가 뭘 먹었는지" 도 남편만 압니다. 그래서 나는 재고를 몰라 같은 재료를 또 만들거나, 다 떨어진 걸 뒤늦게 알게 됩니다.
종이나 메모 앱으로는 두 사람의 정보를 실시간으로 맞출 수 없었습니다. 그래서 "만드는 사람과 먹이는 사람이 같은 데이터를 공유한다" 를 첫 번째 목표로 앱을 만들었습니다.
기능
한 문장으로: 엄마가 큐브를 채우고, 아빠가 꺼내 먹이며 기록하면, 재고·식사 이력·알레르기 기록이 둘 사이에서 자동으로 맞춰집니다.
| 기능 | 하는 일 |
|---|---|
| 🧊 큐브 관리 | 재료를 카테고리·죽 농도·월령 단계에 맞춰 등록하고 재고 추적 |
| 🍽️ 이유식 기록 | 한 끼에 쓴 큐브를 고르면 총량 계산 + 잔여 수량 자동 차감, 실제 먹은 양 별도 기록 |
| ⚠️ 알레르기 테스트 | 새 재료 도입 시 결과(정상/반응)·증상 기록, 재료별 시도 이력 추적 |
| 💡 AI 이유식 도우미 | Claude 기반 챗봇에 손질법·월령별 정보·알레르기 질문 (하루 10회) |
| 👨👩👧 가족 공유 | 초대 링크로 배우자를 같은 아기에 연결, 실시간 동기화 |
| 🔔 푸시 알림 | "큐브 만든 지 11일 됐어요", "당근 4개 남았어요" 등 자동 알림 |
| 📱 iOS 네이티브 | 공유 시트·햅틱 등 네이티브 경험 |
아키텍처
전체 구조
하나의 Next.js 코드베이스를 Vercel에 배포하고, iOS 앱(Capacitor)은 그 배포 URL을 WebView로 감싸 로드합니다. 일반 데이터 조회·저장은 브라우저가 Supabase를 직접 부르고, 로그인·초대·AI 호출처럼 민감하거나 서버 로직이 필요한 일은 /api/* 서버 라우트를 거칩니다.
데이터베이스: Supabase
브라우저가 DB를 직접 부른다는 것
전통적인 시스템은 브라우저 → 백엔드 서버 → DB 3계층입니다. 브라우저는 DB를 모르고 서버 API만 부르며, 권한 검사는 서버 코드가 합니다. 반면 Supabase(BaaS)는 백엔드 서버를 생략하고 브라우저가 DB를 직접 부릅니다.
| 전통적 3계층 | Supabase(BaaS) | |
|---|---|---|
| 구조 | 브라우저 → 서버 → DB | 브라우저 → DB |
| DB 직접 호출 | ❌ 서버만 | ✅ 브라우저가 직접 |
| 권한 검사 | 백엔드 서버 코드 | DB의 RLS |
| RLS 필요성 | 낮음 (생략 잦음) | 필수 (유일한 방어선) |
개발이 빨라지는 대신(1인 개발에 큰 장점), 브라우저에 DB 접근 키(anon key)가 들어갑니다. 브라우저 코드는 사용자가 조작할 수 있으니 "서버가 막아주겠지"를 기대할 수 없고, 그 방어 책임이 DB 자신(RLS) 으로 내려갑니다. 그렇기 때문에 RLS는 "있으면 좋은 것"에서 "없으면 큰일 나는 것" 이 됩니다.
RLS(Row Level Security, 행 단위 보안) 는 "이 행에 지금 요청한 사람이 접근할 자격이 있는가"를 DB가 직접 판단하는 기능입니다. 그리고 RLS는 Supabase만의 기능이 아닙니다. RLS는 PostgreSQL의 기능이고, Supabase가 PostgreSQL을 쓰기 때문에 따라오는 것뿐입니다. Oracle(VPD), SQL Server(2016+)에도 비슷한 행 단위 보안이 있습니다. 다만 MySQL·SQLite·MongoDB에는 내장 RLS가 없어 보통 뷰나 앱 코드로 흉내 냅니다. Supabase에서 RLS가 특히 중요한 이유는, 브라우저가 DB를 직접 호출해 중간에 서버가 권한을 거를 기회가 없어 DB의 RLS가 사실상 유일한 방어선이기 때문입니다.
권한 모델과 RLS 정책
큡큡의 모든 데이터는 baby_members(아기-보호자 매핑)에 멤버로 등록된 사람만 접근할 수 있고, 이를 RLS 정책으로 강제합니다.
-- 큐브는 해당 아기의 멤버만 조회 가능
CREATE POLICY "cubes_select" ON public.cubes
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.baby_members
WHERE baby_id = cubes.baby_id
AND user_id = auth.uid() -- 지금 로그인한 사람
)
);앱 코드가 깜빡 필터를 빠뜨려도 DB가 남의 아기 데이터를 막아주는 구조입니다.
데이터 모델
핵심은 한 명의 아기에 여러 보호자가 붙을 수 있다는 것입니다. users(보호자)와 babies(아기)를 baby_members가 N:M으로 잇고, 여기에 OWNER / MEMBER 역할을 둡니다. 아기에 딸린 cubes·feeding_records·allergy_records는 모두 이 멤버십을 기준으로 접근이 통제됩니다.
users // 보호자 (카카오/Apple 연동, 알림 설정)
└─ baby_members // 보호자 ↔ 아기 N:M 매핑 (OWNER / MEMBER)
└─ babies // 아기 (이름, 생년월일)
├─ cubes // 재료 큐브 (재고·유통기한)
├─ feeding_records // 이유식 급여 기록
└─ allergy_records // 알레르기 테스트 기록
invite_tokens // 가족 초대 링크 토큰 (24h 만료)
chat_logs // AI 채팅 로그 (90일 후 자동 삭제)
push_subscriptions // Web Push 구독 정보
인증, 카카오 로그인
왜 카카오였나
국내 사용자에게 가장 간편한 로그인 수단이기 때문입니다. 별도 회원가입·비밀번호 없이 카카오 계정 한 번으로 시작할 수 있어 진입 장벽이 낮습니다.
그런데 Supabase 기본 카카오 OAuth를 쓰지 않고 직접 구현했습니다. Supabase OAuth는 사용자를 식별할 때 이메일을 핵심 키처럼 쓰는데, 카카오는 이메일을 안 주는 경우가 많기 때문입니다(이메일 제공은 카카오의 선택 동의 항목). 그래서 카카오 ID로 가짜 이메일·비밀번호를 결정론적으로 계산해, 카카오 로그인을 Supabase의 "이메일/비밀번호 로그인"으로 변환하는 자체 플로우를 만들었습니다.
서버 호출 프로세스
사용자 ──클릭──▶ 카카오 ──인가코드(code)──▶ 서버 /api/auth/kakao-callback
서버 ──코드로 토큰 교환──▶ 카카오 ──access_token──▶ 서버
서버 ──유저 정보 요청──▶ 카카오 ──kakao_id·닉네임·프로필──▶ 서버
│
│ kakao_id로 가짜 이메일 + 해시 비밀번호 "계산"
│ (항상 같은 값 = 결정론적)
▼
서버 ──signInWithPassword──▶ Supabase Auth
├─ 성공 → 기존 회원
└─ 실패 → admin.createUser (계정 생성) 후 재로그인 ← 신규 회원
서버 ──users 테이블에 프로필 upsert──▶ Supabase
▼
세션 발급 · 홈으로 이동
핵심은 "결정론적", 같은 카카오 ID면 항상 같은 이메일·비밀번호가 나오므로, 비밀번호를 저장하지 않고 로그인할 때마다 다시 계산합니다. 흐름은 "일단 로그인해보고, 안 되면 가입" 입니다.
// 카카오 ID → 항상 똑같은 가짜 이메일/비밀번호 (결정론적)
makeKakaoEmail(id) // → kakao_123456@kakao.babycube.internal
makeKakaoPassword(id) // → sha256(`${id}:${salt}`) ← 저장 안 해도 매번 재현Apple 로그인도 추가하면서, 한 사람이 카카오/Apple 양쪽으로 로그인해도 같은 계정에 연결되도록 계정 연결 기능을 양방향으로 구현했습니다.
호출 방식 / 데이터 흐름
- 클라이언트 → Supabase 직접: 큐브·급여·알레르기 등 일반 CRUD는 브라우저의 Supabase 클라이언트가 직접 호출하고, RLS가 권한을 책임집니다. (
lib/db.ts) - 서버 전용 작업은 Admin 클라이언트(RLS 우회): 초대 토큰 사용 처리, 채팅 로그 저장, 알림 발송처럼 RLS로는 안 되는 작업은 service_role 키의 Admin 클라이언트로 처리합니다.
- 세션 분리:
@supabase/ssr로 서버용/클라이언트용 Supabase 클라이언트를 분리하고,proxy.ts(미들웨어)에서 세션을 검사해 비로그인 사용자를/login으로 리다이렉트합니다. - AI 채팅: 클라이언트 →
/api/chat→ 일일 사용량 체크 → Anthropic API 호출 → 응답 반환 + 로그 저장.
Claude API 연동
큡큡에는 Claude가 두 곳에서 일합니다. ① 사용자 질문에 답하는 챗봇과 ② 그 질문들을 주간 요약해 운영자에게 보내는 리포트입니다. SDK 없이 HTTP API를 직접 호출해 의존성을 최소화했습니다.
호출 프로세스
Messages API는 POST https://api.anthropic.com/v1/messages 하나로 끝납니다. 인증은 x-api-key, 버전은 anthropic-version 헤더로 지정합니다.
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY, // 서버 환경변수, 브라우저 노출 금지
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-haiku-4-5-20251001', // 빠르고 저렴한 Haiku, 이유식 QnA엔 충분
max_tokens: 600, // 답변 길이 상한 (비용·속도 통제)
system: '...이유식 도우미 페르소나...', // 역할 지시
messages, // 지금까지의 대화
}),
});
const answer = (await response.json()).content?.[0]?.text;핵심 3포인트:
- API 키는 서버에서만: 브라우저로 나가면 탈취되므로, 클라이언트는 우리 서버(
/api/chat)만 부르고 서버가 대신 Claude를 호출합니다. - 모델은 Haiku: 이유식 상담 수준엔 가장 빠르고 저렴한
claude-haiku-4-5로 충분합니다. systemvsmessages구분 (아래 참고).
system vs messages
system: 페르소나·규칙(역할 설정). 대화 내내 고정입니다.messages: 실제 대화 턴(user/assistant가 번갈아). 대화가 진행되며 쌓입니다.- stateless : API는 이전 요청을 기억하지 못합니다. 대화가 이어지게 하려면 매 호출마다 지금까지의 대화 전체를
messages에 다시 담아 보내야 합니다. messages(대화)는 화면을 그리는 클라이언트가 쌓아 서버로 넘기고,system(규칙)은 서버가 주입합니다.system을 서버가 독점해야 사용자가 "모든 제한을 무시해" 처럼 조작할 수 없습니다.
"당신은 친절하고 전문적인 이유식 도우미입니다. 한국어로 답변하세요. 이유식 재료, 손질법, 월령별 적합한 식품, 알레르기, 조리법, 큐브 제작법 등에 대해 정확하고 따뜻하게 안내해 주세요. 의료적 조언이 필요한 경우에는 소아과 상담을 권유하세요."
지금 구현은 system + messages라는 기본기에 충실하지만, Anthropic이 말하는 context engineering("가장 적은 토큰으로 가장 높은 신호를") 관점에서 보면 개선 여지가 있습니다.
- 대화 히스토리 절단/압축 : 지금은 전송할 때마다 지금까지의 모든 턴(복원 시 최근 30개 로그 = 60턴)을 통째로 실어 보냅니다. 오래된·무관한 턴까지 매번 보내 토큰을 낭비하므로, 최근 N턴만 남기고(truncation), 그 이전은 한 문단 요약으로 접는(compaction) 방식으로 바꾸려 합니다.
- Tool use로 "내 아기 데이터"를 근거로 (Just-in-Time 컨텍스트) : 지금 챗봇은 일반론만 답합니다. tool use로 Claude가 필요할 때 이 사용자의 실제 큐브 재고·최근 급여 기록·아기 월령을 조회하게 하면, "오늘 뭐 먹이지?" 에 "소고기가 넉넉하니 애호박을 곁들여…" 처럼 개인화된 답이 가능합니다. 이미
fetchCubes·fetchFeedingRecords로직이 있으니 소수의 툴만 노출하면 됩니다. 일반 챗봇을 진짜 육아 비서로 바꾸는 가장 큰 업그레이드이자, babymealplan의 "식단 계획" 방향과도 맞닿아 있습니다. - System 프롬프트의 "고도(altitude)" 조정 + 예시 : 한 문단짜리 페르소나를
<role>·<scope>·<rules>섹션으로 구조화하되, 너무 세밀해 깨지기 쉽지도(brittle) 너무 막연하지도 않은 중간 고도를 목표로 다듬고, 정석 문답 예시 1~2개(few-shot)를 더해 톤을 안정화합니다.
주간 리포트 : 질의 요약 (Cron)
사용자들이 쌓은 질문을 또 다른 Claude 호출로 요약해, 매주 운영자(나)에게 Slack으로 보냅니다. "사용자들이 실제로 뭘 어려워하는지"를 별도 분석 도구 없이 파악하는 장치입니다.
- 주기: Vercel Cron
0 9 * * 1→ 매주 월요일 오전 9시/api/cron/chat-summary자동 실행 - 동작: 지난 7일치 질문을 모아 Claude에게 "주제 카테고리·자주 묻는 패턴·눈에 띄는 니즈를 3~5개로 요약"시킴 → 총 질문 수·순 사용자 수와 함께
#chat-summary채널로 전송 - Claude 호출이 실패하면 원문 질문 목록으로 폴백
챗봇 로그(chat_logs)는 pg_cron으로 90일 뒤 자동 삭제됩니다. 요약 리포트는 이미 Slack에 남으니, 원문 로그를 오래 쌓아둘 필요가 없습니다.
한계, 그리고 다음 이야기
큡큡을 실제로 쓰면서 (한 명밖에 없는)유저의 불편함이 나타났습니다.
- 식사가 늘면서 "식단 짜기"가 어려워졌다 : 아기가 크면 한 끼 재료가 많아지고 하루 끼니 수도 늘어, "오늘 뭘 어떻게 조합하지?" 가 큰 일이 됩니다. 큡큡은 기록은 도와도 계획은 도와주지 못했습니다.
- 이중 입력 : 이미 쓰던 육아 기록 앱(베이비타임)에 한 번, 큡큡에 또 한 번, 같은 걸 두 번 입력해야 했습니다.
- 재고가 실제와 어긋난다 : 수기 차감·누락 탓에 앱의 숫자와 냉동실 재고가 안 맞는 일이 생겼습니다.
그래서 이 한계를 풀기 위해 다음 프로젝트, babymealplan을 만들게 되었습니다. 재고 관리를 넘어 "식단 계획" 자체를 도와주는 앱입니다. 방향만 미리 귀띔하면:
- 재료 마스터 DB + 단계별 비율 : 재료를 데이터로 관리하고, 월령 단계에 맞는 적정량/조합을 계산
- "오늘의 식단" 중심 화면 : 무엇을 먹일지 계획하고, 먹이면 큐브가 자동 차감되는 흐름
- 알림도 진화 Web Push에서 한 발 더 나아간 텔레그램 연동 실험
babymealplan의 기획·아키텍처·개발기는 다음 포스팅에서 자세히 다룹니다. 큡큡에서 배운 것을 어떻게 다음 앱의 설계로 녹였는지 풀어보겠습니다!
기술 스택: Next.js 16 · React 19 · TypeScript · Supabase · Anthropic Claude · Vercel · Capacitor