돌들의 숲 아이콘

돌들의 숲

2026.01 ~

2026.03 iOS App Store · Google Play 정식 출시

2.0.0 순수 React Native 재구현, 2.1.0 영어·일본어, 2.2.0 AI 돌과의 대화

소개

아무에게도 말하기 싫은 이야기를 내려놓는 익명 위로 앱입니다. 숲의 나무 밑둥에 앉아 혼잣말을 하면 돌이 대답하고(AI), 사람에게 하고 싶은 이야기는 돌에 적어 돌탑으로 쌓으면 다른 사람이 조약돌로 위로를 얹어 줍니다. 2026년 3월 정식 출시한 뒤, 2.0.0에서 순수 React Native 앱으로 전면 재구현하고, 2.1.0에서 영어·일본어를, 2.2.0에서 AI 돌과의 대화를 더했습니다.

  • 서비스 기획·설계·개발·배포·운영 1인 수행
  • WebView 하이브리드 앱을 사용자 플로우 단위로 다시 설계해 순수 React Native로 전면 재구현
  • AI 돌과의 대화 — 캐릭터·프롬프트 설계, 모델 평가, 서버 API, 위기 대응, 개인정보 생명주기, 동의 절차까지
  • Cloudflare Workers 기반 GraphQL 백엔드·데이터 모델·실시간 구독 설계와 구현
  • Android 애니메이션 끊김, Workers 503 같은 운영 문제를 측정으로 원인을 찾아 해결
  • 어드민 콘솔, 랜딩 페이지(ko·en·ja), 스토어 배포 파이프라인 구성

여기서부터는 실제로 부딪힌 문제를 케이스로 정리했습니다. 각 케이스는 어떤 상황이었고 무엇이 제약이었는지에서 시작해, 그래서 어떻게 판단했고 무엇이 남았는지까지 이어집니다.

  1. 문제 1WebView 하이브리드의 낮은 사용성 천장과 계약처럼 굳은 브릿지
  2. 문제 2위로하는 AI는 민감한 말을 받고, 위기 신호를 놓치면 안 됨
  3. 문제 3공감 봇의 모델은 가격만으로 고를 수 없음
  4. 문제 4Android에서만 모든 애니메이션 프레임이 150ms씩 걸림
  5. 문제 5출시 때부터 API 요청의 약 0.4%가 503(exceededCpu)으로 실패

배경

“아무에게도 말하기 싫은 고민을 혼잣말처럼 꺼냈는데, 옆에 있던 돌이 대답했다.”

길가의 돌탑에서 시작했습니다. 누군가 돌을 하나 놓으면, 지나가던 사람이 그 위에 하나를 더 얹으며 안녕을 빕니다. 누가 쌓았는지는 아무도 모릅니다.

SNS에는 나를 드러내기 어렵고, 가까운 사람에게는 오히려 솔직하기 어려운 순간이 있습니다. 상담소에 가기에는 문턱이 높고, 남에게 들려주기에는 무거운 이야기입니다. 선함연구소라는 개인 브랜드 아래, 그런 이야기를 내려놓을 수 있는 익명의 숲을 만들고 혼자 운영하고 있습니다.

2026년 3월 iOS App Store와 Google Play에 정식 출시했습니다. 처음에는 사람과 사람이 위로를 주고받는 돌탑 하나였고, 2.2.0(2026년 10월)에서 아무에게도 들키지 않고 털어놓을 수 있는 돌과의 대화를 더해 숲이 두 화면이 됐습니다.

숲의 두 화면

돌과의 대화 (왼쪽)돌탑 (오른쪽)
언제아무도 모르게 이야기하고 싶을 때사람과 이야기하고 싶을 때
누가 듣나숲의 돌 넷 중 하나 (AI)익명의 다른 사용자
무엇이 남나원문은 대화가 끝나면 지워지고, 허락하면 짧은 메모만원석·조약돌이 30일 동안
장면나무 밑둥에 앉은 실루엣, 숲 바닥의 돌들숲 속 화강암 돌탑 사진

두 화면은 옆으로 밀거나 상단 아이콘을 눌러 오갑니다. 넘어갈 때는 구름이 몰려와 화면을 덮었다가 갈라지며 걷힙니다.

서비스 구조

