지난 이야기
지난 에피소드까지는 깡총 챌린지와 추천 콘텐츠로 기록 리듬을 만드는 데 집중했다. 이후에는 전체적인 검수를 거쳐 TestFlight 단계로 갔다. 이번 편과 다음편에서는 이 단계에서 앱스토어 심사 제출 전 진행했던 QA 및 추가 작업들에 대해 다룰 것이다. 먼저 에피소드 6에는 데이터가 사라진 이유를 디버깅한 과정, 백업과 저장·복원 안전장치 구축기를 담았다.
이번 글에서 다루는 내용
- 실기기에서 Hive 데이터 손실을 재현하고 해결한 과정
- Box<dynamic> 통일·flush/compact 재시도 안전장치
- 사용자가 직접 지킬 수 있는 ZIP 백업 UX 설계
- 비개발자로서 시도한 문제 범위 좁히기, 회고하는 이유
데이터 안전 과제 재정의

TestFlight 테스트 때 "앱 강제 종료 후 다시 켜면 저장한 레시피가 전부 사라진다"는 리포트를 받았다. (테스트 초기에 개발자인 제부, 파워 J 성향의 동생이 각자의 시각으로 QA를 해주어서 강제 종료 후 데이터 손실 이슈, UX/UI 개선 의견까지 바로바로 피드백을 받을 수 있었다. 고마워요!) 사실 처음에 QA 템플릿을 만들었었는데 작은 앱이다보니 캐주얼하고 빠르게 피드백을 주고 받는 게 리소스상 이득임을 깨닫고 이 절차는 바로 스킵했다.
아카이빙 앱 특성상 기록 데이터를 지키는 것이 최우선이었기에 목표를 세 갈래로 정리했다. ① 실기기에서 발생하는 Hive 데이터 손실을 재현하고 원인을 찾는다. ② 저장·복원 안전장치를 정비한다. ③ 사용자가 원할 때 수동 백업이 가능하도록 기능을 마련한다.
Phase 1 — Hive 데이터 손실 재현 타임라인

기존에 갖고 있던 2개의 iOS 실기기에서 직접 테스트해보니 저장 → 강제 종료 → 재실행 시 보관함에 있던 레시피가 사라지는 현상이 동일하게 발생했다. 평소에는 홈 버튼으로 나가거나 백그라운드에 두는 정도만 반복해서 미처 문제를 발견하지 못했다. 디버깅하다보니 보관함 레시피가 사라지는 반면, 알림함에 있는 메시지는 그대로라는 점을 알게 되었다. 알림함은 SharedPreferences로 저장하고 있어서 읽음/안읽음 상태값과 콘텐츠가 그대로 유지되고 있었다. 박스 단위로 비동기 flush되는 Hive와 달리 SharedPreferences는 즉시 처리되기에 Write Timing이 문제라는 결론에 도달했다.
비동기 flush: Hive는 데이터를 박스 단위로 모아서 메모리에 쌓아 두었다가 flush()를 호출하거나 내부 타이머가 돌 때 디스크에 내려쓰는데, 이것이 비동기 쓰기 작업이다.
Hive가 모든 디버그 모드에서 데이터를 지우는 것은 아니지만, Debug 빌드 특유의 Hot Reload·DevTools 결합 때문에 박스가 다시 열리거나 OS 캐시에만 쓰고 끝나는 케이스가 발생했다. 한 번이라도 Hot Reload를 누르면 방금 넣은 값이 flush 되기 전에 재초기화되어 결과를 믿을 수 없었던 것. 한편 Debug 모드뿐만아니라 Release 모드에서도 동일한 증상이 발생했기에 이건 명백히 고쳐야 할 "문제" 상황이었다. 이후부터 모든 데이터 영속성 테스트는 최종 배포 환경과 동일한 Release 모드에서 돌리기로 했다. 릴리즈 빌드는 box.flush() 호출이 바로 디스크까지 내려가며, 수정이 제대로 작동하는지 확인할 유일한 근거가 됐다.
뱅뱅 돌았던 삽질 과정

