[Episode.2] 하루 만에 AI 레시피 앱 MVP 완성 | Flutter CRUD 구현과 OpenAI 연동기

[Episode.2] 하루 만에 AI 레시피 앱 MVP 완성 | Flutter CRUD 구현과 OpenAI 연동기

지난 이야기

프롤로그, 에피소드 1편에서는 레시피 앱을 어떤 계기로 만들게 되었는지 그리고 기술 스택과 디자인 테마는 어떻게 정했는지 회고했다.

이번 편에는 실제로 작동하는 첫 버전을 만들었던 과정을 담았다.


MVP 구현의 시작

아침, 커피 한 잔과 함께 노트북을 열었다. 목표는 오늘 안에 '작동하는' 첫 버전 만들기. 먼저 "작동한다"는 게 무엇을 의미하는지 정의하는 것부터가 숙제였다. 내가 정한 기준은 단순했다. 사용자가 감정 메모를 필수 입력값으로 하여 레시피를 저장할 수 있는가? URL이든 사진이든 어떤 입력을 넣어도 AI가 초안을 채워 주고, 필요하면 수동으로 고친 뒤 바로 보관함에 기록되는가? 오류가 나더라도 안내 메시지와 삭제·수정 같은 안전장치가 잘 작동 하는가? 세 가지 최소한의 기준을 충족할 때 이 앱은 '작동한다'고 볼 수 있었다.

프로젝트 셋업

Episode 1에서 정리한 구조를 실제 프로젝트에 옮기는 작업부터 시작했다.

  1. Flutter 프로젝트 기반 세팅: 필수 패키지(provider, dio, hive, image_picker) 설치와 iOS 권한 정리, 폴더 구조 점검
lib/
├── config/        # API 설정, 테마 색상
├── models/        # Recipe, Ingredient, Mood
├── services/      # OpenAI, Hive
├── screens/       # 화면들
├── widgets/       # 버튼 같은 공통 요소
├── providers/     # 상태 관리
└── utils/         # 도구들
  1. 작업 문서화: CLAUDE.md, ARCHITECTURE.md, PROGRESS.md, TESTPLAN.md 등에 상태 업데이트
  • CLAUDE.md: 프로젝트 전체 개요
  • ARCHITECTURE.md: 시스템 구조 설명
  • DESIGN.md: 디자인 가이드
  • PROGRESS.md: 진행 상황 기록
  • TESTPLAN.md: 플로우별 테스트 시나리오와 체크리스트
  • TESTDATA.md: 감정·태그 조합 샘플 데이터 모음

프로젝트 중후반부터 특히 위 문서들이 큰 힘이 되었다. '왜 이 결정을 했는지'를 남겨 둔 기록 덕분에 다음 단계로 넘어갈 때 흔들리지 않았고, 각 프로세스를 트래킹하기 수월했다. 과거의 나와 대화하는 기분이랄까.

사전 준비, OpenAI API 키와 환경 변수

그 다음으로 한 일은 OpenAI에서 새 API 키를 발급받는 것이었다. 키를 그대로 코드에 넣지 않도록 프로젝트 루트에 .env 파일을 만들고 OPENAI_API_KEY=sk-... 형태로 저장했다. Flutter 쪽에서는 flutter_dotenv로 이 값을 읽어오고, 터미널에서 https://api.openai.com/v1/models 엔드포인트를 호출해 "200 OK"가 떨어지는지 확인했다. 덕분에 이후 앱 코드에서 오류가 나면 내 코드 문제인지, 키나 서버 문제인지 구분할 수 있었다.


데이터 모델 설계와 첫 번째 위기

미리 정한 Recipe 모델 구조(감정 메모 필수, Hive 저장)는 그대로 가져왔다. 이번에는 실제 데이터를 넣어 보며 필수 입력 필드인 emotionalStory가 비어 있으면 저장 단계에서 어떻게 막히는지, 감정 Enum은 잘 연결되는지 검증하는 데 집중했다. 그리고 모델 적용과 동시에 블로그에서 레시피를 끌어오는 스크래퍼를 붙여 첫 실사용 데이터를 만들었다.