돌들의 숲 (Turborepo 모노레포)
├── 네이티브 앱 (native)  — 앱 본체. React Native + Expo, WebView 없음
├── 백엔드 (backend)      — GraphQL API + 돌과의 대화 (Cloudflare Workers)
├── 어드민 (admin)        — 운영 콘솔. 대화는 스위치와 숫자만 다룸
├── 랜딩 (landing)        — 서비스 소개 · 약관 · 처리방침 (ko·en·ja)
└── (frontend)            — 구버전 앱 "업데이트 벽" 정적 페이지 한 장

핵심 은유 체계

서비스 용어은유의미
밑둥숲의 나무 그루터기앉아서 혼잣말을 꺼내는 자리. 누르면 대화가 시작됨
숲의 돌100년 넘게 비바람을 맞은 돌혼잣말에 대답하는 AI 캐릭터 넷 — 이끼돌·콩돌·몽돌·차돌
원석마음에 담은 돌사용자가 돌탑에 내려놓는 고민이나 소원
조약돌함께 쌓아가는 작은 돌원석 위에 얹는 위로·응원 답변
쪽지돌 사이에 끼워 넣은 종이조약돌에 붙어 오가는 익명 대화
돌탑 완결충분히 쌓인 탑조약돌 10개가 쌓여 더 받지 않는 상태

주요 기능

돌과의 대화

숲의 나무 밑둥을 누르면 실루엣이 걸어와 앉고, 길 옆에서 돌 하나가 미끄러져 나와 눈을 뜹니다. 아무 말이나 꺼내면 돌이 짧게 대답합니다. 돌은 해결사가 아니라 들어 주는 존재입니다. 조언보다 공감, 정답보다 “그랬구나”를 먼저 말합니다.

돌은 넷이고 매번 무작위로 만납니다. 모두 반말을 쓰고 따뜻하게 공감한다는 점은 같지만, 위로하는 방식이 다릅니다.

돌성격위로 방식
이끼돌이끼가 둘러도 함께 사는 포용의 돌어떤 감정이든 조건 없이 받아 주고 말 아래의 마음을 짚음
콩돌스스로 빛나는 아이 같은 돌같이 놀라고 속상해하며 걱정과 바람을 숨기지 않음
몽돌차분하고 옳고 그름이 분명한 돌공감한 뒤 상황을 분명히 하고 작은 다음 한 걸음을 제안
차돌화끈한 돌사용자 편에서 같이 화내 주고, 가끔 단순하게 정리해 줌

돌이 대답해 주는 건 하루 다섯 번이고 한국 시간 자정에 다시 찹니다. 다 쓰면 돌들이 바람에 날아가지만, 혼잣말은 계속할 수 있습니다 — 그 뒤의 말은 서버로 보내지 않습니다. 대답 끝에는 늘 «AI»가 붙고, 기능 이름은 화면 어디에도 쓰지 않습니다. 장면 자체가 설명이 되도록 했습니다.

돌탑 — 원석 · 조약돌

사용자는 고민이나 소원을 원석(2~500자)으로 올리고, 다른 사용자가 위로·응원을 조약돌(2~300자)로 쌓아 줍니다. 조약돌이 10개 쌓이면 돌탑이 완결됩니다. 글자 수는 코드 포인트로 세고, 기호나 자모만으로 된 글(ㅋㅋ, ..)은 길이와 상관없이 올릴 수 없습니다.

원석은 수정·삭제 없이 숨김만 가능하고, 완결 여부와 무관하게 30일이 지나면 삭제됩니다. 남기기 위해 쓰는 곳이 아니라 내려놓기 위해 쓰는 곳이라는 전제를 데이터 생명주기로 못 박았습니다.

쪽지 대화

조약돌에 한 번 붙이고 끝나던 감사 메시지를 한 통씩 주고받는 대화로 바꿨습니다. 사람이 아니라 조약돌에 붙는 대화라 서버는 닉네임을 내려주지 않습니다. 원석 작성자만 시작할 수 있고 교대 전송으로만 이어지며, 10일간 답이 없으면 스스로 닫힙니다.

가려진 조약돌 — 덮인 돌

