live · service
우리연애
커플의 일상 · 사진 · 기념일 · 감정을 함께 기록하는 앱. 실시간 채팅, 지도 위 추억, 홈 위젯, AI 가 매일 그려주는 커플 운세까지 — 둘이 연결되어야 비로소 동작하는 앱입니다.
서비스 소개
우리연애는 "한 사람의 앱"이 아니라 "두 사람의 앱"으로 설계됐습니다. 커플 코드로 연결되면 그 시점부터 모든 데이터가 두 사람 단위로 묶이고 — 일정 · 기념일 · 감정 · 사진 · 위치 · 채팅 · 위시리스트가 한쪽 화면에서 일어나도 곧장 반대편에 반영됩니다.
앱은 Flutter + Riverpod, 서버는 Express + Socket.IO + Prisma + PostgreSQL. 사진은 한 대의 VPS 안에서 운영하는 MinIO 에 저장되고, 푸시 알림은 Firebase FCM, 위치 추억은 네이버 정적 지도 API 로 썸네일을 생성합니다. 매일의 커플 운세는 Claude API 가 두 사람의 별자리 · 띠 · 함께한 일수를 보고 직접 써내려갑니다.
- 커플 단위 데이터 모델코드 한 줄로 연결되면 그 이후의 모든 데이터가 둘 단위로 묶입니다. 한쪽이 끊으면 양쪽 모두 정리.
- 실시간 채팅읽음 표시 · 사진 첨부 · 위치 공유 · 채팅 미니게임 (룰렛, 사다리타기) 까지 한 창 안에서.
- 기념일 · D-Day · 캘린더일정·기념일을 등록하면 N일 전 자동 리마인드 푸시. 양쪽이 동일하게 받습니다.
- 지도 위 추억사진을 올린 위치가 지도에 핀으로 남습니다. 네이버 정적 지도 API 로 썸네일 생성.
- 홈 위젯 (iOS · Android)D-Day · 오늘의 기분 · 다음 일정 · 다가오는 기념일 — 잠금화면에서도 한 눈에.
- 그림 위젯홈 화면 2×2 위젯에서 바로 그림 한 장을 그려 상대에게 전송. 받으면 상대 위젯이 즉시 바뀌고, '조용히 보내기'도 지원.
- AI 커플 운세Claude API 가 별자리 · 띠 · 함께한 일수를 보고 매일의 운세를 작성. 광고 시청으로 열어보는 게이팅 방식.
- 소셜 로그인이메일 가입 외에 Google · Apple · Kakao 로 바로 시작. 커플 연결까지 가는 첫 화면을 더 짧게.
- 위시리스트 · 질문카드함께 채워가는 작은 목표들과, 매일 한 가지씩 던지는 질문 카드.
- 완전한 자체 호스팅출시 후 Supabase + Render 에서 Contabo VPS + MinIO + PostgreSQL 로 이전 — 데이터 통제권 확보.
Architecture
한 대의 VPS 안에 모든 핵심 컴포넌트를 도커로 묶어두고, 외부 서비스(Firebase / 네이버 지도 / Claude)는 필요한 라우트에서만 호출합니다.
Behind the scenes
이 앱을 굴리면서 가장 인상 깊었던 세 가지 — 인프라를 통째로 옮긴 일, AI 한 조각을 끼워넣은 일, 그리고 아직도 붙들고 있는 iOS 위젯 갱신.
-
01
Supabase + Render 에서 Contabo + MinIO 로 이사 갔다
출시 직후 약 일주일은 Supabase(스토리지 + DB) + Render(서버) 조합으로 굴렸습니다. 빠르게 띄우는 데에는 좋지만, 가장 큰 문제는 무료 티어의 한계였습니다 — 사용자가 늘어날수록 스토리지 용량 / 대역폭 / 콜드 스타트 / 동시 연결 같은 벽이 한꺼번에 다가왔고, 유료로 올리자니 1인 운영의 비용 구조와 맞지 않았습니다. 거기에 사용자 사진처럼 "절대 잃으면 안 되는 데이터"의 통제권이 외부 서비스에 있다는 점도 마음에 걸렸습니다. 그래서 한 번에 옮겼습니다 — 데이터는 Neon → 자체 PostgreSQL, 파일은 Supabase Storage → MinIO, 서버는 Render → Contabo + Docker + GitHub Actions. 마이그레이션 자체보다, S3 호환 클라이언트가 MinIO 와 미묘하게 어긋나는 기본값과, Naver Static Map 의 신규 엔드포인트로의 키 회전이 실제로는 더 큰 일이었습니다. 이전 후로는 사진 한 장 업로드부터 푸시 한 번 발송까지, 모두 같은 머신 안에서 일어납니다.
-
02
매일의 운세를 AI 에게 시키는 일
커플 운세는 처음에 정적인 템플릿으로 만들 생각이었습니다. 하지만 두 사람의 별자리 · 띠 · 함께한 일수를 조합하면 거의 항상 같은 메시지가 나오는 게 어색했고, 그래서 매일 한 번 Claude API 에게 직접 작성을 부탁하기로 했습니다. 결과물 형식은 JSON 으로 강제하고, 톤과 길이는 프롬프트에 박아둡니다. 남는 문제는 비용과 어뷰징 — 그래서 광고 보상으로 게이팅하고, rate limiter 와 일별 캐시를 같이 두었습니다. "AI 가 매일 다른 말을 해주는 운세"는 사용자에게 작은 즐거움이 됐고, 이 패턴을 더 밀어붙여 다음 서비스 — 운명일기 — 로 이어졌습니다.
-
03
iOS 그림 위젯의 '즉시 갱신' — 아직도 고민 중
그림 위젯을 만들면서 가장 끈질긴 숙제는 — 상대가 그림을 보냈을 때 내 홈 화면 위젯이 곧바로 바뀌게 하는 일이었습니다. Android 는 푸시를 받으면 위젯 provider 가 직접 데이터를 가져와 비교적 즉시 반영됩니다. 그런데 iOS 의 WidgetKit 은 다릅니다 —
WidgetCenter.reloadTimelines는 어디까지나 시스템에 "갱신해달라"는 요청일 뿐이고, iOS 가 위젯 새로고침 예산(budget)을 두고 타이밍을 스스로 정합니다. silent 푸시로 앱을 깨워 갱신을 시도해도, 백그라운드 실행과 위젯 리로드 모두 시스템이 throttle 하기 때문에 "보낸 즉시"가 보장되지 않습니다. 그래서 푸시 payload 메타로 위젯을 직접 채우고, NSE(Notification Service Extension)에서 갱신을 밀고, cooldown 과 토큰 self-refresh 까지 붙여 v1.0.14 / v1.0.15 에 걸쳐 계속 다듬었지만 — iOS 쪽은 여전히 가끔 한 박자 늦습니다. "사용자가 본 순간엔 이미 최신"이라는 경험을 iOS 의 제약 안에서 어떻게 만들지, 지금도 붙들고 있는 문제입니다.
Screenshots
카드를 누르면 큰 이미지로 볼 수 있습니다.
More
지나온 버전과 앞으로 들어갈 항목을 별도 페이지에서 확인할 수 있습니다.