경찰과도둑
세종 창업 아이디어 리그 대상세종대 창업아이템 경진대회 우수상입주공모전 우수 창업 아이템상아롬 데모데이 특별상창업 동아리 SSUP 선정
친구들과 밖에서 노는 GPS 술래잡기, 경도

배경
경찰과 도둑을 좋아해 직접 모여 뛰던 사람들이 "이걸 앱으로 하면 더 재밌겠다"는 생각으로 시작한 프로젝트입니다. 경찰과 도둑은 오프라인 술래잡기에 지도, 위치 공유, 실시간 게임 상태를 더한 Flutter 앱입니다. 호스트가 지도 위에 플레이 구역과 감옥을 그리고, 한 방에 최대 150명까지 참여합니다.
팀은 Flutter 앱, Spring Boot 게임 서버, Next.js 공식 사이트를 저장소별로 나눠 개발했고, 저는 Flutter 앱의 구조 설계와 대부분의 기능 구현을 맡았습니다. 코드 구현은 AI 에이전트(Claude Code)에게 맡기고, 설계·결정·검증은 직접 했습니다. 밖에서 뛰는 게임이라 화면이 꺼져도 위치와 연결이 유지돼야 했습니다.
맡은 역할
- 앱 구조 설계 — Clean Architecture, Riverpod, GoRouter, Dio/Retrofit
- 지도 SDK 선정 — 글로벌 사용자를 전제로 초반에 Google Maps 확정 (초기 SDK 연동은 팀원)
- 서비스 도입 — 소셜 로그인, 딥링크, FCM, Remote Config, Analytics, Crashlytics, AdMob
- 좌표 격자 — 지도를 칸으로 나눠 핑 위치를 팀 채팅에 자동 연결
- 방 만들기·게임 진행 — 지도 위 구역·감옥 그리기, QR 체포, 재진입 시 경로 복원
- 채팅·커뮤니티 — 인게임 채팅 UI 개선·신고·차단, v3 커뮤니티(모집글·모임 채팅방, 알림, 강퇴). 커뮤니티 채팅은 STOMP로 채팅 목록 실시간화(유저당 알림 채널·안 읽은 개수)와 고정 공지를 직접 연결. 게임용 STOMP 연결 기반은 팀원이 구현
- 백그라운드 위치·연결 유지 — iOS 백그라운드 위치, Android Foreground Service
- 잠금 화면 게임 현황 — iOS Live Activity, Android 게임 진행 알림
- 배포 자동화 — GitHub Actions·Fastlane으로 iOS·Android 배포, PR CI 테스트
- 디스코드 브레인 봇 — 팀 디스코드에서 말로 물으면 지식 볼트를 찾아 근거와 함께 답함 (Second Brain Template)
과정
앱 개발자 두 명이 동시에 개발할 수 있게 구조부터 정했습니다. 처음에는 두 명이 앱을 맡았고, 제가 제안한 Feature-First 구조와 Clean Architecture로 도메인 계층은 인터페이스만 두고 데이터 계층이 이를 구현하게 해(의존성 역전), 기능을 나눠 병렬로 개발했습니다. 다른 팀원은 원래 웹 개발자로, 앱 개발을 돕기 위해 합류한 상태였습니다. 후반으로 가면서 홍보와 검색 노출(SEO·AEO)을 위한 웹사이트가 필요해졌고, 앞으로 도메인도 필요할 것으로 보여 그 팀원은 웹사이트와 어드민 콘솔 개발로 보내고 앱은 제가 이어서 맡았습니다. 앱 안 약관 열람, 동의 흐름, 약관 개정은 제가 맡았습니다. 약관·크레딧을 웹 정본으로 두고 앱이 웹뷰로 여는 구조는 팀원이 맡았습니다.
첫 이슈는 기능이 아니라 배포였습니다. 앱 저장소의 1번 이슈가 iOS TestFlight 배포 자동화였고, 이어서 Android 릴리스 서명과 Play Store 배포를 붙였습니다. 이후 빌드 환경 문제로 배포가 여러 번 깨져, Fastlane 설치를 Bundler 방식으로 통일했습니다.
잠금 화면에서도 게임이 보이게 했습니다. 휴대폰을 주머니에 넣고 뛰는 게임이라, 앱을 열지 않아도 다음 위치 공개 시각과 도주 중인 도둑 수를 보여 줍니다. iOS 확장 기능은 따로 서명돼야 해서 TestFlight 배포 설정도 함께 고쳤습니다.


