AppOrbit
← blog

실제 주식 데이터로 차트 예측 게임 만들기 — 2편: 게임 로직 설계

React, TypeScript, 게임로직, 주식데이터, 선형회귀, 알고리즘

1. 들어가며: 진짜 주식 데이터를 쓰기로 했다

차트 예측 게임을 만든다고 했을 때, 가장 먼저 결정해야 할 문제는 **"어떤 데이터를 보여줄 것인가"**였습니다.

크게 두 가지 선택지가 있었습니다. 하나는 랜덤으로 생성한 가짜 차트, 다른 하나는 실제 주식 데이터입니다. 랜덤 차트가 구현은 훨씬 간단하지만, 금방 패턴이 반복된다는 느낌을 줄 수 있습니다. 무엇보다, "이 차트 어디서 본 것 같은데?" 하는 현실감이 게임의 몰입도를 높여줄 거라 판단했습니다.

결국 삼성전자, SK하이닉스, LG에너지솔루션, 삼성바이오로직스, 현대자동차 — 한국 대표 블루칩 5종목의 실제 거래 데이터를 게임에 넣기로 했습니다. 각 종목별로 약 250거래일 분량의 OHLCV(시가·고가·저가·종가·거래량) 데이터를 JSON 파일로 준비했습니다.

2. 데이터 파이프라인: 원본에서 게임 시나리오까지

원본 데이터 구조

준비한 JSON 데이터는 이런 형태입니다:

원본 데이터 형태:
  - 날짜 (Date)
  - 시가 (Open)
  - 고가 (High)
  - 저가 (Low)
  - 종가 (Close)
  - 거래량 (Volume)

이 원본 데이터를 게임에서 바로 쓸 수는 없습니다. 차트 라이브러리가 요구하는 형식으로 변환하고, 게임 한 판에 필요한 100개 캔들을 추출해야 합니다.

시나리오 생성 프로세스

게임 시나리오를 만드는 과정은 크게 4단계로 나뉩니다:

[시나리오 생성 흐름]

1. 종목 선택
   → 5개 종목 중 하나를 랜덤으로 선택

2. 구간 추출
   → 250개 데이터 중 연속된 100개를 랜덤 시작점에서 추출
   → 시작점 범위: 0 ~ (전체 길이 - 100)

3. 데이터 변환
   → 날짜 문자열을 Unix 타임스탬프로 변환
   → 가격 데이터를 숫자형으로 파싱
   → 차트 라이브러리가 이해하는 캔들 포맷으로 매핑

4. 정답 판정
   → 90번째 캔들의 종가 vs 100번째 캔들의 종가 비교
   → 올랐으면 'up', 내렸으면 'down'으로 정답 설정

여기서 가장 중요한 건 4번 정답 판정 로직입니다.

정답은 어떻게 결정하는가

플레이어에게는 처음 90개의 캔들만 보여주고, 나머지 10개를 맞히라고 합니다. 그런데 "오른다" 또는 "내린다"의 기준이 뭘까요?

처음에는 단순히 "91번째 캔들이 90번째보다 높으면 상승"으로 판정하려고 했습니다. 하지만 이렇게 하면 문제가 있습니다. 하루치 등락은 노이즈가 너무 크기 때문입니다. 90번째 캔들에서 소폭 빠졌다가 이후 크게 반등하는 케이스에서, 플레이어는 "올랐다"고 느끼지만 시스템은 "틀렸다"고 판정하는 상황이 생깁니다.

그래서 10개 캔들 전체의 추세를 기준으로 잡았습니다. 90번째 캔들의 종가와 100번째 캔들의 종가를 비교해서, 10거래일의 전체적인 방향성을 정답으로 삼았습니다. 이렇게 하면 플레이어가 체감하는 "올랐다/내렸다"와 시스템의 판정이 훨씬 자연스럽게 일치합니다.

3. 점수 시스템 설계

기본 점수 + 콤보 보너스

점수 체계는 심플하면서도 중독성을 줘야 했습니다. 최종 설계는 이렇습니다:

점수 계산:
  기본 점수 = 100
  콤보 보너스 = (콤보 횟수 / 3)의 내림값 × 50
  최종 점수 = 기본 점수 + 콤보 보너스

예시:
  1연속 정답: 100 + 0 = 100점
  2연속 정답: 100 + 0 = 100점
  3연속 정답: 100 + 50 = 150점
  6연속 정답: 100 + 100 = 200점
  9연속 정답: 100 + 150 = 250점

3콤보마다 보너스가 붙는 구조로 한 이유가 있습니다. 매 라운드마다 보너스가 1씩 증가하면, 점수 인플레이션이 너무 빠르게 일어납니다. 3라운드에 한 번씩 보상이 주어지면 "다음 보너스까지 버텨야지" 하는 소소한 목표 의식이 생기고, 3연속 정답의 성취감이 확실히 커집니다.

왜 2개의 하트인가

하트 수를 결정하는 건 생각보다 고민이 많았습니다.

  • 하트 1개: 긴장감은 최고지만, 첫 실수에서 바로 게임 오버. 이탈률이 높아질 위험
  • 하트 3개: 여유로운 대신 긴장감이 떨어짐. "한두 번 틀려도 돼"라는 안일함
  • 하트 2개: 한 번의 실수는 허용하되, 두 번째 실수부터는 조심해야 하는 적절한 긴장감

