내가 만난 괴짜들 — 매일 자정, 새로운 괴짜가 나타난다
새까만 실루엣과 울음소리, 단서는 이 둘뿐. 운영자가 붙여 놓은 엉뚱한 이름을 맞히는 하루 한 판짜리 퀴즈 게임입니다. 토스 미니앱으로 먼저 내놓고, 같은 백엔드를 그대로 쓰는 안드로이드 앱으로 이식했습니다.
게임 소개
매일 00시에 '오늘의 괴짜' 한 마리가 새까만 실루엣으로 등장합니다. 탭하면 괴짜가 자기 목소리로 울고, 그 소리와 실루엣만 단서 삼아 이름을 맞혀야 합니다. 모든 사용자에게 같은 괴짜가 나가므로, 그날 몇 명이나 정체를 밝혔는지도 함께 보입니다.
핵심은 맞혀야 하는 것이 동물의 종이 아니라는 점입니다. 정답은 운영자가 붙여 둔 엉뚱한 별명이라, 화면에 날아다니는 돼지가 나와도 정답은 '돼지'가 아니라 날으라 돈까스가 됩니다. 그래서 실루엣을 알아본 순간이 끝이 아니라 시작입니다.
촌스럽고 웃긴 B급 톤으로 화면을 통일했습니다. 형광 노랑과 마젠타, 두꺼운 검은 테두리, 삐뚤어진 딱지 — 게임의 농담을 UI가 먼저 하도록 만든 디자인입니다.
규칙 — 틀릴수록 보이고, 보일수록 덜 받는다
오답을 낼 때마다 실루엣이 한 겹씩 벗겨집니다. 윤곽 → 색상 → 얼굴·특징 → 대부분 → 다섯 번이면 전부 드러납니다. 대신 정답 보상인 재화 '밥알'은 한 번도 안 틀리면 10밥알, 그 뒤로 8 · 6 · 4 · 2를 거쳐 20회부터는 1밥알까지 떨어집니다. 빨리 맞힐 것인가, 확실하게 갈 것인가를 매번 저울질하게 만드는 장치입니다.
틀린 답도 버려지지 않습니다. 오답이 정답과 겹치는 글자를 품고 있으면 그 글자가 '수사 기록'에 남습니다. 아무렇게나 찍은 답이 결정적인 단서가 되는 순간이 이 게임에서 제일 재미있는 대목입니다.
힌트는 두 갈래입니다. 맨 앞 글자 · 맨 뒤 글자 · 글자 수 세 종류 중 골라 1밥알에 사는 기본 힌트, 그리고 광고를 보면 몸통 글자를 한 자씩 열어 주는 추가 힌트입니다. 광고를 띄울 수 없는 환경에서는 5밥알로 대신 살 수 있습니다. 광고를 한 번 더 보면 하루 한 마리를 더 만나는 보너스 라운드가 열립니다.
모으는 재미 — 도감 · 미션 · 칭호
지나간 괴짜 100건이 도감에 쌓입니다. 맞힌 괴짜는 정체와 그림이 공개되고, 못 맞힌 괴짜는 실루엣과 물음표로 남습니다. 괴짜마다 그날의 정답률과 평균 시도 횟수 같은 통계도 함께 봅니다.
일일 미션은 매일 초기화되는 5종입니다 — 목소리 듣기, 오답에서 새 글자 발견하기, 힌트 없이 맞히기 같은 것들이고 달성하면 밥알이 자동으로 들어옵니다. 칭호는 31종이며 누른 칭호를 대표로 장착할 수 있습니다.
안드로이드 이식
원본은 토스 앱 안에서 도는 Vue 3 미니앱입니다. 이것을 Flutter로 옮겨 안드로이드 앱을 만들었는데,
이식 자체는 백엔드를 한 줄도 고치지 않고 끝났습니다. 두 플랫폼이 같은 백엔드의 같은
테넌트(peek-a-moo)를 쓰기 때문에 오늘의 괴짜와 정답자 수, 통계가 하나로 집계됩니다.
백엔드에 손을 댄 것은 그 뒤의 계정 싱크가 처음으로, 테이블 두 개와 연동용 라우트만 더했고 기존 응답
계약은 그대로 두었습니다.
게임 로직과 문구는 룰셋 문서 하나를 단일 원본으로 두고, Vue 쪽 파일과 Flutter 쪽 파일이 1:1로 대응하도록 구조를 맞췄습니다. 판정 규칙을 고칠 일이 생기면 양쪽을 함께 고치는 것을 규칙으로 두었고, 문구·판정은 회귀 테스트로 묶어 두어 한쪽만 어긋나면 테스트가 먼저 깨집니다.
플랫폼 사정상 일부러 다르게 둔 것도 있습니다.
| 항목 | 토스 미니앱 | 안드로이드 |
|---|---|---|
| 사용자 신원 | 토스 익명 키 — 계정에 묶여 기기를 바꿔도 같음 | 기기 UUID — 설치에 묶임 |
| 광고 | 앱인토스 지면 그룹 | AdMob 광고 단위 |
| 공유 | 토스 딥링크 + SDK 공유 | OS 공유 시트 |
| 뒤로가기 | SDK 백 이벤트 등록 | OS 기본 동작 |
| 글꼴 | 시스템 한글 고딕 | Pretendard · Black Han Sans 번들 |
계정 싱크 — 합치지 않고 갈아탄다
같은 사람이 토스와 안드로이드를 오가면 서버에는 서로 다른 사용자로 잡힙니다. 밥알과 칭호, 도감이 따로 쌓이죠. 이걸 하나로 모으려고 일회용 연동 코드를 붙였습니다.
다만 두 계정을 합치지 않고 갈아탑니다. 지키고 싶은 쪽에서 코드를 발급하고 버려도 되는 기기에서 입력하면, 입력한 기기가 발급한 계정으로 넘어가고 그 기기의 기존 진행은 사라집니다. 합산 방식을 택하지 않은 이유는 어뷰징입니다 — 밥알을 합쳐 주면 앱을 재설치할 때마다 신규 가입 보상을 받아 본계정에 무한히 쌓을 수 있습니다. 갈아타기는 옮겨지는 값이 아예 없어서 그 경로를 구조적으로 막습니다. 대신 입력 전에 "이 기기의 기록이 사라진다"를 반드시 경고하고, 원래 계정은 서버에서 지우지 않아 해제하면 되돌아옵니다.
기술적 접근
앱은 Flutter와 Riverpod으로 만들고 GoRouter로 게임 · 결과 · 도감 · 미션 네 화면을 묶었습니다.
빌드 설정은 --dart-define 스위치 한 곳으로 모아, 백엔드 주소와 AdMob 광고 단위, 광고
자체를 끄는 스위치까지 빌드 시점에 갈아 끼웁니다. 디버그 빌드는 구글 테스트 광고 단위를 쓰고
릴리스만 실제 단위를 씁니다.
스토어에 올릴 아이콘과 그래픽 이미지도 손으로 만들지 않고 Flutter 렌더러로 직접 그립니다. 이 머신에는 이미지 편집 도구가 없기도 하지만, 앱 안에서 쓰는 디자인 토큰을 그대로 써야 스토어 이미지와 실제 화면이 따로 놀지 않기 때문입니다. Play가 알파 채널을 거부하는 자리는 다시 구워 24비트로 맞춥니다.
릴리스 전에는 릴리스 APK를 실제로 띄워 보는 절차를 빼지 않습니다. 디버그에서는 멀쩡한데 R8 최적화가 들어간 릴리스 빌드에서만 죽는 사고를 막기 위한 것으로, 이 확인이 통과해야 앱 번들을 만듭니다.
주요 기능
화면 갤러리
개발 빌드에서 캡처한 화면이라 힌트 화면의 광고 지면에는 구글 테스트 광고가 들어가 있습니다.
약관 및 정책
회원가입은 없지만 서버에 진행 데이터가 쌓이고 광고가 들어가는 앱이라, 무엇을 받고 무엇을 받지 않는지 문서로 두었습니다.