Flutter 앱 개발기 (4/5) — 성장 일지(GrowNote): SQLite + Riverpod 설계

관계형 데이터 · 데이터 기반 상태 · 주기적 회고 — 기록 앱의 설계 결정 패턴


이 글이 전달하는 것

  • 저장소 선택 기준: 키-값(Hive) vs 관계형(SQLite) — 데이터 구조 분석이 선행되어야 하는 이유와 판단 체크리스트
  • 상태 관리 선택 기준: 이벤트 기반(BLoC) vs 데이터 기반(Riverpod) — 앱의 상호작용 특성에 따른 매핑
  • 회고 시스템 설계: 일간/주간/월간 템플릿 구조와 사용자가 지속 입력하도록 만드는 UX 최소화 원칙
  • 하네스 패턴 적용: Flutter 앱 개발 구조에 프롬프트 하네스를 얹었을 때 바뀌는 것

배경 — 기록 앱이 잘 풀리는 문제 설정

일반적인 일기 앱은 자유 형식 텍스트 입력을 중심으로 설계된다. 장점은 진입 장벽이 낮다는 것이고, 한계는 시간이 쌓여도 "무엇이 달라졌는가"가 드러나지 않는다는 것이다. 과거 기록이 읽히지 않으면 앱의 복귀율은 떨어진다.

GrowNote는 문제 설정을 바꾼 앱이다. "무엇을 했는가" 대신 "무엇이 성장했는가"를 기록 단위로 둔다. 카테고리별 누적, 주기적 회고, 추이 시각화가 세 축이다. 이 문제 설정이 저장소·상태 관리·UX 전반에 결정을 연쇄적으로 강제한다.


1. 데이터 모델 — 관계형이 기본 요구사항

핵심 엔티티:

  • GrowthRecord: 개별 성장 기록. 카테고리, 내용, 수치(선택), 날짜
  • Category: 사용자 정의 성장 영역 (예: 운동, 코딩, 독서, 투자)
  • Tag: 기록에 부착되는 키워드. 다대다 관계
  • Reflection: 일간/주간/월간 회고. 기간과 연결된 기록 참조

관계 구조: - Category ↔ GrowthRecord: 1:N - GrowthRecord ↔ Tag: N:M - Reflection ↔ GrowthRecord: 기간 범위 조인

"특정 카테고리의 최근 30일 기록", "특정 태그가 붙은 기록의 수치 평균" 같은 쿼리가 기본 기능이다. 즉 집합 연산과 조인이 앱의 주 동작이다. 이 지점에서 저장소 선택이 갈린다.


2. SQLite vs Hive — 판단 기준

키-값 저장소(Hive)와 관계형 저장소(SQLite)의 선택은 데이터 구조에서 결정된다.

Hive가 적합한 경우: 단일 엔티티 단위 읽기/쓰기가 지배적이고, 조인이 거의 없는 경우. 설정값, 단순 컬렉션, 캐시 용도. 콰이어트(TXT 리더) 같은 앱이 대표적이다.

SQLite가 적합한 경우: 다대다 관계, 범위 쿼리, 집계가 기능 핵심인 경우. GrowNote가 여기 해당한다.

Hive로 GrowNote의 쿼리를 구현하려면 데이터 전체를 메모리에 적재 후 Dart 코드로 필터링·집계해야 한다. 레코드가 수천 건을 넘기면 첫 화면 진입 시간이 선형으로 증가한다. SQLite는 동일 쿼리가 인덱스 기반 네이티브 연산이다.

구현은 sqflite 패키지, 마이그레이션은 버전 기반. 스키마 변경 시 onUpgrade 콜백에서 ALTER TABLE을 실행한다.

판단 체크리스트: - 다대다 관계가 있는가? - 범위 조회(날짜, 수치)가 주 기능인가? - 집계(합계, 평균, 빈도)를 실시간으로 보여주는가? - 데이터가 수백 건 이상 누적될 전제인가?

2개 이상 "예"라면 관계형 저장소가 기본값이다.


3. Riverpod — 데이터 기반 상태의 선언적 흐름

상태 관리 선택도 동일하게 문제 성격에서 출발한다.

이벤트 기반 상태(BLoC): 사용자 입력이나 게임 이벤트 같은 명시적 트리거가 주인 앱. 마인스위퍼(헥사곤 지뢰찾기)에서 BLoC을 쓴 이유다.

데이터 기반 상태(Riverpod): CRUD가 주를 이루고, 화면 간 상태 공유가 많으며, 파생 상태(통계·차트)가 원본 데이터에 종속적인 앱. GrowNote가 이쪽이다.

주요 Provider 구조:

  • categoryProvider: 전체 카테고리 목록. AsyncNotifier로 DB와 동기화
  • recordsProvider: 필터 조건별 기록 목록. Family Provider로 카테고리/기간별 분리
  • statsProvider: 통계. recordsProviderref.watch하여 자동 재계산

핵심 이점은 파생 상태의 자동 무효화다. recordsProvider가 변하면 statsProvider가 별도 이벤트 없이 재계산된다. "통계 업데이트" 이벤트를 수동으로 발행할 필요가 없어 상태 경로가 선언적으로 유지된다.


4. 회고 시스템 — 일간/주간/월간 계층 구조