실제로 테스트해보니 하트 2개일 때 "아, 한 번 틀렸으니까 이번엔 신중하게 봐야지" 하는 심리가 자연스럽게 작동했습니다. 게임 디자인에서 말하는 **"허용 가능한 실패(tolerable failure)"**를 정확히 1번으로 설정한 셈입니다.

4. 결과 공개: 선형 회귀 트렌드라인

예측 결과를 공개할 때, 단순히 나머지 10개 캔들만 보여주면 밋밋합니다. 그래서 선형 회귀(Linear Regression) 트렌드라인을 추가했습니다.

트렌드라인의 역할

결과 화면에서 10개 캔들 위에 노란색 직선이 하나 그려집니다. 이 직선은 "이 10개 데이터를 가장 잘 대표하는 추세선"입니다. 선이 우상향이면 상승 추세, 우하향이면 하락 추세를 한눈에 파악할 수 있습니다.

플레이어 입장에서는 "아, 내가 예측한 방향이 맞았구나" 또는 "확실히 반대로 갔네" 하는 시각적 피드백을 받게 됩니다. 이 한 줄의 트렌드라인이 결과에 대한 납득감을 크게 높여줍니다.

구현에서의 챌린지: 부동소수점 정밀도

선형 회귀 자체는 고등학교 수학 수준의 알고리즘입니다. 하지만 여기서 까다로운 문제가 하나 있었습니다 — 부동소수점 정밀도 문제입니다.

주식 가격은 보통 수만~수십만 원 단위입니다. 이 값들을 그대로 곱하고 합산하면 JavaScript의 부동소수점 연산에서 오차가 누적됩니다. 특히 sumXY(x와 y의 곱의 합)를 계산할 때, 숫자가 매우 커져서 정밀도가 떨어지는 현상이 발생했습니다.

문제 상황:
  가격 데이터: [52300, 52500, 52100, ...]
  sumXY 계산 시: 0 × 52300 + 1 × 52500 + 2 × 52100 + ...
  → 중간 계산 값이 매우 커져서 부동소수점 오차 발생
  → 기울기(slope)가 실제와 미세하게 달라짐
  → 트렌드라인이 데이터와 어긋나 보이는 현상

해결 방법은 스케일링이었습니다:

해결 방법:
  1. 모든 가격을 SCALE(1억)로 나눠서 0~1 범위의 작은 수로 변환
  2. 작은 수로 선형 회귀 계산 수행
  3. 결과 기울기와 절편을 다시 SCALE로 복원

  이렇게 하면 중간 계산값이 작아져서
  부동소수점 오차가 실질적으로 무시할 수준이 됨

금융 데이터를 다루는 프론트엔드에서는 꽤 흔하게 마주치는 문제인데, 처음 겪으면 "왜 계산이 안 맞지?" 하며 한참을 헤맬 수 있는 함정입니다.

5. 데이터 설계에서 배운 것들

클라이언트 사이드 완결 구조의 장점

이 게임은 백엔드 서버가 없습니다. 모든 데이터가 빌드 타임에 번들에 포함되고, 게임 로직도 전부 클라이언트에서 실행됩니다.

이 구조의 장점은 명확합니다:

  • 응답 속도: 네트워크 지연 없이 즉시 다음 시나리오를 생성할 수 있습니다
  • 서버 비용 제로: 사이드 프로젝트에서 이건 정말 큰 장점입니다
  • 오프라인 가능: 한 번 로딩되면 네트워크 없이도 플레이 가능합니다

물론 단점도 있습니다. 주식 데이터가 정적이기 때문에 무한히 플레이하면 같은 차트가 반복될 수 있습니다. 하지만 5종목 × 약 150개의 가능한 시작점을 조합하면 750가지 이상의 시나리오가 나오고, 캐주얼 게임으로서는 충분한 양입니다.

시나리오 ID로 중복 방지

각 시나리오에는 종목코드_시작인덱스_타임스탬프 형식의 고유 ID를 부여했습니다. 현재는 중복 체크를 엄격하게 하지 않지만, 향후 "이전에 나온 시나리오 제외" 같은 기능을 추가할 때 이 ID가 유용할 것입니다.

6. 마치며

이번 편에서는 게임의 핵심 로직을 설계하는 과정을 다뤘습니다. 요약하면:

  • 실제 한국 블루칩 5종목의 데이터를 사용해 현실감을 높임
  • 10개 캔들의 전체 추세를 기준으로 정답을 판정해 체감과 시스템의 괴리를 줄임
  • 3콤보마다 보너스가 붙는 점수 체계로 단기 목표 의식을 설계
  • 선형 회귀 트렌드라인을 추가해 결과의 시각적 납득감을 높임
  • 부동소수점 정밀도 문제를 스케일링으로 해결

게임 로직은 코드 양 자체는 많지 않지만, "왜 이렇게 설계했는가"에 대한 고민이 결과물의 품질을 크게 좌우했습니다.

다음 편에서는 이 로직들을 어떤 아키텍처로 상태 관리했는지 이야기하겠습니다. Zustand의 선택적 구독 패턴을 활용해 게임의 4가지 페이즈를 깔끔하게 관리한 방법을 다룹니다.

👉 차트예측게임 바로가기