처음에는 알림함처럼 맞추면 되겠지, 라고 생각해 같은 방식으로 보관함을 구현하려 했다. 그러나 레시피 데이터가 담기는 박스는 키 수와 데이터 크기가 훨씬 커서 비교 대상이 아니었다. 클로드 코드, 코덱스를 써도 안되어서 비슷한 사례를 찾아 구글링을 해보았다. 강제 종료 직전에 sleep으로 시간을 벌고 WidgetsBindingObserver/MethodChannel로 이벤트를 받는 케이스를 발견해 await Future.delayed를 심거나 다시 한 번 box.flush()를 호출해도 증상이 그대로였다. box.flush() 뒤에 await box.compact()를 붙여 디스크에 확실히 쓰이게 저장·백업·복원 시퀀스마다 배치도 해봤지만 허사였다. iOS 네이티브와 신호를 주고받아 포그라운드/백그라운드 전환마다 flush()를 강제해보기도 했지만 이 또한 해결방법은 아니었다.
차라리 외부 DB로 옮길까, 싶어 기존에 만들어뒀던 Supabase 프로젝트를 열어 연동도 시도해봤다. 하지만 구조상 로컬에서 관리하던 사용자별 상태값을 옮기려면 복잡도가 높아지는 데다 인증·마이그레이션 준비도 덜 되어 있어 당장 전환하기엔 리스크가 컸다. 결국 레시피 보관도 개인별 저장 시스템이기 때문에 수퍼베이스 연동은 Hive를 안정화한 뒤, 해야 할 이유가 생긴다면 별도 로드맵으로 빼기로 결정했다. (나중에 알았지만 이건 진짜 안해도 되는 시도였다...ㅎㅎ)
Release 모드 강제 테스트 로그
테스트를 하며 시뮬레이터는 Debug 모드만 지원해 릴리즈 빌드를 검증할 수 없다는 사실을 깨닫게 되어 실기기로 Release 테스트를 진행했다. 약 20차례 시나리오를 돌리면서 차수가 높아짐에 따라 중복 시도를 피할 방법을 찾게 되었다. 내가 생각한 방법은 문서화. APP_CRASH_DEBUG.md파일을 만들었고 Test 19 프로토콜이 QA 기준이 됐다.
레시피 작성 → 보관함 저장 → 강제 종료 후 앱 실행 직후에도 레시피가 그대로 남아 있는지 확인해 테스트마다 수정 사항, 재현 조건과 결과를 전부 남겼다.
강제 종료 이슈 핵심 정리
- 홈을 눌러 백그라운드로 두는(paused) 상황에서는 문제가 없었다. 강제 종료(detached)로 완전히 앱을 지울 때만 레시피 박스가 비워졌다.
- Debug 모드에서 Hot Reload·DevTools가 결합되면 박스를 다시 열고 닫느라 로그가 뒤섞이거나 데이터가 비어 결과를 믿기 어려웠다. 그래서
_hiveInitialized플래그가 있는HiveService싱글톤(앱 전체에 하나만 두는 관리자)을 두어 "한 번만 열기, 열려 있으면 그대로 재사용" 원칙을 적용시켰다. 중복 초기화(열어둔 박스를 또 여는 상황)도 막았다. - Release 빌드에서도 동일하게 레시피가 사라졌기 때문에 실제 사용자가 겪는 플로우를 기준으로 오류를 잡아야 했다.
- 따라서 '강제 종료 후 재실행' QA는 릴리즈 빌드에서만, Debug 모드 결과는 참고용으로 남겼다.
Phase 2 — 저장·복원 안전장치 개선