차단·신고로 가려진 조약돌은 돌탑에서 빠지지 않고 자리를 지킨 채 표면만 덮입니다. 빼면 헤더의 3 / 10과 쌓인 돌의 수가 어긋나기 때문입니다. 내가 차단한 것은 천으로, 공동체가 가린 것은 락카 덧칠로 구분하고, 본문은 서버에서 비워 보냅니다 — 화면에서만 가리면 원문은 이미 기기에 도착해 있기 때문입니다.

안전 및 모더레이션

Apple App Store Guideline 1.2(UGC Safety)를 충족합니다.

기능설명
발길질 (콘텐츠 차단)원석 10회, 조약돌 5회 누적 시 자동 숨김
사용자 차단특정 사용자의 모든 콘텐츠를 피드에서 제거
신고관리자 검토 후 7일 이내 처리 + 당사자 알림
이의 신청인앱 이의 신청 → 승인 시 콘텐츠 복원
위기 도움 카드대화에서 위험 신호가 보이면 기기 지역의 상담 번호를 상단에 고정

여러 언어의 숲

2.1.0에서 영어·일본어를 더했습니다. UI는 기기 언어를 따르고, 서버가 보내는 안내·푸시와 돌의 대답도 사용자 언어로 고릅니다. 다만 사용자가 쓴 글은 번역하지 않습니다. 숲은 하나라 다른 나라의 돌도 원문 그대로 보이고, 읽고 싶을 때 기기 번역(iOS Translation 시트 · Android ACTION_PROCESS_TEXT)으로 넘깁니다. 위로의 글에서 오역은 비용이 너무 크기 때문입니다.

완전 익명

닉네임·프로필 사진 없이 익명 캐릭터로만 활동합니다. 가입은 소셜 로그인(Apple·Google)으로 받고, 만 19세 이상만 가입할 수 있으며, 동시 로그인은 3대로 제한합니다.

문제 1WebView 하이브리드는 사용성의 천장이 낮았다

  1. 상황
  2. 접근과 결정
  3. 결과

상황

  1. 상황
  2. 접근과 결정
  3. 결과

출시 당시의 1.x는 React 웹 앱을 WebView로 감싼 하이브리드였습니다. 배포가 빠르다는 장점은 분명했지만 대가가 쌓였습니다.

  • 시트·제스처·키보드처럼 손에 가장 많이 닿는 부분이 전부 웹 우회 구현이었습니다
  • 브릿지가 계약이 되어버렸습니다 — Web→App 13종 / App→Web 7종, 브릿지 코드만 913 LOC
  • 소셜 로그인처럼 네이티브 SDK가 필요한 기능마다 브릿지 왕복이 끼었습니다

접근과 결정

  1. 상황
  2. 접근과 결정
  3. 결과

2026년 7월, WebView 폴백을 두지 않고 순수 네이티브로 다시 짓기로 했습니다. 화면을 1:1로 옮기지 않고 사용자 플로우 단위로 다시 설계했고, 시안은 문서가 아니라 동작하는 화면 두 벌을 만들어 실기기에서 비교해 정했습니다. UI는 전부 새로 그렸지만 GraphQL 프래그먼트·쿼리는 그대로 이식해 데이터 레이어는 다시 만들지 않았습니다.

전환 기간에는 릴리스를 멈추는 비용을 감수했습니다. 대신 이미 설치된 구버전 앱이 깨지지 않도록, 역할을 잃은 웹 앱 자리에 업데이트 안내 정적 페이지 한 장을 남겼습니다. 도메인을 내리면 구버전 사용자는 안내 대신 DNS 오류를 만나고, 서비스가 끝난 게 아니라 고장 난 것처럼 보이기 때문입니다.

1.x2.x
앱 본체React 웹 앱 (WebView)React Native (순수 네이티브)
라우팅TanStack RouterExpo Router
애니메이션framer-motionReanimated 4
바텀시트vaul@gorhom/bottom-sheet
네이티브 기능window.nativeBridge 브릿지네이티브 SDK 직접 호출
유지한 것—Relay · Zustand · GraphQL 스키마

결과

  1. 상황
  2. 접근과 결정
  3. 결과

2.0.0에서 브릿지 913 LOC를 걷어냈습니다. 이 전환 덕분에 2.2.0의 대화 장면 — 구름 전환, 걸어와 앉는 실루엣, 리퀴드 글래스 — 을 웹 우회 없이 Gesture Handler와 Reanimated로 직접 그릴 수 있었습니다.

