토스 앱 안에서 주식 차트 예측 게임을 만들어봤다 — 1편: 기획과 시작
목차
금융 앱 안에 게임을 넣는다고?
토스를 쓰다 보면 '만보기', '행운퀴즈' 같은 미니 콘텐츠들이 눈에 들어옵니다. 금융 앱인데 이런 게 왜 있을까 싶다가도, 매일 한 번은 꼭 열어보게 되는 자신을 발견하게 됩니다. 그러다 문득 이런 생각이 들었습니다.
"토스에 주식 차트 예측 게임이 있으면 재밌지 않을까?"
토스 증권을 쓰는 사람이라면 누구나 한 번쯤 "이 차트, 다음에 오를까 내릴까?" 하고 감으로 판단해본 경험이 있을 겁니다. 그 직감을 게임으로 만들면 어떨까. 맞히면 점수를 주고, 틀리면 하트가 줄어들고, 콤보가 쌓이면 보너스 점수를 주는 — 단순하지만 중독성 있는 그런 게임을 구상했습니다.
마침 토스에는 **앱인토스(Apps in Toss)**라는 미니앱 플랫폼이 있었고, 이 플랫폼 위에서 WebView 기반의 앱을 만들어 토스 사용자들에게 직접 서비스할 수 있다는 사실을 알게 되었습니다. 사이드 프로젝트로 이보다 매력적인 놀이터가 또 있을까 싶었습니다.
규칙은 최대한 단순하게
게임의 이름은 **"차트예측게임"**입니다. 규칙은 정말 단순합니다.
게임 플로우
- 실제 한국 주식 데이터에서 랜덤으로 90개의 캔들스틱을 보여줍니다
- 플레이어는 다음 10개 캔들이 **상승(LONG)**할지 **하락(SHORT)**할지 예측합니다
- 맞히면 점수를 얻고, 틀리면 하트를 잃습니다
- 하트가 0이 되면 게임 오버입니다
UX에서 신경 쓴 부분
금융 데이터를 다루는 게임이지만, 진입 장벽은 최대한 낮추고 싶었습니다. 차트를 읽을 줄 모르는 사람도 "올라갈 것 같다 / 내려갈 것 같다" 정도의 직감만으로 즐길 수 있어야 했습니다.
하트 시스템은 2개로 설정했습니다. 너무 많으면 긴장감이 없고, 1개면 첫 실수에 바로 끝나서 이탈률이 높아질 거라 판단했습니다. 2개는 "한 번은 실수해도 괜찮지만, 두 번째는 조심해야 한다"는 적절한 긴장감을 줍니다.
콤보 시스템도 넣었습니다. 연속으로 맞히면 콤보가 쌓이고, 3콤보마다 보너스 점수를 받을 수 있습니다. 이 장치가 있어야 "한 판만 더" 하는 동기가 생기기 때문입니다.
결과를 공개할 때는 나머지 10개 캔들이 순차적으로 드러나면서, 예측이 맞았는지 시각적으로 확인할 수 있게 했습니다. 맞혔을 때는 화면 전체에 짧은 플래시 효과와 점수 팝업이 뜨고, 콤보가 높아지면 confetti 애니메이션이 터지도록 했습니다. 이런 마이크로 인터랙션이 게임의 재미를 만드는 핵심이라고 생각합니다.
무엇으로 만들었나
React + TypeScript + Vite
앱인토스 플랫폼 자체가 WebView 기반이므로 웹 기술 스택이 필수입니다. React를 선택한 이유는 단순합니다 — 가장 익숙하고, 앱인토스 SDK가 React를 1순위로 지원하기 때문입니다.
Vite는 개발 서버 시작 속도와 HMR(Hot Module Replacement) 성능이 압도적이었습니다. 특히 이 프로젝트처럼 빈번하게 UI를 수정하며 실시간으로 확인해야 하는 게임 개발에서는 빌드 도구의 반응 속도가 생산성에 직접적인 영향을 줍니다.
TypeScript는 게임 상태 관리에서 빛을 발했습니다. 게임 페이즈(idle, playing, revealing, gameover)에 따라 UI가 완전히 달라지는 구조에서, 타입 시스템 없이 상태를 관리했다면 꽤 많은 런타임 에러를 겪었을 겁니다.
Zustand — 가볍고 직관적인 상태 관리
게임의 전체 상태를 하나의 스토어로 관리해야 했는데, Redux는 보일러플레이트가 과하고, Context API는 리렌더링 최적화가 까다로웠습니다. Zustand는 단 하나의 파일에 상태와 액션을 모두 정의할 수 있고, 선택적 구독(selective subscription) 패턴을 적용하기도 수월했습니다. 이 부분은 3편에서 자세히 다루겠습니다.
lightweight-charts — 전문가 수준의 캔들스틱 차트
차트 라이브러리 선택에는 꽤 고민이 있었습니다. Chart.js나 Recharts는 범용적이지만, 금융 차트에 특화된 라이브러리가 필요했습니다. lightweight-charts는 TradingView에서 만든 라이브러리로, 캔들스틱 차트를 네이티브에 가까운 성능으로 렌더링합니다. Canvas 기반이라 100개의 캔들을 그려도 프레임 드랍 없이 부드럽게 동작했습니다.
framer-motion — 애니메이션의 품질을 결정하는 도구
게임에서 애니메이션은 선택이 아니라 필수입니다. 점수가 올라갈 때의 팝업, 하트가 사라질 때의 떨림, 결과 배너가 나타날 때의 스프링 효과 — 이런 것들이 없으면 게임이 밋밋해집니다. framer-motion은 선언적인 API로 복잡한 애니메이션을 간결하게 표현할 수 있었고, 특히 spring 물리 엔진이 자연스러운 느낌을 주는 데 큰 역할을 했습니다.
앱인토스 SDK — 토스 네이티브 기능 연동
@apps-in-toss/web-framework와 @toss/tds-mobile은 앱인토스 개발에 있어 핵심 의존성입니다. 전자는 WebView와 네이티브 앱 사이의 브릿지 역할을 하고, 후자는 토스의 디자인 시스템을 그대로 사용할 수 있게 해줍니다. 버튼, 바텀시트, 토스트 같은 UI 컴포넌트를 토스 앱의 네이티브 느낌 그대로 쓸 수 있다는 점이 가장 큰 장점이었습니다.
앱인토스라는 놀이터
앱인토스는 쉽게 말해 **"토스 안의 앱스토어"**입니다. 개발자가 웹 기술로 미니앱을 만들고, 토스 앱 내에서 유저에게 직접 서비스할 수 있는 플랫폼입니다.
기술적으로는 WebView 위에서 동작하되, 토스 네이티브 기능(결제, 로그인, 광고, 푸시 알림 등)에 접근할 수 있는 브릿지 API를 제공합니다. 즉, 웹 개발자가 네이티브 앱에 가까운 경험을 만들 수 있는 환경입니다.
개발 프로세스는 이렇습니다:
- Granite CLI로 프로젝트를 생성하고 로컬에서 개발합니다
granite.config.ts에 앱 메타데이터(이름, 아이콘, 네비게이션 설정 등)를 정의합니다- 개발이 끝나면 빌드 후 앱인토스에 제출합니다
- 심사를 거쳐 승인되면 토스 앱에 노출됩니다
이 과정에서 겪은 심사 반려 이야기는 5편에서 자세히 다루겠습니다.
granite.config.ts에 무엇을 적는가
설정 파일 하나가 앱의 껍데기를 전부 결정합니다. 실제로 쓴 내용은 이렇습니다.
export default defineConfig({
appName: 'stock-chart-game',
brand: {
displayName: '차트예측게임',
primaryColor: '#3182F6',
icon: 'https://chart.apporbit.xyz/icon.svg',
},
webViewProps: {
type: 'game',
},
navigationBar: {
withBackButton: true,
initialAccessoryButton: {
id: 'game-info',
title: '게임 방법',
icon: { name: 'icon-info-mono' },
},
},
permissions: [],
outdir: 'dist',
});여기서 몇 가지를 짚고 싶습니다.
webViewProps.type: 'game'. 이 한 줄이 WebView의 동작 방식을 바꿉니다. 일반 콘텐츠형 미니앱은 스크롤과 당겨서 새로고침 같은 웹 기본 동작을 살려두는데, 게임 타입으로 선언하면 그런 제스처가 게임 조작을 방해하지 않도록 처리됩니다. 차트를 드래그해서 보는 게임에서 실수로 페이지가 새로고침되면 판이 날아가니까요.
primaryColor: '#3182F6'. 토스의 파란색입니다. 내 앱의 브랜드 컬러를 넣고 싶은 유혹이 있었지만 넣지 않았습니다. 미니앱은 토스 앱 안에서 열리기 때문에, 색이 튀면 "잘 만든 앱"이 아니라 "여기 왜 이런 게 있지"가 됩니다. 껍데기는 토스의 것이고 나는 안쪽만 채운다는 감각이 필요했습니다.
initialAccessoryButton. 네비게이션 바 오른쪽에 '게임 방법' 버튼을 달았습니다. 게임 화면 안에 도움말 버튼을 따로 그리는 대신 플랫폼이 제공하는 자리를 쓴 건데, 결과적으로 화면이 훨씬 깨끗해졌습니다. 아이콘도 icon-info-mono처럼 토스가 정해 둔 이름으로 지정하니 다른 미니앱과 시각적으로 일관됩니다.
permissions: []. 아무 권한도 요청하지 않습니다. 위치도, 연락처도, 알림도 받지 않습니다. 처음엔 푸시 알림으로 "오늘의 차트가 도착했어요" 같은 걸 보내볼까 했는데 접었습니다. 금융 앱 안에서 열리는 게임이 권한을 요구하면 그 자체로 경계심이 생깁니다. 권한 목록이 비어 있다는 게 이 앱에 대한 가장 짧은 설명이라고 생각했습니다.
서버 없이 만들기로 한 결정
이 게임에는 백엔드가 없습니다. 주식 데이터 JSON을 번들에 통째로 넣고 게임 로직도 전부 클라이언트에서 돕니다.
처음부터 그럴 생각은 아니었습니다. 랭킹 보드를 만들려면 서버가 필요하고, 그러면 로그인이 필요하고, 로그인을 하려면 토스 계정 연동을 받아야 합니다. 그 시점에 계산이 섰습니다. 랭킹 하나 때문에 개인정보를 받고 서버를 운영하고 심사 항목을 늘리는 게 맞나.
랭킹을 포기하니 남는 게 많았습니다. 서버 비용이 0원이고, 네트워크 지연이 없어 다음 판이 즉시 시작되고, 심사에서 설명할 데이터 처리 항목이 사라졌습니다. 사이드 프로젝트에서 기능 하나를 빼는 결정이 이렇게 여러 곳에 이득을 주는 경우는 흔치 않습니다.
남의 앱 안에서 만든다는 것
앱인토스로 개발하면서 가장 크게 달라진 감각은 이 앱이 내 것이 아니라는 점입니다.
일반 웹 서비스는 도메인부터 헤더, 색, 폰트, 뒤로 가기 동작까지 전부 내가 정합니다. 미니앱은 아닙니다. 상단 네비게이션 바는 토스의 것이고, 뒤로 가기는 토스의 규칙을 따르고, 앱을 벗어나는 순간 사용자는 토스로 돌아갑니다. 내가 통제하는 건 그 안쪽 사각형 하나입니다.
이 제약이 처음엔 답답했지만 곧 편해졌습니다. 정할 게 줄어드니까요. 헤더를 어떻게 디자인할지, 로딩 스피너를 뭘 쓸지, 바텀시트를 직접 만들지 라이브러리를 쓸지 같은 결정이 통째로 사라집니다. 토스 디자인 시스템 컴포넌트를 가져다 쓰면 그게 곧 정답입니다. 사이드 프로젝트에서 가장 비싼 자원이 시간이라는 걸 생각하면 나쁘지 않은 거래였습니다.
대신 다른 종류의 책임이 생깁니다. 여기서 실수하면 내 앱이 아니라 토스가 이상해 보입니다. 금융 앱 안에서 열리는 게임이 로딩 중에 흰 화면을 3초 띄우면, 사용자는 "이 게임 별로네"가 아니라 "토스가 느리네"라고 느낍니다. 플랫폼 위에 얹히는 앱은 그 플랫폼의 신뢰를 빌려 쓰는 만큼 그걸 깎아먹지 않아야 합니다.
그래서 데이터를 전부 번들에 넣어 첫 화면이 네트워크를 기다리지 않게 만든 것도, 권한을 하나도 요청하지 않은 것도 같은 이유에서 나온 결정입니다. 내 앱의 완성도가 아니라 내가 빌려 쓰는 신뢰를 지키는 쪽으로 판단 기준이 옮겨간 셈입니다.
이건 웹 서비스만 만들어 봤을 때는 해볼 일이 없는 경험이었습니다.
한 가지 덧붙이면, 이 프로젝트를 시작할 때 가장 도움이 됐던 건 기술 문서가 아니라 이미 올라와 있는 미니앱들을 직접 써 본 것이었습니다. 어디까지가 플랫폼의 영역이고 어디부터가 개발자의 영역인지, 로딩은 보통 어느 정도 걸리는지, 어떤 화면이 자연스럽게 느껴지는지는 문서에 안 적혀 있습니다. 만보기와 행운퀴즈를 며칠 쓰면서 감을 잡은 게 설계의 절반이었습니다.
그리고 이런 감각은 문서를 읽어서 얻는 것보다 훨씬 오래갑니다. 지금도 새 플랫폼에 뭔가 올릴 일이 생기면 API 레퍼런스보다 그 위에 이미 올라와 있는 것들을 먼저 봅니다.
정리
이번 1편에서는 프로젝트의 기획 배경, 핵심 게임 메커니즘, 그리고 기술 스택을 왜 그렇게 선택했는지를 정리했습니다.
사실 사이드 프로젝트에서 가장 어려운 건 기술적인 문제가 아니라, **"이걸 정말 만들어야 하나?"**라는 확신을 갖는 것이라고 생각합니다. 이 프로젝트의 경우, 토스라는 대규모 플랫폼 위에서 바로 유저를 만날 수 있다는 점이 그 확신을 만들어줬습니다.
다음 편에서는 게임의 핵심인 주식 데이터 처리와 게임 로직 설계에 대해 이야기하겠습니다. 실제 한국 주식 데이터를 어떻게 가공해서 게임에 활용했는지, 승패 판정 로직은 어떤 고민 끝에 나왔는지를 다룹니다.