문제를 파고들자 OS 버퍼에 쓰기가 남아 있는 사이 앱을 종료하면 Hive에 값이 남지 않는다는 사실이 드러났다. 동시에 백업 복원 시 ID가 중복되거나 Hive key 타입(int/String)이 섞이면 박스가 손상된다는 것도 확인했다. 원인을 파헤치고 해결하기까지 꼬박 나흘을 썼는데, 해결책은 두 가지였다. 첫째, 저장 직후 다시 읽어 값이 남아 있는지 검증하고 실패 시 재시도 루틴을 추가했다. 둘째, 병합 복원 시 ID 충돌을 감지해 새 타임스탬프 ID를 발급하고 모든 키를 String으로 통일했다. 이전에 크롤링 데이터를 빅쿼리에 적재할 당시, 테이블 스키마 정의 시 "key type은 전부 string으로 맞추는 게 정신건강에 좋을 것"이라 했던 데이터 파트 팀장님 조언이 떠올랐다.(또르륵..) '데이터'로 이 경험들이 연결되면서, 어떤 종류의 작업이라도 언젠가는 도움이 되고 의미가 있다는 것도 다시 한 번 느꼈다.
싱글톤 초기화 역시 이 시점에서 확정됐다. "저장은 되는데 박스를 닫지 못한 채 앱이 종료되는 것 같다. 다음 실행에서 크래시가 나며 아예 켜지지 않는다"고 개발자인 제부에게 상황을 설명했더니 "굳이 박스를 닫아서 크래시가 난다면 열어둔 채 관리하라"고 조언해줬다. (열었으면 잘 닫는 게 기본인 줄 알았는데.. 안 닫는 방법도 있었구나..!) 그 말을 따라 HiveService를 앱 전체에서 한 번만 초기화하도록 묶고 _hiveInitialized 플래그로 중복 초기화를 막았다.
문제를 좁히는 3단계
재현 기록: 무슨 순서로 무엇을 했는지 글로 정리한다. 환경 비교: 디버그/릴리즈, 기기/시뮬레이터, 데이터 양을 바꿔 '어디에서만 터지는가'를 찾는다. 요약 공유: 이미 해본 것과 못 해본 것을 적어 지인·AI·커뮤니티에 묻는다.그래도 모르겠으면 해당 현상만 남긴 작은 테스트 프로젝트를 만들어 파고들어본다.
실제 구현에서는 복원 대상을 순회하면서 현재 박스의 ID 집합과 비교해 충돌을 감지하고, 충돌 시에는 새로운 타임스탬프 ID를 발급해 복제본을 저장한 뒤 로그로 변환 과정을 남기도록 했다.
recipes, settings, stats, burrowMilestones, burrowProgress 5개 Box 모두를 dynamic 타입으로 통일한 뒤, challenge_progress 박스에도 flush()를 강제 호출해 챌린지 진행률이 즉시 디스크에 남도록 했다. 이 작업을 거친 뒤에야 "앱을 껐다 켜도 데이터가 남는다"고 말할 수 있었다.
데이터 다루기 Q&A
타입 정책
- Hive는 박스 키 타입이 뒤섞이면 다시 열지 못한다. 기존에는
int와String이 섞여 있었기에 어느 타입으로 열지 정해지지 않아 실패했다. - 지금은
recipes,settings,stats,burrowMilestones,burrowProgress박스를 모두Box<dynamic>으로 열고 JSON(Map)만 넣어 타입 혼선을 없앴다.
JSON vs Map & 키·값 구조
- JSON은 디스크에
{ "title": "수박" }처럼 문자열로 저장된 형태고,Map<String, dynamic>은 그 문자열을 Dart에서 다루기 좋게 펼친 버전이다. - Recipesoup 앱은 객체 →
toJson()→Map<String, dynamic>→ Hive 순서로 저장하므로, 박스 안에는 실제로"recipe_1759029682515": { "title": "클램 차우더", "emotionalStory": "오늘의 기록" ... }같은 구조가 통째로 들어간다. - 여기서 바깥 키(
"recipe_1759029682515")가 레시피 전체를 가리키는 이름표고, 안쪽 JSON의"title","calorie","ingredients"등이 내부 key, 텍스트·숫자는 내부 value이다. - 이제 받아주는 박스는
Box<dynamic)이지만 실제 값은 항상 JSON(Map) 하나라 '서랍은 자유형, 내용물은 JSON' 구성이 되었다. Hive가 박스를 열 때 타입 혼동을 일으킬 위험이 줄었다.
이제 '문은 한 번만 열고, 안에 넣는 물건 종류도 통일' 하는 두 축이 결합돼 데이터 무결성이 깨지던 루프가 멈췄다.
Phase 3 — ZIP 백업 플로우 설계

