Flutter 가계부 앱 만들기(2) - 프로토타입에 영혼 불어넣기

"딱 봐도 그럴싸한 '가짜 앱'은 이만하면 됐다. 진짜 데이터를 보여주자."

시작 - 차별을 위한 고민과 도전

플레이스토어에 '가계부'를 검색하면 이미 수많은 앱이 자리를 차지하고 앉아있다. 이런 레드오션에서 이 앱이 살아남을 수 있을까? 단순히 예쁘기만 한 가계부 앱은 이미 차고 넘쳤다. 아무리 스터디지만 사용자들의 '귀찮음'을 해결해 줄 결정적인 한 방, '킬러 기능'이 필요했다.

고민은 길지 않았다. "영수증을 찍기만 하면, 지출 내역이 자동으로 입력된다면?" 이것이 내가 찾던 차별점이 될 것 같다. DB에 연결하고 AI 기반의 영수증 OCR 기능을 탑재하는, 프로토타입에 진짜 생명을 불어넣는 작업에 착수했다.

모바일 앱용 UI 화면 세트

앱의 뼈대 세우기 (DB 개발 및 연동)

모든 데이터는 결국 데이터베이스로 통한다. 나는 백엔드 서비스로 Supabase를 선택했다. (기본이 되는 SQLite는 웹 클라우드 서비스용으로는 부적합하고, Flutter와 창떡궁합으로 알려진 Firebase는 SQL과 테이블간 관계를 사용할 수 없어서 검토대상에서 제외시켰다.

첫 번째 난관: 유령 사용자 이름

하지만 프로필 화면에서 사용자 이름이 제대로 표시되지 않는 문제가 있었다. 분명히 profile 테이블도 있고, 회원가입 시 이름을 저장하는 로직도 잘못된 게 없는데 .... 원인은 데이터베이스와 코드의 미세한 불일치 -

해결방법: profiles 테이블에 이름이 null일 경우, '사용자'라는 기본값을 설정하고 DB에 즉시 업데이트하는 로직을 추가하도록 AI에게 요청했다. 이제 더이상 유령 사용자는 나타나지 않는다.

// lib/features/profile/profile_screen.dart

// Load user profile from profiles table
final profileResponse = await SupabaseService.client
    .from('profiles')
    .select('name, avatar_url') // avatar_url도 함께 가져오도록 수정!
    .eq('id', user.id)
    .single();

String fetchedName = profileResponse['name'] ?? '사용자';
// 이름이 null이면 DB에도 기본값 '사용자'로 업데이트
if (profileResponse['name'] == null) {
  await SupabaseService.client
      .from('profiles')
      .update({'name': '사용자'})
      .eq('id', user.id);
}

setState(() {
  _userName = fetchedName;
  _avatarUrl = profileResponse['avatar_url']; // 아바타 URL도 상태에 저장
  _isLoading = false;
});

정적 페이지의 동적화: '공지사항'이나 '문의하기' 같은 정적 페이지들도 이번 기회에 모두 DB 테이블로 분리했다. announcements와 contact_methods 테이블을 만들고 연결하니, 이제 관리자가 Supabase 대시보드에서 공지를 올리면 모든 사용자의 앱에 실시간으로 반영된다!


영수증 OCR 기능 추가

가계부 앱의 '킬러 기능'은 뭘까? 여러가지 아이디어를 생각해봤지만 단연 '영수증 스캔'을 통한 지출내역 자동입력일 것 같다. 이 마법 같은 기능은 GPT-4o-mini 모델을 사용하여 간단하게 구현할 수 있었다. (코드짜느라 고민하지 말자!)

과정:

  1. image_picker로 영수증 사진을 가져온다.

  2. openAIService에서 이미지를 Base64로 인코딩한다.

  3. 정교하게 작성된 프롬프트와 함께 OpenAI Vision API에 전송!

    "너는 영수증 OCR 전문가야. 이미지에서 상호명, 총액, 날짜, 결제수단, 카테고리를 JSON 형식으로 추출해 줘. 다른 말은 말고, JSON만 줘."

  4. API가 돌려준 JSON 응답을 파싱하여 AddEspenseScreen 각 필드에 자동으로 채워 넣는다.

앱으로 영수증 사진을 찍자, 잠시 후 'AI가 영수증을 분석하고 있어요...'라는 메시지와 함께 각 입력란에 착착 채워지는 순간, 짜맀함을 느꼈다.

두 번째 난관: AI의 변덕과 불안정한 네트워크

이번엔. 사용 중에 가끔 앱이 멈추는 행동을 보였다. 이건 또 어떻게 하지.. ? API 키가 없거나, 네트워크가 불안정하거나, AI가 가끔 이상한 응답을 줄 때마다 그럴 수 있단다.

해결방법: 사용자 정의 예외 클래스를 만들면 된다고 귀띰해주었다. (이 녀석 병주고 약주고 ㅋㅋ)한번은 Claude Code limit에 걸려서 Gemini cli로 대체해서 작업을 이어가야 했었는데, 그러다 보니 VS Code도 version 1.108(2025.12)로 업데이트해야 했다. 그랬더니 관련 라이브러리 간의 의존성이 깨져서 한참동안 앱을 돌려보지 못하기도 했다. (이것 때문에 휴일 반나절을 헌납했다)

// lib/core/services/openai_service.dart
class OpenAIServiceException implements Exception {
  final String message;
  final int? statusCode;
  // ...
}

// HTTP 상태 코드가 200이 아닐 경우
if (response.statusCode != 200) {
  // 에러 메시지와 함께 예외를 던진다!
  throw OpenAIServiceException(
    'OpenAI API Error: $errorMessage',
    statusCode: response.statusCode,
  );
}

이제 앱은 이 예외를 캐치하여 "API 키가 잘못되었습니다." 또는 "응답 데이터 분석에 실패했습니다."와 같이 사용자가 걱정하지 않도록 하는 오류 메시지도 보여줄 수 있게 되었다.


앱에 숨 불어넣기

어느정도 핵심 기능들이 구현되고, 이젠 앱의 사용성을 끌어올리는 '디테일'에 집중했다.

  • Provider 최적화: 이전에는 지출 내역을 추가할 때마다 전체 목록을 DB에서 다시 불러왔다. 이 때문에 UI가 깜빡이는 걸 알아챌 수 있었다. AI가 제안한 방안대로 로컬 상태를 먼저 업데이트하도록 몇 개의 메서드를 추가했더니, UI가 즉각적으로 반응하며 훨씬 부드러워졌다.

  • DateFormat의 배신: 잘 보니 오늘이나 어제가 아닌 날짜에 뜬금없이 '시간'만 반환하는 것을 발견했다. 마치 어제 만난 친구가 오늘 갑자기 나를 "오후 3시"라고 부르는 것과 같은 황당함이었다. 바로 YYYY.MM.DD 형식으로 날짜형식을 수정했다.

  • 예산 설정 기능: '예산'은 분석화면에서만 보였다. 사용자가 설정할 수 있는 방법이 없었는데, 이제 사용자가 직접 카테고리별 예산을 설정할 수 있는 다이얼로그를 추가했다. 이 작은 UI는 사용자가 진짜 앱의 주인이 되는 경험을 선사할 것이다.

  • 아바타 변경: 밋밋했던 기본 아이콘 대신, 사용자가 직접 프로필 사진을 올릴 수 있는 기능도 추가했다. 사용자는 먼저 사진을 선택하고, 데이터베이스에 업로드한 뒤, 공개 URL을 프로필 테이블에 저장하는 흐름으로 구현했다. 이제 사용자들은 각자의 개성을 뽐낼 수 있다.


휴대전화와 태블릿이 표시된 웹페이지 스크린샷
다양한 앱이 표시된 휴대전화 화면

거꾸로 가는 개발경험

보통의 앱 개발은 기술스택 결정, DB 설계, 기능 개발, 그리고 마지막에 UI를 입히는 순서로 진행되는 걸로만 알고 있었다. 하지만 이번 Smart Expense 프로젝트는 정반대의 과정으로 진행되었다. 우선 구글 Stitch를 활용해 만든 UI 디자인에서 출발했다. 눈에 보이는 결과물을 먼저 손에 쥔 채, 이를 작동시키기 위해 DB 구조를 고민하고 기능을 구현해나가는 '역방향 개발'을 경험한 것이다.

이 방식의 장점

  • 사용자 경험 중심: 개발 초기부터 사용자의 시각에서 앱을 바라보게 되니, 모든 기능 구현이 자연스럽게 UX를 중심으로 이루어질 수 있다.

  • 확실한 목표: 처음부터 최종 목표(UI)가 눈에 보이기 때문에, 불필요한 과정은 최소화되는 것 같다. "이 버튼을 누르면 이 화면이 나와야 해"와 같이 명확해진다.

물론 단점도 느꼈다.

  • DB구조 재구성의 어려움: UI를 먼저 만들다 보니, 나중에 데이터를 연동할 때 구조가 맞지 않아 F. key나 컴포넌트를 다시 수정해야 하는 경우가 발생했다. 마치 멋진 옷을 먼저 만들어 놓고, 나중에 그 옷에 사람의 몸을 맞추는 듯한 느낌이었다.

  • 이중 작업의 함정: 하드코딩된 UI를 만든 뒤, 다시 그 부분을 실제 데이터와 연동하는 과정은 때로는 두 번 일하는 것처럼 느껴지기도 했다.

'거꾸로 가는 개발' 경험도 새롭게 느껴졌다. (AI가 길을 안내해주니까 잘못될거란 걱정은 안든다) 멈춰 있던 그림에 하나씩 생명을 불어넣어, 마침내 스스로 데이터를 보여주고 사용자와 상호작용하는 '살아있는 앱'으로 만드는 과정이 은근 재미있기도 했다.

스터디 사례로 무작정 시작했던 Smart Expense 앱은, 이제 차별화된 기능을 갖춘, 진짜 '스마트한' 가계부로 태어났다. 이 역방향의 여정이, 어쩌면 새로운 제품을 만드는 또 하나의 재미있는 방식이 될 수 있지 않을까?

"잘 만든 UI는 최고의 기획서다."

4
4개의 답글

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요