일한 방식
Lock은 걸어도, Block은 말고.
잡힌 사람도 지도는 보게 하자고 제안했습니다.
- 상황축제를 열흘 앞둔 리허설에서, 잡힌 사람이 감옥을 찾지 못했습니다. 닫을 수 없는 체포 안내창이 지도를 가렸기 때문입니다. 채팅은 참가자 목록 속 좁은 창에 있어서, 감옥에서 채팅할 수 있다는 것도 잘 몰랐습니다.
- 한 일화면이 한 단계 깊어지더라도 채팅은 전체 화면으로 빼자고 했습니다. 잡혀도 지도는 보이게 하고, 그 위에 철창을 덮자고도 했습니다. 팀원이 "그러면 버튼이 안 눌릴 것처럼 보인다"고 반대해서, 감옥 밖을 어둡게 하는 쪽으로 정했습니다.
- 결과9월 13일에 배포하고 보니, 화면 전체가 어두워져 갇혔다는 느낌이 약했습니다. 그래서 버튼을 철창보다 위에 두어 그대로 눌리게 하고, 철창을 다시 넣었습니다. 9월 19일 축제 전에 나갔습니다.
Right는 Right에.
확인 창의 긍정 버튼을 오른쪽으로 옮기자고 했습니다.
- 상황디자인 시안에서는 확인 창의 긍정 버튼이 왼쪽에 있었고, 두 버튼의 색도 같았습니다.
- 한 일디자이너에게 긍정 버튼에 색을 넣고 오른쪽으로 옮기자고 제안했습니다. 오른손잡이가 많으니, 자주 누르는 버튼을 오른쪽에 두자는 이유였습니다.
- 결과디자이너가 시안을 고쳤고, 저는 이 규칙을 앱의 공용 확인 창에 넣었습니다. 지금 앱의 확인 창 31곳이 이 창을 씁니다. 버튼이 두 개인 19곳은 모두 왼쪽이 회색 취소, 오른쪽이 색이 들어간 확인입니다.
제가 세운 Gate, 제가 걷습니다.
제가 넣은 배터리 설정 요구를, 원인부터 고쳐 없앴습니다.
- 상황Android에서 화면을 끄면 게임 연결이 끊겼습니다. 저는 기기 배터리 정책 탓으로 보고, 배터리 설정을 "제한 없음"으로 바꿔야 방에 들어오게 막았습니다. 약 3개월 뒤, 이 요구가 가장 큰 진입 장벽이 됐습니다.
- 한 일지도·음악 앱은 이런 설정을 요구하지 않는다는 점에서 다시 봤습니다. 진짜 원인은 CPU가 잠드는 것이었고, CPU를 깨워 두도록 고쳤습니다.
- 결과배터리 설정 요구를 없앴습니다(v2.4.8). 게임에 들어가는 조건은 위치 권한 하나만 남았습니다.
Count 말고 Cause를.
튜토리얼은 횟수를 바꾸다가, 구조를 바꿨습니다.
- 상황"튜토리얼을 강제한다, 답답하다"는 피드백이 계속 들어왔습니다. 팀별 한 번, 팀과 상관없이 한 번, 별도 페이지로 노출 방식을 세 번 바꿨지만 같은 불만이 이어졌습니다.
- 한 일횟수가 아니라 구조가 문제라고 봤습니다. 건너뛸 수 없는 안내와 닫을 수 없는 창이 게임 직전에 연달아 떴습니다. 이 안내를 모두 걷어내고, 설치 후 한 번만 보는 온보딩 화면으로 바꿨습니다.
- 결과9월 2일 v3.0.1로 배포했습니다. 온보딩은 처음 한 번, 네 장으로 끝납니다.
기술적 결정
지도는 초반에 Google Maps로 확정
- 상황개발 초반에 지도 SDK를 정해야 했고, 처음 후보는 카카오·네이버 지도였습니다.
- 결정한국어·영어·일본어 지원과 해외 사용자까지 목표로 하자고 제안했습니다. 해외에서도 똑같이 동작하는 Google Maps로 초반에 확정했습니다.
- 대안카카오·네이버 지도는 국내 중심이라 글로벌 목표와 맞지 않아 제외했습니다.
- 결과어느 나라에서든 같은 지도 위에 플레이 구역과 감옥을 원형·폴리곤으로 그립니다. 42개국에서 사용됐습니다.
백그라운드를 도입하고, 배터리 설정 요구는 원인부터 고쳤습니다
- 상황앱에는 세 상태가 있습니다. 화면을 보는 foreground, 홈으로 나가거나 화면을 끈 background, 앱을 종료한 terminated입니다. foreground에서는 API와 웹소켓이 잘 동작했지만, background로 넘어가면 OS가 앱 프로세스를 멈춰 웹소켓이 끊기고 API 요청도 나가지 않았습니다.
- 문제앱을 다시 열어야 이벤트가 보였습니다. 서버에는 background로 가기 직전의 위치만 남아, 경찰이 도둑의 옛 위치를 보고 혼선이 컸습니다.
- 결정필요성을 제가 먼저 제기해 도입했습니다. Android는 Foreground Service, iOS는 background location 모드로 프로세스와 위치 전송을 유지했습니다. 위치를 상시 수집하게 되어 개인정보처리방침과 이용약관도 함께 개정했습니다.
- 후속Android에서 화면을 끄면 STOMP 하트비트가 끊겨 게임이 중단되는 문제가 남았고, 처음에는 사용자에게 배터리 "제한 없음" 설정을 요구해 우회했습니다. 3개월 뒤 이 요구가 가장 큰 진입 장벽이 됐습니다. iOS는 추가 설정 없이 동작하고 내비게이션·음악 앱도 이런 설정을 요구하지 않는다는 점에서 원인을 다시 봤습니다.
- 원인Foreground Service만으로는 CPU가 잠들고 있었습니다.
- 재결정프로세스는 Foreground Service로, CPU는
PARTIAL_WAKE_LOCK으로 따로 깨워 뒀습니다. Wake Lock은 정상 종료, 태스크 스와이프, 4시간 타임아웃 중 하나로 반드시 해제됩니다. - 결과배터리 차단 다이얼로그가 사라지고, 게임 진입 조건은 위치 권한 하나만 남았습니다.
초대는 링크 하나로 — 앱과 웹이 나눠 맡은 딥링크
- 상황초대 코드를 직접 입력하거나 앱 안 스캐너로 QR을 찍어야 했고, 기본 카메라로 찍으면 의미 없는 텍스트만 보였습니다.
- 문제링크로 앱을 바로 열려면 도메인에 검증 파일이 있어야 했는데, 그 도메인은 웹 팀원이 맡은 Next.js 저장소에 있었습니다. 게다가 카카오톡 같은 인앱 브라우저에서는 OS가 링크를 가로채지 않아, 앱이 있어도 스토어로 넘어갔습니다.
- 대안Firebase Dynamic Links는 2025년 8월 서비스가 종료돼, 처음부터 App Links·Universal Links 같은 OS 네이티브 딥링크로 설계했습니다.
- 결정앱은
app_links로 링크를 받고, 로그인 전에 받은 초대는 로그인 후 방으로 이어 줍니다. 웹의/join페이지는 인앱 브라우저를 빠져나와 앱을 깨웁니다. 웹 팀원에게는 검증 파일 배치와 폴백 페이지 코드를 담은 핸드오프 문서를 넘겼습니다. - 결과대기방 QR도 같은 링크로 바꿔, 카카오톡으로 받은 링크나 기본 카메라로 찍은 QR 하나로 방에 바로 들어오게 됐습니다.
카드 탭 체포 대신 QR 대면 체포, 그리고 30초 만료
- 상황경찰이 목록에서 도둑 카드를 눌러 체포하다 보니, 잘못 누르거나 멀리서 체포하는 일이 생겼습니다.
- 결정도둑이 띄운 QR을 경찰이 카메라로 찍어야만 체포되게 바꿨습니다. 찍는 행위 자체가 같은 자리에 있다는 증거라 확인 모달도 없앴습니다.
- 후속QR에 참가자 ID만 있어 찍어 둔 사진으로 언제든 체포할 수 있었기에, 만료 시각을 넣어 30초가 지나면 거부되게 했습니다.
- 대안서버 서명은 위조 QR까지 막지만 백엔드 변경이 필요합니다. 함께 뛰는 사람들끼리의 게임에서 위조는 현실적인 위협이 아니라고 보고 넣지 않았습니다.
- 결과체포는 실제로 마주친 상태에서만 성립하고, 만료 경계는 단위 테스트로 고정했습니다.
REST는 조회·명령, STOMP는 실시간 이벤트
- 상황체포·탈옥 같은 요청과 위치 공개·체포 알림 같은 이벤트가 한 게임 안에서 동시에 오갑니다.
- 결정요청은 REST로 보내고, 이벤트는 STOMP로 받도록 나눴습니다.
- 결과연결이 끊겼다 복구되면 REST 상태 스냅샷으로 놓친 상태를 보정합니다. 이 재연결 복구는 팀원이 구현했습니다.
심사 거부로 배운 위치 표시등
- 상황표시등 없이 백그라운드에서 위치를 수집했다가 Apple 심사에서 거부됐습니다.
- 결정iOS 위치 인디케이터 표시를 정책 요건으로 받아들여 넣었습니다. 게임이 끝난 뒤에도 표시등이 남아 위치를 계속 수집하는 것처럼 보이는 문제는 위치 구독 해제 시점을 바로잡아 고쳤습니다.
- 교훈"백그라운드 위치 수집은 사용자에게 보이는 신호와 함께 설계한다"를 팀 규칙으로 기록했습니다. 이후 스토어 심사 대응의 기준이 됐습니다.
지도에 격자를 깔아 "어디"를 한마디로
- 상황"어디로 가는 중"이라고 쓰면 추상적이라 알아듣기 어려웠고, 뛰면서 채팅을 칠 틈도 없었습니다. 위치를 알리는 핑은 2.5초면 사라져 거의 쓰이지 않았습니다.
- 계기한동안 붙잡고 있었지만 좋은 방법이 떠오르지 않았습니다. 그러다 누워서 쉬던 중 천장에 앉아 있는 파리를 보다가, 지도에 좌표를 붙이면 되겠다는 생각이 떠올라 바로 만들어 팀에 보여 줬습니다.
- 결정지도 위에 체스판처럼 격자를 깔고, 칸마다 "D5"처럼 이름을 붙였습니다. 열은 왼쪽부터 알파벳, 행은 위에서부터 숫자라서 "D5"는 D열 5행입니다. 격자를 켠 채 핑을 찍으면 "D5에서 도둑을 봤어요"가 팀 채팅에 자동으로 올라가서, 핑을 놓쳐도 기록이 남습니다.
- 조정칸 크기는 플레이 구역의 크기에 맞춰 자동으로 정해집니다. 격자는 켠 사람에게만 적용해, 원하지 않는 사람의 채팅이 늘지 않게 했습니다.
- 구상나중에 보이스 채팅이나 무전기가 생기면 "D5에 둘, E6으로 이동"처럼 말로 브리핑하기도 쉬워집니다.
서비스를 도입한 이유
- 소셜 로그인 — Firebase Authentication. 일반 회원가입보다 접근성이 높고 가입 이탈이 적다는 사례를 근거로 제안했습니다. Google·Apple 로그인 후 백엔드 JWT를 발급받습니다.
- 딥링크 — 링크 하나로 바로 유입되면 가입·유입이 늘어난다는 사례를 근거로 제안했습니다. Google이 소개한 쇼피 사례에서는 딥링크 도입 후 앱으로 바로 들어오는 쇼핑 트래픽이 약 20%에서 50%로 늘고 결제가 126% 증가했고(Think with Google), 딥링크 캠페인의 전환율이 일반 링크보다 51% 높다는 Moburst 수치(Airbridge)와 유입 35% 증가(AppsFlyer)도 있었습니다. 넣어서 잃을 것은 없고 얻을 것만 있다고 판단했습니다. 앱 수신부는 제가, 웹 랜딩은 웹 팀원이 맡았고, 새 경로를 열 때 앱과 웹을 한 작업 단위로 보는 협업 규칙을 만들었습니다. 자세한 내용은 위 딥링크 항목입니다.
- FCM — 당장 필요한 기능은 아니었지만 APNs 설정 등 준비할 것이 많아 초기에 한 번에 세팅했습니다. 이후 게임 시작·인게임 채팅·커뮤니티 알림에 쓰였고, 채팅 푸시는 30초에 1건으로 묶어 알림 과다를 막았습니다.
- Remote Config — 이벤트 배너·점검·광고 on/off를 앱 재배포 없이 바꾸려고 도입했습니다. 설정이 없거나 실패하면 꺼지게 했습니다.
- Analytics — 로그인부터 이탈까지 어디서 사용자가 빠지는지 데이터가 없어 도입했습니다. 계측 실패가 앱 흐름을 막지 않게 했습니다.
- AdMob — 수익화 수단이 없어 게임 종료 전면광고와 커뮤니티 네이티브 광고를 넣었습니다. 광고가 실패해도 게임은 진행되고(fail-open), Remote Config로 원격에서 끌 수 있습니다.
- Crashlytics — 앱이 갑자기 꺼졌을 때 원인을 모아 보여 주는 도구입니다. 화면을 그리다 난 오류와 잡히지 않은 비동기 오류를 릴리즈 빌드에서만 수집하고, 디버그 빌드에서는 끕니다. Crashlytics 설정이 실패해도 앱은 계속 뜹니다.
결과
- iOS App Store·Google Play에 출시했고, 한국어·영어·일본어를 지원합니다.
- Firebase Analytics 기준 2026년 1월 1일부터 10월 12일까지 새 사용자 840명이 들어왔고, 한국·미국·일본 등 42개 국가에서 앱을 사용했습니다.
- 탈퇴를 제외하고 남아 있는 사용자 수는 딥링크·QR 전환을 마친 뒤 156명(2026.06.18)에서 660명(2026.10.19)으로 약 4배가 됐습니다.
- 잠금 화면 게임 현황(iOS Live Activity·Android 게임 진행 알림)을 v3.1.20으로 배포했습니다.
- 수상: 세종 SW 창업 아이디어 리그 대상, 세종대 창업아이템 경진대회 우수상, 입주공모전 우수 창업 아이템상, 아롬 데모데이 특별상, SSUP 선정
- 오프라인 행사 참여: 서울게임타운, 서울 대학생 연합 페스티벌, 광진구 청춘대로
배운 점
- 사용자에게 설정 변경을 요구하기 전에, 같은 종류의 앱은 어떻게 동작하는지 먼저 확인해야 합니다. 내비게이션·음악 앱은 배터리 설정을 요구하지 않았고, 실제 원인은 CPU가 잠드는 것을 놓친 제 구현이었습니다.
- 보안 대책은 막을 위협의 범위부터 정해야 합니다. 막을 대상을 찍어 둔 QR 사진의 재사용으로 정하니, 서버 서명까지는 필요 없다고 판단할 수 있었습니다.
- 백그라운드 위치 수집은 사용자에게 보이는 신호와 함께 설계해야 합니다. 표시등 없이 올렸다가 심사에서 거부된 뒤, 이를 팀 규칙으로 남겼습니다.







