- 발행일
갈무리부엌 — 일주일, 커밋 894개를 에이전트에게 맡기고 내가 한 일
갈무리부엌
사둔 재료, 남김없이 챙겨요
냉장고에 뭐가 있는지 적어 두면 상하기 전에 알려 주고, 남은 재료로 레시피를 찾아 주고, 모자란 것만 장보기 목록에 담아 주는 모바일 웹 서비스입니다. 원티드 AI Championship 2026에 냈어요.
| 항목 | 내용 |
|---|---|
| 기간 | 2026-09-13 10:51 첫 커밋 → 09-20 22:15 마지막 커밋 |
| 커밋 | 894개(그중 --no-ff 병합 315개) |
| 스택 | React 19·TypeScript·Vite, Flask 3.1·SQLAlchemy, PostgreSQL, Claude API, Cloudflare R2, Render |
| 테스트 | 백엔드 pytest 1,701개(SQLite·PostgreSQL 둘 다), 프런트 검사 스크립트 11개, Playwright E2E 82개 — 9/20 67be15e 기준 |
| 저장소 | hyo814/galmuri-kitchen |
| 체험 | galmuri-kitchen.onrender.com/?demo=1 — 로그인 없이 체험 계정으로 열립니다 |
먼저 밝혀 둘 게 있습니다. 이 저장소의 코드는 제가 한 줄씩 친 게 아닙니다. Claude Code에서 구현·검토·검증을 에이전트로 나눠 돌렸고, 저는 요구사항을 말하고, 선택지 중에 고르고, 시안과 push를 승인했습니다. 그래서 이 글은 "이렇게 구현했다"보다 "무엇을 정했고, 무엇이 어디서 잡혔나" 에 가깝습니다.
1. 무엇을 만들었나
문제는 세 가지로 잡았습니다.
- 날짜를 따로 적지 않은 두부·요거트가 냉장고 안쪽에서 잊혀 상한다.
- 마트에서는 집에 대파가 있었는지 기억이 안 나고, 돌아오면 참기름이 떨어져 있다.
- 있는 재료로 뭘 만들지 떠오르지 않아 또 산다.
그래서 재고 → 레시피 → 장보기를 한 흐름으로 묶었습니다. 영수증·냉장고 사진을 찍으면 AI가 재료를 채우고, 곧 먹어야 할 재료가 위로 올라오고, 그 재료를 먼저 쓰는 레시피를 보여 주고, 없는 재료만 장보기에 담습니다. 마트 지하처럼 인터넷이 없어도 장보기 체크가 되고, 연결되면 서버로 보냅니다. 뒤로 식단 짜기·영양 계산·먹은 기록·요리 일기가 붙었어요.
2. 어떻게 만들었나
한 기능이 main에 들어가기까지 순서는 매번 같았습니다.
- 요구사항이 생기면 코드보다 설계 문서를 먼저 고친다. 결정에는
(사용자 결정 2026-09-13)처럼 날짜를 남긴다. - 화면이 바뀌면 시안을 먼저 보고 제가 승인한다. 승인한 시안은
docs/design/에 둔다(16묶음). - 태스크 단위 구현 계획을 쓴다(단계별 13개).
- 태스크마다 git worktree와 브랜치를 따로 만들고, 구현 에이전트가 테스트와 함께 구현한다.
- 변경 종류에 맞춰 검토 에이전트를 병렬로 붙인다 — 백엔드는 코드·보안·테스트, 화면은 코드·UX·접근성.
- 지적은
fix/…브랜치에서 반영하고, 작성자가 아닌 검증 에이전트가 테스트·빌드를 다시 돌린다. main에--no-ff로 병합하고, 단계가 끝나면 전체 브랜치를 한 번에 보는 최종 검토를 한다.
병합 제외 커밋 종류를 세면 fix가 feat보다 많습니다(9/20 00:28 67be15e 기준 fix 224, feat 144). 검토에서 나온 지적을 따로 fix/ 브랜치로 고쳤기 때문이에요. 식단 짜기 단계(4b-1)는 사람 개입 없이 밤사이 돌렸는데, 9/14 21:52 시안 병합부터 9/15 02:35 최종 검토 병합까지 약 4시간 40분 걸렸습니다.
3. 테스트로는 안 잡힌 것들
구현 직후 테스트는 통과했는데, 검토나 다음 태스크, 운영에서 잡힌 문제들입니다. 이 목록이 이 방식에서 제일 쓸모 있던 산출물이었어요.
조금씩 흘려 보내는 서버가 8초 제한을 넘긴다
블로그 링크를 붙이면 서버가 그 주소를 받아 레시피로 정리합니다. 서버가 외부 주소를 부르니 SSRF 방어가 필요하고, 사설 IP 차단은 처음부터 있었어요. 보안 검토가 짚은 건 다른 쪽이었습니다. 응답을 아주 조금씩 보내는 서버는 읽을 때마다 뭔가 오니 소켓 타임아웃에 안 걸리고, 요청 스레드를 계속 붙잡을 수 있었습니다.
그래서 리다이렉트까지 합친 전체 8초를 감시 타이머가 재고, 시간이 지나면 소켓을 끊게 했습니다. 연결은 DNS 검사를 통과한 IP 하나에만 하고, IPv4를 담는 IPv6 대역(NAT64·6to4)도 거절합니다(7393caa, 9/14 10:19). 6분 뒤 이어진 검토에서 "시간이 지난 뒤에 새로 만든 연결" 은 타이머가 못 닫는다는 틈이 또 나와 한 번 더 고쳤어요(b534630, 10:25).
세션이 만료되면 오프라인에서 체크한 것이 사라진다
마트에서 인터넷 없이 체크한 변경은 기기(IndexedDB) 대기열에 쌓였다가 연결되면 갑니다. 그런데 연결된 순간 세션이 만료돼 401이 오면, 앱은 "로그아웃됐으니 기기 데이터를 지운다"는 흐름을 탔습니다. 보내지 못한 체크가 같이 지워졌죠.
검토 지적을 받아 에이전트가 401이면 기억한 사용자만 지우고, 대기열은 주인(로그인 방법 + id)과 함께 남기게 바꿨습니다. 같은 사람이 다시 로그인하면 이어서 보내고, 다른 사람이면 지웁니다. 직접 로그아웃할 때는 보내지 않은 변경 N개가 사라져요로 묻고요(601de80). 기기 저장소 읽기가 실패했을 때 대기열을 빈 값으로 덮어쓰던 문제도 같은 날 따로 나왔습니다(4128c19). "못 읽었다"와 "비어 있다"를 같은 값으로 다루면 읽기 실패 한 번이 데이터 삭제가 됩니다.
닫는 괄호 하나가 빠졌고, 빌드는 통과했다
9/15 저녁 먹은 기록 사진 화면을 병합(933b6fa)하면서 styles.css의 규칙 하나에서 }가 빠졌습니다.
.sh-photo.add:disabled {
opacity: 0.4;
/* 요리 일기 (5) */
CSS는 이걸 오류로 보지 않습니다. 그 뒤 규칙이 전부 이 블록 안에 중첩된 것으로 읽혀 조용히 무효가 되고, Vite 빌드도 통과해요. 재료를 지울 때 이유를 고르는 시트의 스타일이 통째로 안 먹는 상태로 운영에 나갔고, 자정 넘어 에이전트가 한 줄로 고쳤습니다(43671f7, 9/16 00:12).
에이전트가 7분 뒤 빌드 전에 괄호 짝을 세는 검사를 넣었습니다(46c49e3). 주석과 문자열만 건너뛰고 { }를 세는 36줄짜리 스크립트고, 짝이 안 맞으면 몇째 줄 근처인지 찍고 빌드를 멈춥니다. 문법 전체를 보는 stylelint는 '필요해지면 그때 들인다'로 미뤄 뒀고, 코드 주석에도 그렇게 남아 있습니다. 괄호 말고 다른 문법 오류는 아직 못 잡는다는 뜻입니다.
사실 같은 사고는 9/14 장보기 탭 작업(9df537c)에서 이미 한 번 났습니다. 그때는 괄호만 닫고 넘어갔고, 두 번째로 운영에 나간 뒤에야 검사를 넣었습니다. 처음 났을 때 재발 방지를 걸지 않은 게 이 흐름의 빈틈이었습니다.
같은 날 00:28에 다른 작업 트랙에서 똑같은 한 줄을 또 고친 커밋(99a9522)이 들어왔습니다. diff가 파일 해시까지 같아요. 병렬 트랙이 서로의 수정을 모른 채 같은 버그를 따로 본 거예요. 트랙을 나눌 때 같은 파일을 만지는 작업은 앞 트랙 병합 뒤로 미루는 규칙이 있었지만, 당시 6,000줄(지금은 7,900줄)짜리 CSS 한 파일은 거의 모든 화면 작업이 만지니 이 규칙으로는 못 막습니다.
운영 데이터에서만 보이는 중복
9/19 새벽, 에이전트가 운영 체험 계정을 폰 크기 화면으로 훑다 발견했습니다. 음식 찾기에서 비빔밥을 찾으면 이름이 똑같은 줄이 6개 나왔어요(1인분 100g·530g·222g …). 식품영양성분 DB 실자료를 세어 보니 콩기름 18개, 엑스트라버진올리브유 15개처럼 같은 이름이 흔했고, 검색 결과 20칸이 같은 이름으로 다 찼습니다.
로컬 E2E는 API 키를 비운 예시 모드라 이 데이터가 없어서, 로컬 E2E가 전부 통과해도 안 보였던 문제입니다. 에이전트가 정렬한 뒤 20개로 자르기 전에 이름이 같은 행을 하나만 남기게 했어요(d8477fb). 어떤 행을 남길지는 앞서 정한 정렬(빠진 영양소가 적은 행 먼저)을 그대로 따릅니다. 1인분 무게는 화면에서 사용자가 고치는 값이라 고를 근거로 쓰지 않았고요.
그 뒤 운영에 대고 돌린 E2E에서도 몇 건이 예시 모드와 달랐지만 실제 AI 응답 차이였고, 새로 나온 결함은 없었습니다.
4. 만들지 않기로 한 것
에이전트는 시키면 만듭니다. 아래 표에서 앞의 세 줄은 에이전트가 선택지를 보여 주고 제가 고른 것이고, 광고·데이터 판매는 첫날 수익화 방향을 적으면서 함께 뺀 것입니다.
| 안 만든 것 | 이유 | 대신 |
|---|---|---|
| 앱 안 최저가 표시 | 네이버 쇼핑 검색 API가 2026-07-31에 종료됐고 공식 대체가 없음. 가격 스크래핑은 약관·차단 위험 | 쇼핑몰 6곳의 낮은 가격순 검색 링크. 5곳은 폰에서 정렬까지 확인했고, G마켓은 봇 확인 화면에 막혀 주소가 열리는 것까지만 봤음(9/17) |
| AI 레시피 음식 사진 생성 | 9/14 AI 레시피 계획을 세울 때 이미지 생성 대신 비슷한 공공 레시피 사진을 쓰기로 고름 | 이름이 비슷한 공공 레시피 사진을 찾아 비슷한 요리 사진으로 밝혀 씀. 없으면 사진 없이 둠 |
| 유튜브 전체 검색 | search.list는 한 번에 100 units라 하루 무료 한도 10,000을 금방 씀 | 고른 요리 채널 영상만 1 unit짜리 API로 받아 캐시하고 그 안에서 검색. 더 찾고 싶으면 YouTube 검색 페이지로 연결 |
| 배너 광고·사용자 데이터 판매 | 배너는 수익이 적고 디자인 방향과 안 맞음. 재고·식습관 데이터는 팔지 않음 | AI 호출 횟수로만 나누는 구독을 기획. 원가를 먼저 잼 |
마지막 줄의 "원가를 먼저 잼"은 실제로 코드에 있습니다. AI 호출마다 모델과 토큰을 ai_calls 테이블에 남기고, 그걸로 영수증 1장 인식 약 13원, AI 레시피 3개 약 30원, 한 주 식단 초안 약 70원을 추정했어요(추정값). 재고·임박 경고·공공 레시피 추천·오프라인 장보기처럼 AI가 필요 없는 기능은 무료로 둡니다. 경계를 AI 사용 횟수로 둔 건 첫날(9/13) 설계에서 '비용이 드는 곳이 경계'로 먼저 정했고, 이 숫자는 그 경계에 가격을 붙이려고 잰 값입니다. 가격은 아직 정하지 않았습니다.
같은 이유로 한도를 촘촘히 걸었습니다. 사용자별 하루 사진 인식 20번·AI 레시피 20번, 60초 안 3번 연속 제한, 로그인 없이 체험하는 계정은 하루 5번에 체험 계정 전체 24시간 80번 예산. 처음엔 성공한 호출만 셌는데, 그러면 실패를 반복하며 비용을 쓸 수 있다는 검토 지적이 나와 AI로 보낸 호출은 모두 세도록 바꿨습니다. 다만 사진을 못 읽은 경우까지 다 세면 사용자가 억울해서, 나중에 하루 3번까지는 한도에서 빼 주고(연속 제한·전체 예산에는 셈) 그 뒤로는 세도록 다시 조정했습니다.
5. 숫자를 어디서 가져왔나
임박 판정은 이 서비스의 첫 화면이라 숫자 근거가 중요했습니다. 품목별 기본 규칙 21개는 공공기관 참고값에서 가져왔고, 화면에 출처를 같이 보여 줍니다.
- 식약처 「식품유형별 소비기한 설정 보고서」 — 두부 23일, 발효유 32일, 햄 57일 등. 이 값은 제조일 기준인데 사용자가 아는 건 구입일이라, 참고값의 80%(내림)가 되는 날을 빨강, 그 3일 전을 노랑으로 잡았습니다. 두부면 구입 18일째 빨강, 15일째 노랑이에요.
- 국립수산물품질관리원 — 생선·오징어 1
2일, 굴 45일. 참고값이 범위라 환산하지 않고 짧은 쪽을 씁니다. - 농촌진흥청 — 김치. 규칙이 없어 냉장 7일 기본값에 걸려 김치가 일주일 만에
오래됨이 떴고, 마지막 날(9/20) 이 규칙을 넣어 고쳤어요.
직접 적은 유통기한이 있으면 그게 품목 규칙보다 우선이고, 냉동실에는 품목 규칙을 쓰지 않습니다. 둘 다 제가 정한 순서예요. 그래서 얼린 생선에는 수품원의 1~2일 경고가 붙지 않습니다.
반대로 아직 확인 못 한 값은 확인 못 했다고 적어 뒀습니다. 양념 기본 비율은 레시피마다 차이가 커서 출처 두 곳 이상과 비교해야 하는데, 그 전까지는 임시값으로 넣고 코드 출처 메모에 확인 전이라고 남겼어요. 밥숟가락 12ml만 9/18에 통용 계량 안내로 확인해 확정했습니다.
6. 에이전트가 대신하지 못한 일
- 실기기 확인. 쇼핑몰 정렬 링크는 쇼핑몰마다 앱으로 넘어갈 때 동작이 달라, 정렬 링크(9/17)와 홈 화면 추가(9/19)는 제가 갤럭시 S22 Ultra 크롬으로 직접 열어 봤습니다. 카메라·오프라인은 에이전트가 운영 주소를 폰 크기 브라우저로 확인하고 저는 스크린샷으로 승인했고, 삼성 인터넷은 브라우저를 따로 깔아야 해서 넘겼습니다. 홈플러스는 폰에서 정렬되는 주소를 확인하지 못해 목록에서 뺐고요.
- 요리 채널 14개 고르기. 채널 정책과 콘텐츠는 사람이 봐야 했습니다.
- 상표 조사. KIPRIS는 자바스크립트로 그리는 화면이라 에이전트가 조회를 못 해서, 절차만 체크리스트에 적어 두고 출시 전에 제가 봅니다.
- 멈출 때 정하기. 심사·투표 기간이 9/21부터라, 9/18에 "기간 중에는 배포하지 않는다"로 정했습니다. 진행 중인 AI 요청이 배포 재시작에 끊기지 않게요. 계획했던
styles.css·FoodLogSheet.tsx나누기는 9/17에 Claude 사용량 때문에 취소했고, 배포를 멈추는 기간과 겹쳐 투표가 끝난 뒤로 미뤘습니다. 괄호 사고가 난 그 CSS 파일(지금 7,900줄)이 쪼갤 대상 1순위라는 건 부채로 남겨 둡니다.
돌아보며
커밋 894개 가운데 fix가 224개입니다. 빨리 만든 만큼 고칠 것도 많았고, 돌아보면 제 손이 실제로 간 곳은 아래 셋이었습니다.
- 무엇을 만들지 않을지 정하기. 스크래핑, 이미지 생성,
search.list처럼 만들 수는 있지만 비용·약관·신뢰 때문에 안 만들기로 한 것. - 테스트 밖을 보는 단계를 흐름에 넣기. 이번에 잡힌 건 전부 검토 에이전트와 운영 훑기에서 나왔습니다. 제가 한 일은 그 단계를 빼지 않고 매번 돌리게 한 것입니다.
- 같은 사고가 세 번 나지 않게 하기. 괄호 사고는 두 번 나고 나서야 검사를 걸었습니다. 처음 났을 때 막았어야 했습니다.
실제로 한 번 써 보고 싶으면 체험 링크를 폰에서 열어 보세요. 두부가 내일까지라는 배지가 맨 위에 떠 있을 거예요.