OpenAI Service 구현

1) 링크로 가져오기 플로우
  • 소스 확보: 여러 차례 테스트를 거쳐 모바일 네이버 블로그를 스크래핑 대상으로 설정했다. PC에 비해 DOM이 단순하고 광고 노이즈가 적어 제목·재료·조리 단계를 안정적으로 추출할 수 있었기 때문이다.
  • AI 매핑: 추출한 텍스트를 템플릿에 넣어 chat.completions으로 보내면 title, ingredients, instructions 값이 채워진다.

진행 안내: 레시피 재료 준비중 → 레시피 추출 완료 → AI로 레시피 굽는중 → 작성 완료 메시지를 단계별로 표시해 URL 입력부터 저장까지 흐름이 끊기지 않는지 확인했다.

2) 이미지 분석 플로우
  • 전처리: 촬영/앨범 이미지는 1024px·2MB 이하로 압축 후 Base64로 변환해 네트워크 전송 비용과 타임아웃을 줄였다.
  • 타입 감지 & 분석: OpenAI 모델이 이미지를 감지해 레시피가 적힌 스크린샷이면 한글 OCR 분석 프롬프트를, 일반 사진이면 음식 분석 프롬프트를 사용하도록 적용했다. 음식 사진이 아니라면 메시지를 띄워 다른 이미지를 넣도록 요청한다.
  • 로딩 UX: 분석이 길어질 수 있어 이미지 타입 감지중, AI로 레시피 분석중 같은 메시지를 띄워 사용자가 현재 진행 상태를 알 수 있게 했다.
3) 키워드·재료 기반 빠른 입력
  • 퀵레시피 작성하기: 요리 이름 한 줄만 입력하면 텍스트 전용 프롬프트를 활용해 10초 안에 초안을 채워 준다. 최소 입력으로 감정 메모만 보강하면 바로 저장까지 이어지는 흐름이 유지된다.
  • 냉장고 재료 입력하기: 사용자가 집에 가지고 있는 재료 리스트를 선택하면 바로 만들 수 있는 요리를 제안하도록 구성했다. 마음에 들지 않으면 다른 메뉴 추천받기도 가능하다. 재료가 부족할 땐 대체 재료까지 AI가 함께 추천해 기록 습관이 끊기지 않게 했다.
4) 에러 핸들링 전략
  • 이미지 흐름의 경우 Base64 검증이나 스크린샷 감지가 실패하면 즉시 일반 분석 루틴으로 내려 안전하게 마무리한다. (Base64는 이미지를 텍스트 형식으로 포장하여 안정적으로 전송되도록 돕는 역할)
  • 응답이 비거나 네트워크가 불안정하면 최대 3회까지만 재시도하고, 실패 시 안내 메시지를 띄워 수동 입력으로 전환을 유도한다.
  • 토큰 오류, 429 Too Many Requests, 500과 같은 에러는 원인별 메시지를 나눠 프록시 작업 시 로그를 볼 때도 바로 식별할 수 있도록 준비했다.
CORS 에러의 등장

AI를 붙여 테스트를 돌리니 콘솔에 아래와 같은 경고가 뜨기도 했다.

Access to XMLHttpRequest at 'https://api.openai.com/v1/chat/completions'
from origin 'http://localhost' has been blocked by CORS policy

웹 빌드에서는 브라우저가 "너 지금 다른 서버에 바로 가려고 하잖아?"라며 보안을 이유로 막아 버린다. Flutter 앱이라도 웹에서 돌리면 이 규칙을 피할 수 없고 API 키를 그대로 노출하는 것도 보안상 위험했다.

당시에는 flutter run -d chrome으로 실행해 두고 Playwright MCP로 자동 테스트 시나리오를 돌리는 중이었다. 크롬 브라우저 안에서 Recipesoup 웹앱을 띄워 놓은 상태였기 때문에 브라우저 보안 정책(CORS)이 적용된 것이다.

