React Native로 로컬 우선(local-first) 무드 저널을 만들다 — 그리고 지루한 동기화 부분이 거의 나를 망하게 할 뻔했다
요약
React Native와 Expo를 사용하여 개인의 감정 기록에 초점을 맞춘 로컬 우선(local-first) 무드 저널 앱 'MoodMO'를 개발했습니다. 이 앱은 소셜 기능이나 의료적 틀을 배제하고, 사용자가 관계적 맥락에서 자신의 감정을 사적으로 기록하는 데 중점을 둡니다. 특히 개인 정보 보호와 로컬 데이터 처리를 핵심 원칙으로 삼았습니다.
핵심 포인트
- 로컬 우선(Local-first) 아키텍처를 채택하여 강력한 개인 정보 보호 기능을 제공합니다.
- 소셜 기능, AI 코치, 의료적 진단 등 외부 요소를 의도적으로 배제했습니다.
- React Native/Expo 환경에서 동기화 및 개인 정보 보호 문제를 다루는 엔지니어링 경험을 공유합니다.
긴장되는 대화를 나눈 후, 나는 메모 앱을 열어 반 단락 정도 타이핑하고 삭제하는 일을 반복하곤 했다.
감정이 진짜가 아니어서가 아니라—메모 앱이 잘못된 컨테이너처럼 느껴졌기 때문이다. 원하지 않는 곳에 동기화하기 너무 쉬웠고, 나중에 패턴을 파악하는 데 도움이 될 만큼 구조적이지도 않았다. 그리고 내가 시도했던 모든 '무드 트래커'는 다른 무언가로 나를 끌어당겼다: 숙제 같은 느낌의 이모지 그리드, 두 사람이 온라인 상태여야 하는 커플 앱, 또는 혼자 내버려 두지 않는 수다스러운 AI 코치들.
내가 실제로 필요했던 것은 더 조용한 것이었다:
보통 누군가에 관한 감정을 쓸 수 있는 사적인 공간—그것을 치료 숙제나 소셜 제품으로 변질시키지 않고 말이다.
그래서 나는 React Native/Expo를 사용하여 나 자신을 위해 MoodMO를 만들었다. 현재 이 앱은 Android가 아닌 iOS App Store에 게시되어 있다. 이 글은 문제점, 내가 넘기기를 거부한 경계선들, 그리고 실제 엔지니어링 시간을 소모했던 몇 가지 동기화/개인 정보 보호 결정들에 관한 것이다. 만약 공감한다면, iOS에서 사용해 보고 여전히 어색한 부분이 무엇인지 알려주었으면 좋겠다.
중요: 이것은 치료, 진단 또는 위기 지원이 아닌 개인 저널링 도구입니다. 즉각적인 도움이 필요하면 지역 응급 서비스나 신뢰할 수 있는 핫라인에 연락하십시오.
내가 해결하려 했던 문제점
대부분의 무드 앱들은 다음과 같은 질문에 답한다: “당신은 어느 정도 수준으로 느끼나요?”
하지만 나의 밤들은 다음과 같았다: “나는 이 사람 때문에 속상하고, 이런 종류의 트리거에 대해 속상한데, 오늘 이야기해야 할지, 이번 주에 해야 할지, 아니면 그냥 혼자 소화시켜야 할지 모르겠다.”
그 관계적 맥락은 일반적인 트래커에서는 계속해서 빠져나갔다. 커플 앱은 공유 저널링을 해결한다. AI 코치는 대화를 해결한다. 나는 싱글 플레이어 루프를 원했다: 기록 → 나중에 상황이 완화되었는지 확인 → 더 이상 붙잡고 바라볼 필요가 없는 단어를 선택적으로 해제하기.
개인 정보 보호는 마케팅 문구가 아니었다. 만약 일기 내용, 태그 또는 음성 메모가 언제든 원격 측정 데이터(telemetry fodder)나 소셜 콘텐츠처럼 느껴진다면, 나는 그 앱을 스스로 사용하지 않을 것이다.
내가 의도적으로 만들지 않은 것들
'아니오'라고 말하는 것이 초기에 잘못된 제품을 출시하는 것을 막아주었다:
- 의료적 틀 없음 — 위기 대응 도구가 아니며, CBT(인지행동치료) 과정도 아닙니다.
- 대화형 AI 코치가 아님 — 사용자를 따라다니는 채팅 기능은 없습니다 (자세한 내용은 다음 섹션 참조).
- 커플 공동 작성 기능 / 인앱 소셜 피드가 없음 — 타인을 위한 타임라인이 아닌, 한 사람의 관계 기억입니다.
- 기본적으로 로컬 우선(Local-first) — 계정 없이 작성할 수 있으며, 클라우드 동기화는 나중에 선택 사항입니다.
- 기본적으로 일기를 쏟아내지 않는 공유 카드 기능 — 날씨/가든 크롬과 마무리 문구만 포함하며, 사용자가 옵트인한 경우에만 짧은 사용자 스니펫이 추가됩니다. 기본적으로 항목 본문이나 사람/트리거 목록은 노출되지 않습니다.
이러한 제약 조건들이 어떤 기능 브레인스토밍보다도 UX 은유를 더 많이 형성했습니다.
의도적으로 지루하게 유지한 AI
선택적 부가 기능들은 배치(batch) 스타일로만 제공됩니다: 기분 예측 텍스트, 짧은 성찰적인 "팟캐스트 스타일" 블러브, 제안된 다음 단계, 그리고 하나의 리뷰 마무리 문구입니다. 인앱에 "API 키 붙여넣기" 화면은 없습니다.
AI가 실행될 때, 이는 **빌드 시간(build time)**에 존재하는 Groq 키에 의해 제한됩니다. 이 키는 EXPO_PUBLIC_ 환경 변수이며, 이는 클라이언트 번들 내부에 포함된다는 의미입니다—IPA를 언패킹하는 사람이라면 누구나 추출할 수 있습니다. 이것은 백엔드 프록시가 없는 소규모 인디 앱을 위한 의도적인 트레이드오프일 뿐, 클라이언트가 보유한 제공업체 키가 안전하다는 주장은 아닙니다. 만약 이 규모가 커진다면, 올바른 해결책은 서버 측 프록시(그리고 유출된 모든 것의 순환)입니다. 키가 누락된 경우, 해당 기능들은 기기 내 기본 복사본으로 폴백합니다.
키가 존재하는 경우, 호출은 전체 일기 단락이 아닌 집계값(개수, 평균, 트리거 레이블)을 전송하지만—이것 역시 제3자 처리입니다. 이는 아래의 휴지 상태(at-rest) 간격과 같은 수준의 투명성을 가집니다.
제품이 갖춰진 과정
강도에 따른 날씨 기능. 성장에 따른 가든 기능. 사람/트리거 태그, 부드러운 "언제 이 이야기를 할 수 있을까?"라는 질문, 사람 타임라인, '이 날짜의 나', 단어를 풀어놓는 의식(ritual), 그리고 작은 홈 화면 위젯(Soft Stack—일기 텍스트 없이 가든/날씨 크롬만).
저는 업계 최고의 아키텍처를 주장하는 것이 아닙니다. 제가 해결하고자 했던 문제에 대한 일관성만을 주장합니다.
지루한 부분이 실제로 어떻게 작동하는가
이 섹션은 제가 거의 건너뛸 뻔했던 부분이며, 사용자들이 낯선 사람에게 이 앱을 사용해 보라고 신뢰할 수 있게 만든 핵심 내용입니다.
로컬 우선 저장 방식 (Local-first storage) 및 솔직한 저장 시점의 취약점(at-rest gap). 항목들은 먼저 장치에 JSON 형태로 AsyncStorage에 저장됩니다(게스트 키 vs 사용자별 키). 게스트 모드는 네트워크 연결을 필요로 하지 않습니다. 선택적 동기화는 사용자가 로그인할 때 Supabase를 사용합니다. 인증 세션 토큰은 SecureStore를 사용하지만, 다이어리 항목들은 그렇지 않습니다. 항목들은 iOS 앱 샌드박스에 저장되며 플랫폼 보호 기능에 의존합니다. 저는 저장 시점에 애플리케이션 계층 암호화를 추가하지 않았습니다. 이는 알려진 취약점일 뿐, 주장하는 바가 아닙니다.
소프트 삭제(Soft delete) 대 "영구적으로 사라짐". 소프트 삭제는 deletedAt을 기록하여 (휴지통에서 복원 가능). 영구 삭제는 로그인한 사용자를 위해 툼스톤 ID(tombstone id)를 삽입합니다. 풀/푸시 과정은 이 ID들을 필터링하여 다른 장치가 제거된 항목을 부활시키는 것을 방지합니다. 툼스톤들은 현재 자동 만료 작업이 없는 클라우드 테이블에 존재하며, 해당 행들이 남아 있는 한 그 보장은 유지됩니다.
업로드와 풀(Pull)은 별개의 동작이며, 하나의 자동 파이프라인이 아닙니다. 프로필에서 사용자는 업로드(푸시) 또는 풀(클라우드로부터 복구)을 선택합니다. 풀을 실행하면 ID 충돌 시 클라우드 버전이 우선한다는 확인 대화 상자가 표시됩니다.
의미론적 정의:
- 풀(Pull) → 공유된 각 ID에 대해 서버 버전이 로컬 버전을 대체하며, 로컬 전용 ID는 유지됩니다.
- 푸시(Push) → 마지막 성공적인 동기화 이후 수정된 레코드만 반영됩니다 (개념적 시계가 아닌 알려진 단순화인
updatedAt필드를 기준으로 합니다). - 결과(Result) → 사용자가 의도적으로 풀을 실행한 후, 공유 ID에 대해 장치들은 서버와 동일하게 보입니다.
따라서 네: 만약 오프라인에서 편집하고 먼저 업로드하지 않은 상태로 풀을 하면, 클라우드가 사용자 데이터를 덮어쓸 수 있습니다. 이는 확인 프롬프트가 있는 의도적인 트레이드오프이며, 조용한 마법 같은 동기화(silent magic sync)가 아닙니다. 풀 실행 전에 "동기화되지 않은 로컬 편집 내용이 있음"을 명확하게 보여주는 사용자 경험(UX)은 여전히 제가 계획하고 있는 목록에 있습니다.
위젯을 적대적인 다중 프로세스 저장소로 사용하기 (iOS). Soft Stack은 일기 본문, 사람 이름 또는 오디오 경로를 받지 못하며, 단지 화이트리스트 스냅샷(날씨, 성장 단계, 활성 계정 힌트, 스키마 버전)만 받습니다. 게시(Publish)는 날씨가 재계산될 때 실행되며; 로그아웃 시에는 지우기(Clear)가 실행됩니다. 이 과정은 나중에 게시하는 것이 '지우기'를 덮어쓰지 못하도록 직렬화됩니다. iOS에서는 스냅샷이 위젯 확장과 공유되는 App Group UserDefaults 스위트 내의 JSON 형식입니다. (Android 위젯 코드는 리포에 존재하지만, Android는 공개되지 않았습니다.)
우리가 거부하는 원격 측정(Telemetry). Sentry를 사용하지 않습니다. 일기 본문/태그/오디오 콘텐츠를 분석용으로 전송하지 않습니다. 클라우드 동기화(활성화된 경우)는 HTTPS와 사용자 범위 접근을 사용합니다.
이 모든 것이 새로운 것은 아닙니다. 이것은 '개인적인 기록'이라는 느낌을 충분히 주어 제가 스스로 앱을 계속 사용하게 만든 접착제입니다.
여전히 여러분의 시선이 필요한 부분
- 날씨/정원 언어가 도움이 되나요—아니면 3일째 이후에 방해가 되나요?
- 나쁜 저녁에도 기록하기 충분히 빠르나요?
- 사람 태그 + 타임라인이 도움이 되나요, 아니면 '감정을 정리'해야 한다는 압박감을 주나요?
- 개인 정보 보호 스토리가 명확한가요—특히 저장 시 암호화(at-rest encryption)와 클라이언트 보유 AI 키가 알려진 격차라는 점을 고려했을 때?
- 2주 차에 무엇이 부족한가요?
칭찬보다는 핵심적인 피드백이 더 유용합니다.
iOS에서 사용해 보세요 (그리고 무엇이 망가지는지 말해주세요)
iPhone 전용 (App Store). 게스트 모드는 오프라인으로 작동하며; 동기화는 로그인 후 선택 사항입니다:
https://apps.apple.com/app/id6759703686
앱에는 영어와 중국어가 있습니다. 댓글에 피드백을 남겨주세요—특히 이탈하는 분들의 의견이 제가 글을 올린 이유입니다.
런칭 발표가 아닌 빌더 노트(builder note)를 읽어주셔서 감사합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기