백엔드 없는 로컬 앱, Drift로 데이터 안 깨지게 버전 올리기

내 단식 앱은 백엔드가 없다. 서버도 계정도 없이 100% 기기 로컬(Drift, SQLite 위)로 돌아간다. 서버비 0, 프라이버시 부담 0이라는 장점이 있지만, 대가가 있다. “앱 업데이트 때 사용자의 기존 데이터를 안 깨뜨리는 것”이 전적으로 내 책임이 된다. 서버라면 마이그레이션을 한 번만 돌리면 되지만, 로컬은 수많은 기기에서 제각각 버전이 올라온다.

레이어를 나눠 도메인을 순수하게 지킨다

순수 로직과 SDK 글루를 섞으면 테스트도 마이그레이션도 지옥이 된다. 그래서 계층을 이렇게 나눴다.

  • domain: 순수 함수·불변 모델(단식/식사 페이즈 도출, 광고 게이트, BMI 등). 외부 의존 0 → 단위테스트가 쉽다.
  • data: Drift DB + 리포지토리(meal/profile/hunger/weight).
  • providers: Riverpod 파생 상태(단일 소스).
  • screens / services: UI와 광고·알림 SDK 글루(얇게 유지).

데이터 접근이 리포지토리 한 층에 갇혀 있으니, 스키마가 바뀌어도 영향 범위가 좁다.

마이그레이션 규칙: ‘컬럼 추가만’ 한다

로컬 DB에서 데이터가 날아가는 가장 흔한 원인이 마이그레이션이다. 나는 규칙을 하나로 못 박았다. 스키마를 올릴 때 컬럼은 추가(addColumn)만, 삭제·이름변경·타입변경은 하지 않는다.

실제 이력을 보면 방향이 일관된다.

  • v2: 프로필·주간목표 테이블 추가
  • v3: 주간 목표일수 컬럼 추가
  • v4: “마지막 전면광고 시각” 컬럼 추가(광고 빈도 캡용)
  • v5: 이벤트 타입 컬럼 추가(2-상태 사이클 도입)

전부 더하기만 했다. 기존 행을 건드리지 않으니 사용자 데이터가 보존된다. 지우고 싶은 컬럼이 생겨도 그냥 안 읽고 놔둔다. 로컬 앱에서 “깔끔한 스키마”보다 “안 깨지는 데이터”가 훨씬 비싸다. 죽은 컬럼 몇 개는 감수할 만한 대가다.

단일 행 설정 테이블

사용자 설정·프로필처럼 “앱에 하나뿐인” 데이터는 id=1 고정의 단일 행 테이블로 뒀다. 여기에 목표시간, 알림 on/off, 광고제거 만료시각, 온보딩 완료 여부, 주간 목표일수, 마지막 전면광고 시각을 다 담는다. 조회가 where id=1로 단순해지고, 행이 없으면 기본값으로 시드한다. 여러 행을 뒤질 일이 없어 버그 여지가 준다.

백엔드가 없다는 것의 진짜 의미

“서버 관리에서 해방”이 아니라 “데이터 수명 관리를 클라이언트가 떠안는다”는 뜻이다. 서버가 없으니 잘못된 마이그레이션을 나중에 SQL로 고칠 수도 없다. 한 번 배포되면 그 코드가 사용자 기기에서 그대로 돈다. 그래서 규칙을 단순하고 되돌릴 수 있게(추가만, 단일행, 기본값 시드) 가져가는 게, 1인 개발에서 데이터 사고를 막는 현실적인 방어선이었다.

단일 원천 위에 파생 상태를 얹는다

데이터 계층 위에는 Riverpod 프로바이더로 파생 상태를 단일 소스로 노출한다. 화면은 DB를 직접 만지지 않고 이 프로바이더만 구독한다. 덕분에 “지금 페이즈”, “이번 주 달성일수” 같은 값이 여러 화면에서 제각각 계산되지 않고 한 곳에서 나온다. 단, 앞선 글에서 다뤘듯 광고 캡처럼 정확성이 중요한 판정은 스트림 대신 DB 직접 읽기로 예외 처리한다. 로컬 앱의 데이터 흐름은 “저장은 추가만, 파생은 단일 소스, 정확성은 직접 읽기”라는 세 규칙으로 정리됐다.

댓글 남기기

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.ㅇ