당장은 앱 흐름을 완성하는 데 집중하느라 프록시를 손댈 여유가 없었다. 대신 터미널에서 직접 curl을 날려 'API는 정상'인지 확인하고 iOS 시뮬레이터에서 프록시 없이 텍스트 요약과 이미지 분석 플로우가 잘 돌아가는지 다시 체크했다. 프록시 구현은 별도 TODO로 묶어 이후 배포 시점에 적용했다.

참고로 네이티브(iOS·Android)는 CORS에 걸리지 않는다. 그래도 API 키와 호출 흐름을 한 곳에서 관리하고자 추후 브라우저와 앱 모두 프록시를 통하도록 설계했다.

에러 케이스와 대응 요약

  • 401 Unauthorized: 프록시를 거치지 않고 호출하다가 Authorization: Bearer <토큰> 헤더를 누락해 로그를 통해 이를 확인하고 다시 넣었다.
  • 429 Too Many Requests: OpenAI가 잦은 요청을 막아서 요청 사이에 딜레이를 넣어 2초 쉬게 하고 사용자에게는 "조금 뒤에 다시 시도해 주세요" 토스트를 보여주도록 했다.
  • 500 Internal Server Error: OpenAI 쪽이 잠깐 불안정해지면 최대 세 번까지만 다시 호출, 내부 테스트에서는 로그로 남겨 사용자에게는 "잠시 후 다시 시도해 주세요" 정도만 보이게 했다.
  • SocketException (오프라인): 인터넷을 끊으면 요청이 아예 나가지 않아서 콘솔 로그로 오프라인 상황을 남기고, 사용자가 AI 대신 직접 입력으로 흐름을 이어가도록 했다.
  • CORS (웹 빌드): 크롬에서만 막히는 문제로 프록시 구축(우리 서버 → OpenAI)은 별도 TODO로 넘겼다.

AI 응답을 어떻게 보여줄까

디자인 테마는 미리 정해두었기에 UI/UX는 빈티지 톤과 폰트, 카드 스타일을 실제 화면에 입히는 과정이었다. 목표는 자동으로 채워진 데이터와 사용자가 직접 적은 메모가 자연스럽게 어우러지게 하는 것. 두 가지가 섞여도 '기록하는 공간'이라는 느낌이 깨지지 않도록 하고자 했다.

AI 기능을 통해 분석된 레시피는 재료, 소스, 조리 방법이 자동으로 분류되어 레시피 작성 화면으로 넘어오게 했다. 조리 단계를 보여줄 때에는 1. … 형식으로 넘버링해 가독성을 높였다. <레시피 작성> 버튼을 누르면 매핑된 값이 필드 입력란에 그대로 채워진다. 사용자는 감정 메모나 디테일만 다듬고 바로 저장할 수 있다.

분석이 실패하면 안내 메시지를 띄워 수동 입력으로의 전환을 유도했다. '자동과 수동 사이를 어떻게 매끄럽게 오가게 할 것인가'가 이 단계의 핵심이었다.


CRUD 구현과 Hive 재확인

기술 스택을 정할 때 로컬 우선을 기준으로 해서 Hive를 골랐고 그 선택은 이 단계에서 빛을 발했다. put 한 줄로 박스에 저장한 뒤 곧바로 리스트가 갱신되는 걸 보고 로컬 DB 구현은 적절한 선택이었음을 실감했다. 테이블 스키마를 짜고 생성할 필요도 없었고 마이그레이션 걱정도 덜었다. 덕분에 CRUD 테스트를 하는 동안 작업이 늘어지지 않고 스무스하게 진행되었다.

삭제 기능은 한 번 더 고민했다. 개인적인 기록을 아카이빙하는 앱에서 사용자가 실수로 데이터를 삭제하도록 방치하면 부정적인 경험만 남을 것이다. 이러한 이유로 MVP에 꼭 넣어야 하는 기능이 아님에도 "정말 삭제할까요?" 다이얼로그를 추가했다. 실수 방지 장치는 앱의 특성상 꼭 필요한 장치였다.