데이터 손실 이슈를 재현하고 저장 로직을 손본 뒤에는 최소한의 안전 장치를 마련하기 위해 ZIP 백업 플로우를 설계했다. Hive 박스를 JSON으로 직렬화해 ZIP 파일로 묶고, 이메일·AirDrop 등으로 내보내는 수동 백업 흐름을 기본으로 삼았다. 백업 파일명 규칙을 정해 복원 시 충돌 여부를 체크하고, 오류가 나면 어떤 메시지를 띄울지까지 세분화했다. 이 과정에서 토끼굴 데이터를 여러 번 초기화·복원하며 진행 상태값이 정상적으로 돌아오는지 테스트도 반복했다.
작업 범위 확정
클라우드 백업이나 자동화 기능까지 욕심내면 일정에 차질이 생길듯해 수동 ZIP 백업을 확실하게 제공하는 데 집중했다. 버튼 위치, 저장 직후 안내 문구, 오류 대응 흐름을 다듬으면서 '지금 바로 제공 가능한 가치'와 '향후 확장 옵션'을 구분했다.
마무리하며
이번 작업을 통해 배운 것은 "문제 상황을 끝까지 정리하기"이다. 재현 기록을 꼼꼼히 남기고 디버그·릴리즈 환경을 번갈아 비교하고, 정리된 상태로 조언을 구했기에 뾰족한 힌트를 얻게 되었다.
두 번째 다짐은 "끝까지 포기하지 않을 것"이었다. 이번 문제를 만나기 전까지 어떤 이슈든 하루 안에 해결 못한 적이 없었지만, 이번엔 달랐다. 나흘째가 되자 '혼자선 못 푼다, 그냥 포기해야 하나? 앱 출시가 무모했던 걸까?'라는 생각까지 밀려왔다. 그래도 하는 데까지 원인을 좁혀가며 노트에 정리했다. 그러다 결국 노트북을 들고 동생 부부를 찾아갈 채비까지 했던 날, 기적처럼 길이 열렸다. 아마 이때 포기했더라면 앱 출시도 하지 못했을 것이다.
세 번째로 기억해두어야 할 건 "AI 시대라도 기본기는 중요하다"는 사실이다. 그게 내가 회고글을 쓰며 다시 복기하고 공부해보는 이유이기도 하다.
기본기 쌓는 정석 루트는 'CS 공부 → 이론 학습 → 실무 적용'일 것이다. 나는 실전에서 부딪힘 → 문제 해결 → 회고하며 원리 이해하는 방식으로 배워가고 있다. 아주 깊게는 못해도 '왜 그런 건지' 이해하게 되면 그것도 나름의 기본기가 되지 않을까? 이번 경험을 통해 내가 배우고 얻은 것들을 나열해보면 이러하다. Hive의 작동 원리(flush, compact, 타입 시스템), Debug와 Release 환경의 차이, 데이터 영속성 테스트 방법, 문제 재현부터 디버깅까지의 QA 프로세스 및 체크리스트.
그냥 문제를 해결하고 끝내면 '어떻게든 되긴 했네'로 남지만, 회고를 통해 복기하다보면 '왜 그렇게 됐는지를 어렴풋이 이해하게 된다. 이렇게 몸소 익힌 경험을 글로 정리하다 보면, 조금씩 내 것이 되어가는 것 같다.
다음 에피소드 예고
다음 글에서는 iOS 제출 체크리스트를 다시 정리한 과정을 다룬다. Info.plist 권한, 앱 아이콘, 실기 테스트, 심사 문서, 프록시 아키텍처까지 TestFlight 전 마지막 준비를 이어서 공유한다.