GrowNote의 차별 기능이자 설계 난도가 높은 부분이다. 단순 기록의 문제는 사용자가 과거를 돌아볼 유인이 없다는 것이며, 회고 템플릿은 이 유인을 구조화한다.

일간 회고: 당일 기록 항목을 카테고리별로 그룹화 표시, "오늘의 핵심 배움" 한 줄 입력. 입력 항목을 최소화해 진입 장벽을 낮춘다.

주간 회고: 7일간 카테고리별 활동 빈도와 수치 추이를 시각화. "이번 주 가장 성장한 영역"과 "소홀했던 영역"이 자동 하이라이트된다. 이 자동 하이라이트는 statsProvider의 범위 필터링으로 처리된다.

월간 회고: 한 달 단위 패턴. 카테고리별 히트맵, 태그 빈도, 수치 변화 그래프.

회고 템플릿은 그 자체로 추가 기록을 유도하는 피드백 루프다. 주간 회고에서 빈 카테고리가 시각적으로 보이면 "이번 주는 운동을 안 했다"는 자각이 다음 주 입력으로 이어진다. 즉 회고 UI가 기록 비율을 끌어올리는 2차 효과를 낳는다.

설계 원리: 회고 템플릿은 입력 항목이 적을수록 작동한다. 초기 버전은 입력 항목이 많아 부담이 컸고, 핵심 항목만 남기고 나머지를 선택 사항으로 내린 뒤에야 지속 입력이 유지됐다. 최소 입력 → 자동 집계 → 시각적 피드백 순서가 회고 UI 설계의 일반 원칙이다.


5. 하네스 패턴과 데이터 내보내기

프롬프트 하네스를 Flutter 개발 구조에 얹으면 바뀌는 것은 두 가지다. 첫째, CLAUDE.md에 데이터 모델과 SQLite 스키마를 명시해 세션 간 문맥을 보존한다. 둘째, 기능 추가 시 계획 → 실행 → 검증 흐름이 강제된다. 즉흥적인 코드 추가가 줄고 스키마 마이그레이션 실수가 감소한다.

데이터 내보내기/가져오기는 사용자 데이터 주권의 기본 요소다. JSON 형식으로 전체 데이터를 내보내고 동일 형식으로 가져올 수 있다. SQLite DB 파일이 아닌 JSON으로 변환하는 이유는 호환성(다른 툴에서도 읽기 가능)과 가독성(사용자가 내용을 확인 가능)이다. 기기 변경·백업 시나리오가 실제로 발생하는 앱이라면 이 레이어는 초기부터 설계하는 편이 비용이 낮다.


한계와 개선 방향

초기 Hive 선택의 비용: 초기 단계에 키-값 저장소로 시작했다가 관계 구현이 복잡해지며 SQLite로 전환한 구간이 있었다. 교훈은 데이터 구조 분석이 저장소 선택보다 선행되어야 한다는 것이다. 구조 없이 도구를 먼저 고정하면 재작업 비용이 누적된다.

통계 계산 위치: UI 위젯에서 직접 계산하던 초기 구현은 화면 전환마다 재계산이 발생해 체감 속도가 느렸다. Provider 레이어로 이동하고 캐싱을 적용한 뒤 해결됐다. 파생 상태는 상태 관리 레이어에 둔다는 Riverpod의 기본 패턴을 벗어나면 성능 문제가 시각적으로 드러난다.

회고 템플릿 과설계: 입력 항목이 많으면 사용자가 회고 자체를 건너뛴다. "더 많은 정보를 묻는 것"과 "더 자주 입력되게 만드는 것"은 충돌하며, 후자가 장기 지표에 유리하다.


적용 가능 범위

이 글의 설계 원칙은 GrowNote 고유가 아니다.

  • 관계형 데이터 + 집계 쿼리가 핵심인 개인 앱 → SQLite + Riverpod 조합이 출발점으로 합리적이다
  • 이벤트 트리거 중심 앱(게임·실시간 상호작용) → BLoC이 여전히 유효하다
  • 단일 엔티티 중심 앱(리더·노트) → Hive로 충분하다

같은 Flutter 스택이라도 콰이어트(키-값, Hive), 마인스위퍼(이벤트, BLoC), GrowNote(관계형, SQLite + Riverpod) 세 조합이 모두 최적이었다는 사실은, 스택 선택은 문제 성격의 함수라는 점을 반복 확인한다.

열린 질문

  • 회고 템플릿의 "최소 입력"을 어디까지 더 줄일 수 있는가? 질문 자체를 AI가 당일 기록에서 생성하는 구조는 어떤 트레이드오프를 만드는가?
  • SQLite의 FTS(Full-Text Search) 확장을 태그·내용 검색에 도입할 때 얻는 이점과 마이그레이션 비용은 어떻게 나뉘는가?
  • 데이터 내보내기 포맷을 JSON에서 SQLite 덤프로 이중화하면 어떤 사용자 시나리오가 추가로 커버되는가?

시리즈 전체 안내: 시리즈 목차

댓글

이 블로그의 인기 게시물

"LLM 핵심 학습 (1/6) — 기본: 토큰화·임베딩·어텐션·위치 인코딩"

"LLM 핵심 학습 (2/6) — 파인튜닝: LoRA·QLoRA·증류·Adapter"

"ML 기초 학습 (1/9) — 머신러닝과 sklearn: 학습의 좌표계"