하루 타임라인 정리

  • 아침 | OpenAI 키 발급, .env 정리 및 간단한 API 호출로 응답 확인
  • 오전 | Flutter 프로젝트 생성과 폴더 구조 정리, 필수 패키지 설치, 작업 문서 업데이트
  • 이른 오후 | 블로그 스크래핑 및 AI 요약 흐름 점검, 첫 이미지 분석 성공, JSON 파싱 로직 정리
  • 늦은 오후 | 401/429/500/오프라인/CORS 테스트, 사용자 메시지 다듬기, 프록시 TODO 분리
  • 저녁 | 빈티지 톤 UI 적용, 자동 채워진 필드와 수동 입력 Fallback 동작 확인
  • | CRUD 동작 최종 점검, 삭제 다이얼로그 추가, QA 체크리스트 작성

중간중간 다른 작업을 먼저 할까, 하는 유혹이 들었지만 '오늘의 목표는 작동하는 흐름 만들기'라는 걸 계속 되새겼다.

밤 11시, 마지막 테스트를 마쳤다.

  • 블로그 URL 스크래퍼와 요약 기능도 기대대로 잘 돌아갔다.
  • 사진을 찍으면 AI가 분석해주고, 필요하면 금방 고쳐 저장할 수 있었다.
  • AI 사용 시 단계별 프로그레스 메시지도 끊김 없이 이어졌다.
  • 레시피가 저장된 보관함에서 내용을 볼 수 있고, 수정하고, 삭제할 수 있다.
  • 레시피 기록 구조와 빈티지 테마 디자인이 기능에 잘 입혀졌다.

이렇게 최소한의 기능을 붙인 첫 MVP 버전이 완성되었다.


마무리하며

이번 에피소드까지의 작업을 통해 네 가지를 확인했다.

첫째, '최소한'은 검증에 꼭 필요한 것만 남기는 일. 이번 과제는 "사용자가 감정을 담은 레시피를 저장할 수 있는가?"였기에 당장 필요하지 않은 기능은 뒤로 미뤘다. 잘라내는 것도 연습이 필요하다.

둘째, 초보라면 기술 선택은 쉽고 빠른 길로. Flutter·Hive·Provider는 내 수준에서 최대한 빠르게 구현할 수 있는 조합임을 다시금 깨달았다. 개발에 대해 잘 알지 못하는 상태로 1인 작업 시 '최고'의 아키텍처를 욕심내는 건 사치.

셋째, 에러 케이스를 통한 배움. CORS 덕분에 '클라이언트에서 바로 키를 쓰면 안 된다'는 사실을 다시 한 번 확인했다. MVP는 기능을 만드는 것 못지않게 문제를 빠르게 찾아서 대응하는 과정에 가깝다는 생각도 든다.

마지막으로, 결정했던 기준은 크리티컬한 오류가 있는 게 아니라면 유지. emotionalStory를 필수 필드로 묶어 둔 선택은 번거롭지만 앱의 방향을 지키는 역할을 하므로 계속 이어 가기로 했다.

이번 에피소드 핵심 요약

  • 작업 사항: 블로그 요약과 이미지 분석 AI 연동, 자동 채움·수동 편집이 가능한 레시피 작성 화면 구현, CRUD 전 흐름에 삭제 확인·에러 메시지 같은 안전장치 마련
  • 기술/운영: OpenAI 연동 전 .env·curl로 통신을 확인하고, 401·429·500·오프라인·CORS 등 에러 대응, 프록시 구축은 다음 단계로 이관

단거리 달리기가 끝났으니 이제 이 앱이 계속 쓰이도록 동기부여를 할 차례다.

다음 이야기, Episode 3 예고편

  • 감정 기록을 꾸준히 이어가게 하는 '토끼굴' 마일스톤 설계를 어떻게 시작했는지
  • Claude와 ASCII 시안으로 토끼굴 UI를 잡고 32단계 성장 여정을 만든 과정
  • burrow_unlock_service.dart 재작성과 QA에서 터진 보상/레벨 버그를 해결한 기록

다음 편에 계속..