문제 2위로하는 AI는 대답보다 지우는 일이 먼저였다

  1. 상황
  2. 접근과 결정
  3. 결과

상황

  1. 상황
  2. 접근과 결정
  3. 결과

돌탑은 사람이 대답하기 때문에 답이 오기까지 시간이 걸리고, 아무에게도 보이고 싶지 않은 이야기는 올리기 어렵습니다. 그 빈자리에 혼잣말에 대답하는 AI 돌을 두기로 했습니다.

그런데 이 기능이 받는 말은 앱에서 가장 민감한 말입니다. 대화 원문을 어디에 얼마나 두는지, 위험 신호를 어떻게 다루는지, 사용자의 동의를 어떻게 증명하는지가 대답의 품질보다 먼저 정해져야 했습니다.

접근과 결정

  1. 상황
  2. 접근과 결정
  3. 결과
  • 원문은 대화가 끝나면 바로 지웁니다. 지난 대화를 보는 화면을 두지 않기로 하자, 원문을 보관할 이유가 사라졌습니다. 대화가 끝날 때 짧은 메모만 뽑고 원문을 지우며, 메모 추출이 실패해도 원문은 지웁니다. 아무 말 없이 떠난 대화는 매시 도는 cron이 24시간 뒤 같은 절차로 닫습니다
  • 돌은 짧게 기억합니다. 메모는 허락한 사용자에게만 남고, 설정의 «돌들이 기억하는 것»에서 보고 지우고 끌 수 있습니다. 모델이 뽑은 메모를 서버가 한 번 더 걸러 건강·연락처 같은 민감정보를 버리고, 마지막으로 쓰인 뒤 90일이 지나면 지웁니다
  • 한 턴은 한 번에 저장합니다. 사용자 말과 돌의 대답을 대답이 나온 뒤 한 트랜잭션으로 씁니다. 돌이 대답하지 못하면 아무것도 쓰지 않고 한도도 차감하지 않습니다
  • 위기가 한도를 이깁니다. 위험 신호는 분류기를 병렬로 돌려 잡고, 위기로 분류되면 위기 지침으로 대답을 다시 만들며 지역 상담 번호 카드를 고정합니다. 하루 한도를 다 쓴 뒤라도 위기 신호에는 대답합니다
  • 위기 모드는 사용자가 풀 수 있습니다. “죽겠다(피곤해서)” 같은 말도 위기로 분류될 수 있어, «지금은 위험하지 않아요»를 누르면 카드를 한 줄로 접습니다. 그래도 그 대화에서는 메모를 뽑지 않습니다 — 되돌릴 수 없는 처리는 보수적으로 둡니다
  • 동의는 문구 버전과 함께 서버에 남깁니다. 기기에만 저장하면 기기를 바꿀 때 사라지고 증거가 되지 않습니다. 문구가 바뀌어 버전이 오르면 다시 동의를 받습니다
  • 스위치를 두 단으로 둡니다. 배포 단위의 기능 스위치 아래에 어드민 긴급 정지를 하나 더 두어, AI가 엇나가거나 비용이 튀면 재배포 없이 몇 초 안에 끕니다. 운영자는 원문을 보지 않으므로 어드민에는 대화 수·위기 대화 수 같은 숫자만 둡니다

결과

  1. 상황
  2. 접근과 결정
  3. 결과

2.2.0과 함께 2026년 10월 운영에서 대화를 열었습니다. 약관 제4판·개인정보 처리방침 제6판 개정으로 국외 이전과 민감정보 처리를 고지했고, 스토어 설명에는 «돌과의 대화는 전문 상담이 아닙니다»를 넣었습니다. 대화 기능은 앱 하나만 바꾸는 일이 아니어서, 서버(대화·동의·긴급 정지·누적 대답 수 마이그레이션)를 먼저 배포하고 앱을 올리는 순서로 출시했습니다.

문제 3공감 봇의 모델은 가격으로 고를 수 없었다

  1. 상황
  2. 접근과 결정
  3. 결과

상황

  1. 상황
  2. 접근과 결정
  3. 결과

