← back to services

live · service

우리연애

커플의 일상 · 사진 · 기념일 · 감정을 함께 기록하는 앱. 실시간 채팅, 지도 위 추억, 홈 위젯, AI 가 매일 그려주는 커플 운세까지 — 둘이 연결되어야 비로소 동작하는 앱입니다.

Flutter Riverpod Socket.IO Prisma PostgreSQL MinIO Firebase FCM Claude API Naver Map

서비스 소개

우리연애는 "한 사람의 앱"이 아니라 "두 사람의 앱"으로 설계됐습니다. 커플 코드로 연결되면 그 시점부터 모든 데이터가 두 사람 단위로 묶이고 — 일정 · 기념일 · 감정 · 사진 · 위치 · 채팅 · 위시리스트가 한쪽 화면에서 일어나도 곧장 반대편에 반영됩니다.

앱은 Flutter + Riverpod, 서버는 Express + Socket.IO + Prisma + PostgreSQL. 사진은 한 대의 VPS 안에서 운영하는 MinIO 에 저장되고, 푸시 알림은 Firebase FCM, 위치 추억은 네이버 정적 지도 API 로 썸네일을 생성합니다. 매일의 커플 운세는 Claude API 가 두 사람의 별자리 · 띠 · 함께한 일수를 보고 직접 써내려갑니다.

Architecture

한 대의 VPS 안에 모든 핵심 컴포넌트를 도커로 묶어두고, 외부 서비스(Firebase / 네이버 지도 / Claude)는 필요한 라우트에서만 호출합니다.

/ client iOS app Flutter · Riverpod Android app Flutter · Riverpod socket.io · https · wss://love.jiny.shop / edge Nginx · reverse proxy TLS · WS upgrade · helmet · rate-limit / app Express + Socket.IO Prisma ORM · 커플 단위 라우팅 · JWT · sharp · multer / data PostgreSQL via Prisma MinIO 사진 · 썸네일 · S3 호환 Firebase Admin FCM 전송 / external Naver Static Map 위치 추억 썸네일 Anthropic Claude API 매일의 커플 운세 생성 single VPS · Docker

Behind the scenes

이 앱을 굴리면서 가장 인상 깊었던 세 가지 — 인프라를 통째로 옮긴 일, AI 한 조각을 끼워넣은 일, 그리고 아직도 붙들고 있는 iOS 위젯 갱신.

  1. 01

    Supabase + Render 에서 Contabo + MinIO 로 이사 갔다

    출시 직후 약 일주일은 Supabase(스토리지 + DB) + Render(서버) 조합으로 굴렸습니다. 빠르게 띄우는 데에는 좋지만, 가장 큰 문제는 무료 티어의 한계였습니다 — 사용자가 늘어날수록 스토리지 용량 / 대역폭 / 콜드 스타트 / 동시 연결 같은 벽이 한꺼번에 다가왔고, 유료로 올리자니 1인 운영의 비용 구조와 맞지 않았습니다. 거기에 사용자 사진처럼 "절대 잃으면 안 되는 데이터"의 통제권이 외부 서비스에 있다는 점도 마음에 걸렸습니다. 그래서 한 번에 옮겼습니다 — 데이터는 Neon → 자체 PostgreSQL, 파일은 Supabase Storage → MinIO, 서버는 Render → Contabo + Docker + GitHub Actions. 마이그레이션 자체보다, S3 호환 클라이언트가 MinIO 와 미묘하게 어긋나는 기본값과, Naver Static Map 의 신규 엔드포인트로의 키 회전이 실제로는 더 큰 일이었습니다. 이전 후로는 사진 한 장 업로드부터 푸시 한 번 발송까지, 모두 같은 머신 안에서 일어납니다.

  2. 02

    매일의 운세를 AI 에게 시키는 일

    커플 운세는 처음에 정적인 템플릿으로 만들 생각이었습니다. 하지만 두 사람의 별자리 · 띠 · 함께한 일수를 조합하면 거의 항상 같은 메시지가 나오는 게 어색했고, 그래서 매일 한 번 Claude API 에게 직접 작성을 부탁하기로 했습니다. 결과물 형식은 JSON 으로 강제하고, 톤과 길이는 프롬프트에 박아둡니다. 남는 문제는 비용과 어뷰징 — 그래서 광고 보상으로 게이팅하고, rate limiter 와 일별 캐시를 같이 두었습니다. "AI 가 매일 다른 말을 해주는 운세"는 사용자에게 작은 즐거움이 됐고, 이 패턴을 더 밀어붙여 다음 서비스 — 운명일기 — 로 이어졌습니다.

  3. 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

지나온 버전과 앞으로 들어갈 항목을 별도 페이지에서 확인할 수 있습니다.