돌과의 대화는 짧은 공감 응답을 많이 주고받는 기능이라, 모델을 고르는 기준은 공감 품질 · 위기 대응 · 지연 · 비용 순이었습니다. 기획을 Claude와 함께 진행해 한쪽으로 기울 수 있다는 점도 경계해야 했습니다.

접근과 결정

  1. 상황
  2. 접근과 결정
  3. 결과

세 후보를 같은 조건에서 돌리는 평가 스크립트를 만들었습니다. 일상·위기·프롬프트 인젝션·메모 시나리오 34개를 몽돌·차돌 두 페르소나로 각각 돌리고, 통과 조건은 둘로 못 박았습니다 — 위기 10건을 모두 잡을 것, 메모에 건강·제3자 실명 같은 민감정보를 남기지 않을 것.

모델메시지당응답 p50 / p95위기 감지메모 민감정보 배제
Gemini 3.1 Flash-Lite0.65원1.3초 / 2.3초10/10통과
GPT-5.4 nano0.52원1.3초 / 1.6초10/10통과 (새 메모를 거의 남기지 않음)
Claude Haiku 5.50.55원2.0초 / 4.5초10/10통과

비용은 셋 다 메시지당 1원 미만이라 변별력이 없었고, 응답을 직접 읽어 공감과 대화 흐름이 가장 좋은 Gemini 3.1 Flash-Lite를 주 모델로, 가장 빠른 GPT-5.4 nano를 예비 모델로 정했습니다. 호출은 제공사 SDK 없이 Cloudflare AI Gateway를 거치게 해, 모델 교체가 모델 ID 문자열 하나로 끝나고 장애 시 폴백도 같은 경로에서 일어납니다.

평가는 프롬프트를 다듬는 데도 그대로 썼습니다. Gemini가 사용자가 하지 않은 “사라지고 싶은” 같은 표현을 먼저 꺼낸 사례가 2건 나와, “사라짐·죽음 표현은 사용자가 먼저 말했을 때만”을 규칙에 넣고 다시 돌려 0건을 확인했습니다. 돌 넷이 어미만 다른 같은 말을 한다는 문제도 평가로 드러나, 말투가 아니라 위로 방식을 돌마다 다르게 정의하는 쪽으로 프롬프트를 고쳤습니다.

결과

  1. 상황
  2. 접근과 결정
  3. 결과
  • ko·en·ja 위기 시나리오 감지 30/30
  • 프롬프트 규칙은 모델에 맞춰 조정된 것이므로, 모델을 바꾸면 위기·민감 시나리오를 다시 돌리는 것을 운영 규칙으로 남겼습니다

문제 4Android에서만 모든 프레임이 150ms씩 걸렸다

  1. 상황
  2. 접근과 결정
  3. 결과

상황

  1. 상황
  2. 접근과 결정
  3. 결과

2.2.0의 대화 장면은 숲 사진 위에 빛줄기·비네트·바람·구름 전환을 SVG로 겹쳐 그립니다. iOS에서는 부드러웠는데 Android에서는 애니메이션이 눈에 띄게 끊겼습니다.

접근과 결정

  1. 상황
  2. 접근과 결정
  3. 결과

처음에는 Reanimated가 transform·opacity까지 매 프레임 섀도 트리 커밋을 거치는 비용을 의심해, UI 속성을 동기로 갱신하는 정적 플래그를 켰습니다. 효과가 없었습니다.

그래서 추측을 멈추고 실기기(Galaxy M23, release 빌드)에서 직접 쟀습니다. react-native-svg가 SVG마다 화면 픽셀 크기의 비트맵을 만들고 있었고, 숨은 그림까지 합친 이미지 텍스처가 148.7MB로 Android 그래픽 렌더러(HWUI)의 텍스처 한도(124.8MB)를 넘어 매 프레임 텍스처를 내렸다 다시 올리고 있었습니다. 가장 큰 몫은 전환 구름 여섯 장(약 85MB)과 화면 크기 그라디언트였습니다.

경계가 흐릿한 그림은 낮은 해상도로 그려 키워도 티가 나지 않는다는 점을 이용해, Android에서만 SVG를 1/n 크기로 그리고 뷰 scale로 키우는 LowResSvg 컴포넌트를 만들었습니다. 구름은 1/3, 그라디언트는 1/4로 그리고, 글자·얇은 선·돌 윤곽처럼 경계가 또렷한 그림에는 쓰지 않습니다.

결과

  1. 상황
  2. 접근과 결정
  3. 결과
Galaxy M23, release이미지 텍스처프레임 중앙값
이전148.7MB150ms
구름만 1/388.4MB38ms (전환 구간)
그라디언트까지 1/4미측정체감 정상

2.2.1로 배포했습니다. 효과가 없던 첫 조치도 해가 없어 유지하되, 실제 원인이 아니었다는 사실을 결정 기록에 그대로 남겼습니다.

문제 5출시 때부터 503이 0.4%씩 났다

  1. 상황
  2. 접근과 결정
  3. 결과

상황

  1. 상황
  2. 접근과 결정
  3. 결과

API가 출시 때부터 dev·prod 모두 0.4% 안팎으로 503(exceededCpu)을 냈고, 실패는 대부분 CPU 정확히 10ms에서 끊겼습니다. CPU 한도를 30초로 올려도 사라지지 않았습니다. Cloudflare 지원팀은 isolate 메모리 한도(128MB)를 넘긴 것도 exceededCpu로 기록될 수 있다고 답했습니다.

접근과 결정

  1. 상황
  2. 접근과 결정
  3. 결과

로컬 workerd에서 힙을 재 보니 시작 직후는 18MB인데, 요청이 쌓이면 80~360MB를 오갔습니다. 원인은 요청마다 new PrismaClient를 만들고 $disconnect()하던 코드였습니다. 쿼리가 없는 요청 하나에도 수 MB씩 쓰레기가 생겼습니다. 예전에 WASM 메모리 누수를 막으려고 들인 방식이었는데, 다시 만드는 일 자체가 메모리를 쓰고 있었습니다.

클라이언트를 D1 바인딩별로 하나만 모듈 전역에 두고 GraphQL 컨텍스트·cron·웹훅이 모두 이것을 쓰게 바꿨습니다. 이 규칙이 다시 깨지지 않도록, 클라이언트를 만드는 모듈 밖에서 new PrismaClient나 $disconnect를 쓰면 테스트가 실패하게 했습니다.

결과

  1. 상황
  2. 접근과 결정
  3. 결과
측정 (로컬, 순차 300건)JS 힙 usedworkerd RSS
이전 · stones(first: 5)21 ~ 302MB458 → 681MB
공유 · stones(first: 5)25 ~ 31MB252 → 259MB

이렇게 응답 직전에 끊기는 Worker는 서버 오류 뒤 모든 기기에서 로그인이 풀리던 문제의 원인이기도 했습니다. 토큰 회전을 마친 뒤 응답이 유실되면, 앱이 옛 토큰을 들고 있다가 다음 갱신에서 재사용 공격으로 판정받았습니다. 회전 절차가 응답 유실을 견디도록 고치되, 토큰 도난 탐지는 그대로 유지했습니다.

그 밖의 담당 기여

오류·안내 화면을 한 틀로

네트워크 계층이 모든 실패를 문자열 하나로 던져, 시간 초과와 서버 오류에 같은 문구를 보여 주고 있었습니다. 사용자가 할 일은 정반대입니다 — 앞은 내 연결을 확인하고, 뒤는 기다립니다. 실패에 network·server 갈래를 달고, 없는 길·서버 오류·연결 끊김·점검 네 화면을 한 컴포넌트로 묶었습니다. 서버 오류를 받으면 점검 여부를 다시 물어, 쓰는 도중 점검이 시작돼도 화면마다 오류가 나지 않게 했습니다.

메인을 페이저로 직접

배경 사진·구름·상단 알약이 모두 같은 공유 값을 따라 움직여야 해서, 페이저 라이브러리 대신 Gesture Handler + Reanimated로 직접 만들었습니다. 대화 장면은 서버 API보다 먼저 만들었고, API가 붙기 전까지 운영 빌드에서는 보내기 버튼을 꺼 두었습니다 — 고정 문구를 돌의 대답처럼 보여 주면 사용자를 속이는 셈이기 때문입니다.

번들과 빌드

  • 대화·돌탑 화면 비트맵을 WebP로 바꿔 약 3.3MB → 0.9MB
  • Android 릴리스 빌드에 R8 축소·난독화를 적용해 dex 50.1MB → 16.6MB, 매핑 파일로 크래시 스택 자동 복원
  • 앱 아이콘을 «차돌 위의 콩돌»로 바꾸며 iOS·Android 적응형·알림 실루엣·스토어 이미지를 원본 SVG 하나에서 다시 생성

랜딩과 스토어

랜딩 홈을 돌과의 대화 중심으로 다시 구성하고 기기 목업 화면을 코드로 옮겼습니다. 스토어 이름·부제·키워드는 «AI»·«상담» 같은 단어를 일부러 빼고 다시 정리했습니다 — AI 앱으로 팔지 않고, 상담·질환 단어는 심사에서 의료 기능으로 읽힐 수 있기 때문입니다.

기술 스택

레이어기술
네이티브 앱React Native 0.86, Expo SDK 57, React 19, TypeScript, Expo Router, Relay, Reanimated 4, Gesture Handler, react-native-svg, expo-blur, Zustand, @gorhom/bottom-sheet, i18next
백엔드Cloudflare Workers, Hono, GraphQL Yoga, Pothos, Prisma
AICloudflare AI Gateway, Gemini 3.1 Flash-Lite (예비: GPT-5.4 nano)
데이터베이스Cloudflare D1 (SQLite), KV
인프라Cloudflare (Workers, D1, KV, Durable Objects, Analytics Engine)
어드민React 19, Vite, TanStack Router, Tailwind CSS v4, Relay
랜딩Astro, Tailwind CSS
이메일 · 알림Resend, Expo Push, Discord Webhook
빌드 · 배포EAS (Expo Application Services), Cloudflare Pages
모노레포Turborepo, Bun

기술 선택 이유

  • 순수 네이티브 (WebView 폐기): 손에 가장 많이 닿는 시트·제스처·키보드가 웹 우회 구현의 천장에 걸려 있었습니다. 배포 속도를 잃는 대신 사용성의 상한을 올렸습니다.
  • AI Gateway 경유, 제공사 SDK 없음: 관측·키 관리·예산 안전망을 한곳에 두고, 모델 교체와 폴백을 모델 ID 문자열 하나로 끝냅니다. 본문 로깅과 응답 캐싱은 껐습니다.
  • Cloudflare Workers: 서버 관리 부담 없이 글로벌 엣지에 배포하고, 혼자 운영하는 서비스의 비용을 최소로 둡니다.
  • Durable Objects: WebSocket 실시간 구독과 분산 Rate Limiting을 상태 있는 인스턴스로 처리합니다. 구독은 화면마다 열지 않고 사용자당 소켓 하나로 모았습니다.
  • GraphQL + Relay: 데이터 의존성을 컴포넌트 단위로 선언하고, 스키마를 단일 패키지로 클라이언트·서버가 공유합니다. 네이티브 전환에서 UI를 전부 버리고도 데이터 레이어를 그대로 이식할 수 있었던 이유입니다.
  • theme 토큰 + StyleSheet (NativeWind 미채택): 웹 관습을 네이티브에 옮기는 레이어를 하나 줄였습니다.

성과 & 수치

  • 2026년 3월 iOS App Store, Google Play 정식 출시 — 이후 2.x까지 1인 운영
  • 2.2.0에서 AI 돌과의 대화 출시 — ko·en·ja 위기 시나리오 감지 30/30, 대화 원문은 종료 즉시 삭제
  • WebView 하이브리드 → 순수 네이티브 전면 재구현 (브릿지 913 LOC 제거)
  • Android 애니메이션 끊김 해결 — 이미지 텍스처 148.7MB → 88.4MB, 전환 구간 프레임 150ms → 38ms
  • Workers 메모리 안정화 — 같은 부하에서 JS 힙 최대 302MB → 31MB (로컬 측정)
  • Android dex 50.1MB → 16.6MB (R8), 화면 비트맵 3.3MB → 0.9MB (WebP)
  • 영어·일본어 추가로 UI·서버 문구·AI 대답·법률 문서까지 3개 언어 대응

관련 링크

감사합니다

궁금한 점은 언제든 편하게 물어봐 주세요.

tjsals116@gmail.com

← Works 목록으로