AppOrbit
← blog

모바일 WebView에서 부드러운 캔들스틱 차트 구현하기 — 4편: 차트 렌더링과 애니메이션

lightweight-charts, framer-motion, 캔들스틱차트, 모바일최적화, 애니메이션, Canvas

1. 들어가며: 차트가 게임의 얼굴이다

차트예측게임에서 차트는 단순한 데코레이션이 아닙니다. 플레이어가 판단의 근거로 삼는 핵심 정보이자, 게임 화면의 절반 이상을 차지하는 메인 비주얼입니다. 차트가 뭉개지거나 버벅거리면 게임 전체의 신뢰도가 떨어집니다.

특히 이 게임은 토스 앱의 WebView 위에서 동작합니다. 네이티브 앱이 아니라 웹이라는 제약 속에서, 100개의 캔들스틱을 부드럽게 그리고, 결과 공개 시 애니메이션까지 매끄럽게 처리해야 했습니다. 이번 편에서는 그 여정을 이야기하겠습니다.

2. 차트 라이브러리 선택: lightweight-charts

왜 범용 차트 라이브러리를 쓰지 않았나

Chart.js, Recharts, Nivo 등 React 생태계에는 좋은 차트 라이브러리가 많습니다. 하지만 이들은 대부분 범용 차트에 초점이 맞춰져 있습니다. 캔들스틱 차트를 지원하더라도, 금융 차트에 특화된 기능(거래량 히스토그램, 크로스헤어, 가격 스케일 등)은 부족하거나 커스터마이징이 까다롭습니다.

lightweight-charts는 TradingView가 만든 오픈소스 차트 라이브러리입니다. 이름처럼 가볍고(약 45KB gzipped), Canvas 기반으로 렌더링하기 때문에 DOM 조작 오버헤드 없이 수백 개의 캔들도 부드럽게 그릴 수 있습니다.

선택 이유를 정리하면:

  • 캔들스틱 차트 네이티브 지원: 별도 플러그인 없이 OHLC 데이터를 바로 시각화
  • 거래량 히스토그램 통합: 차트 하단 20%에 거래량을 오버레이로 표시 가능
  • Canvas 기반 고성능: SVG 대비 대량 데이터 렌더링에 유리
  • 반응형 리사이즈: 컨테이너 크기 변경 시 자동으로 재조정
  • 미니멀 API: 설정할 것이 적어 빠르게 통합 가능

차트 컴포넌트 설계

차트 컴포넌트는 React와 lightweight-charts 사이의 브릿지 역할을 합니다. 핵심 동작을 정리하면:

[차트 컴포넌트 생명주기]

마운트 시:
  1. 차트 인스턴스 생성 (Canvas 엘리먼트에 바인딩)
  2. 캔들스틱 시리즈 추가
  3. 거래량 히스토그램 시리즈 추가
  4. 초기 스타일 설정 (색상, 그리드, 패딩 등)

데이터 변경 시:
  1. 캔들스틱 데이터 업데이트 (visibleCandles 만큼만)
  2. 거래량 데이터 동기화
  3. fitContent() 호출 → 자동으로 뷰포트 조정
  4. (결과 공개 시) 트렌드라인 시리즈 추가

언마운트 시:
  1. 차트 인스턴스 제거
  2. Canvas 메모리 해제

90개 → 100개 캔들 전환

게임에서 가장 인상적인 시각적 순간은 결과 공개 시점입니다. 플레이어가 예측 버튼을 누르면, 90개였던 캔들이 100개로 늘어나면서 나머지 10개가 드러납니다.

이 전환을 자연스럽게 만들기 위해, 데이터를 한 번에 교체하는 대신 visibleCandles 상태 값을 90에서 100으로 바꾸는 방식을 택했습니다. 차트 컴포넌트는 이 값의 변화를 감지해서 데이터를 슬라이싱하고, fitContent()로 뷰포트를 재조정합니다.

[결과 공개 흐름]

PLAYING 상태:
  visibleCandles = 90
  → 차트 데이터: scenario.data.slice(0, 90)
  → 거래량 데이터: 90개

makePrediction() 호출:
  visibleCandles → 100
  phase → 'revealing'

REVEALING 상태:
  visibleCandles = 100
  → 차트 데이터: scenario.data.slice(0, 100)
  → 거래량 데이터: 100개
  → 트렌드라인: 마지막 10개 캔들에 선형 회귀선 표시

트렌드라인 오버레이

결과 화면에서 노란색 트렌드라인은 별도의 라인 시리즈로 추가됩니다. lightweight-charts는 하나의 차트에 여러 시리즈를 겹칠 수 있어서, 캔들스틱 위에 라인 시리즈를 오버레이하는 것이 간단했습니다.

트렌드라인은 마지막 10개 캔들의 시간축에 매핑되며, 2편에서 설명한 선형 회귀 결과(기울기와 절편)를 기반으로 시작점과 끝점 두 개의 좌표를 계산해서 그립니다.

3. 게임 애니메이션 설계

framer-motion을 선택한 이유

게임에서 애니메이션은 피드백입니다. 플레이어의 행동에 시각적으로 반응해야 "게임을 하고 있다"는 느낌이 듭니다. CSS 트랜지션으로도 기본적인 효과는 가능하지만, 스프링 물리학 기반의 자연스러운 모션, 조건부 애니메이션, mount/unmount 전환 등은 framer-motion이 압도적으로 편합니다.

애니메이션 카탈로그

게임에서 사용한 주요 애니메이션을 정리하면:

하트 애니메이션

하트를 잃을 때:
  → 해당 하트 아이콘이 좌우로 흔들리며(shake) 사라짐
  → 남은 하트는 스케일이 살짝 커졌다 줄어듦(pulse)
  → 스프링: stiffness 300, damping 10

왜 이렇게 했는가:
  → 단순히 하트 개수만 줄이면 변화를 알아채기 어려움
  → 흔들림은 "위험!" 시그널을 직관적으로 전달
  → 마지막 하트의 pulse는 "이제 한 개밖에 없다"는 긴장감 강화

점수 팝업

정답을 맞혔을 때:
  → "+100" 텍스트가 아래에서 위로 올라오며 나타남
  → 0.8초 후 위로 올라가며 투명해짐
  → 콤보 보너스가 있으면 "+150 🔥" 형태로 표시

초기값: opacity 0, y +20
목표값: opacity 1, y 0
퇴장값: opacity 0, y -30

결과 배너

결과 공개 시:
  → "정답!" 또는 "오답!" 배너가 스케일 0에서 1로 팝업
  → 스프링: stiffness 260, damping 20 (약간의 바운스 효과)
  → 정답이면 초록색, 오답이면 빨간색 배경

왜 스프링을 쓰는가:
  → ease-out 같은 곡선보다 바운스가 있는 스프링이
    "짠!" 하는 느낌을 더 잘 전달

화면 플래시

정답을 맞혔을 때:
  → 화면 전체에 반투명 흰색 오버레이가 0.15초간 나타났다 사라짐
  → 일종의 "카메라 플래시" 효과
  → 시각적 보상감을 극대화

타이밍이 중요:
  → 너무 길면 (0.5초 이상) 거슬림
  → 너무 짧으면 (0.05초 이하) 인지 못함
  → 0.15초가 "인지는 되지만 방해는 안 되는" 적정 시간

confetti 효과

높은 콤보 달성 시:
  → canvas-confetti 라이브러리로 화면에 색종이 효과
  → 파티클 수: 50~100개
  → 방향: 화면 중앙에서 방사형으로 퍼짐
  → 색상: 게임 테마에 맞는 초록/파랑/금색

사용 시점:
  → 3콤보 이상 달성 시 트리거
  → 게임 오버 시 최종 결과에서 트리거 (성과에 따라)

4. 모바일 최적화: 저사양 기기와의 전쟁

문제의 발견

PC 브라우저에서는 모든 애니메이션이 부드럽게 동작했습니다. 하지만 실제 토스 앱의 WebView에서 테스트하자 문제가 드러났습니다.

특히 중저가 Android 기기에서:

  • 결과 공개 시 여러 애니메이션이 동시에 실행되면서 프레임 드랍 발생
  • 캔들스틱 100개 + 트렌드라인 + 결과 배너 + 점수 팝업이 한꺼번에 렌더링
  • 체감 프레임이 30fps 이하로 떨어지는 구간 존재

최적화 전략 1: 애니메이션 스태거링

모든 애니메이션을 동시에 실행하지 않고, 시간 차를 두고 순차적으로 실행하도록 변경했습니다.

[최적화 전 - 동시 실행]
0ms: 차트 데이터 업데이트 + 트렌드라인 + 결과 배너 + 점수 팝업 + 플래시
→ 한 프레임에 모든 것이 변경 → GPU/CPU 스파이크

[최적화 후 - 스태거링]
0ms:   차트 데이터 업데이트 (100개 캔들)
100ms: 트렌드라인 표시
200ms: 결과 배너 팝업
350ms: 점수 팝업
400ms: 플래시 효과 (정답인 경우)

→ 각 변경이 분산되어 프레임 유지

이렇게 시간 차를 두면 각 애니메이션의 초기 프레임 비용이 분산됩니다. 사용자 입장에서는 오히려 "결과가 순차적으로 공개되는" 연출처럼 느껴져서, 단순히 성능 최적화가 아니라 UX 개선이기도 했습니다.

최적화 전략 2: 스프링 파라미터 조정

framer-motion의 스프링 애니메이션은 stiffnessdamping 값에 따라 계산량이 달라집니다. stiffness가 높을수록 빠르게 목표값에 도달하지만, 그 과정에서 더 많은 프레임을 계산해야 합니다.

[스프링 파라미터 튜닝]

최초 설정 (PC에서 최적화):
  stiffness: 500, damping: 15
  → 높은 반동, 많은 프레임 계산
  → PC에서는 부드럽지만 모바일에서 버벅임

모바일 최적화 후:
  stiffness: 260, damping: 20
  → 적당한 반동, 빠르게 안정
  → 모바일에서도 60fps에 가까운 성능

damping을 올리면 반동(바운스)이 줄어들어 "톡" 하고 멈추는 느낌이 됩니다. 완전히 제거하면 밋밋해지지만, 20 정도면 미세한 바운스가 남아서 자연스러우면서도 빠르게 수렴합니다.

최적화 전략 3: Canvas 차트 리사이즈 제어

lightweight-charts는 컨테이너 리사이즈 시 자동으로 Canvas를 다시 그립니다. 하지만 게임의 페이즈 전환 시, 레이아웃이 바뀌면서 불필요한 리사이즈가 여러 번 트리거되는 문제가 있었습니다.

[리사이즈 이슈]

페이즈 전환 시:
  1. 기존 섹션 언마운트 → 컨테이너 높이 변화
  2. 새 섹션 마운트 → 컨테이너 높이 다시 변화
  3. 각 변화마다 차트 resize + 데이터 redraw 발생
  → 순간적으로 2-3번의 불필요한 리드로우

해결:
  차트 데이터가 실제로 변경될 때만 fitContent() 호출
  → useEffect의 의존성 배열을 정밀하게 관리
  → 컨테이너 크기 변화만으로는 전체 리드로우 하지 않음

5. 색상 시스템: 금융 차트의 컨벤션

차트의 색상은 한국 주식 시장의 컨벤션을 따랐습니다.

[색상 규칙]

상승 (양봉): 초록색 (#20c997)
  → 한국 증시에서 상승은 빨간색이 관례이지만,
    글로벌 스탠다드인 초록을 채택
    (토스 증권도 같은 컨벤션을 사용)

하락 (음봉): 빨간색 (#f04452)
  → 하락 = 위험의 직관적 연결

거래량 막대:
  → 해당 캔들의 등락에 따라 초록/빨간으로 색상 동기화
  → 거래량만 봐도 "이날 올랐구나/내렸구나" 파악 가능

배경: 투명 (차트 뒤에 카드 UI가 보이도록)
그리드: 매우 연한 회색 (데이터에 집중하도록)

토스 앱의 디자인 시스템 색상을 그대로 사용한 덕분에, 앱 내에서 이질감 없이 자연스럽게 녹아드는 비주얼을 만들 수 있었습니다.

6. 글래스모피즘과 카드 UI

차트 바깥 영역의 UI는 글래스모피즘(Glassmorphism) 스타일을 적용했습니다. 반투명 배경에 미세한 블러 효과를 준 카드 형태입니다.

[글래스 UI 스타일]

카드 컨테이너:
  배경: 반투명 흰색 (rgba(0, 0, 0, 0.03))
  테두리: 1px solid rgba(0, 0, 0, 0.06)
  둥근 모서리: 16px
  그림자: 미세한 drop shadow

이유:
  → 토스 앱의 미니멀한 디자인 언어에 맞춤
  → 차트가 시각적 주인공이 되도록 나머지 UI는 절제
  → 카드 형태로 정보 영역을 구분하되 무겁지 않게

점수, 하트, 콤보 등의 배지도 같은 글래스 스타일을 적용해서, 전체적으로 통일감 있는 비주얼을 유지했습니다.

7. 마치며

이번 편에서 다룬 내용을 정리하면:

  • lightweight-charts: Canvas 기반 금융 차트 라이브러리로 100개 캔들을 고성능 렌더링
  • 90→100 캔들 전환: visibleCandles 상태로 제어해 자연스러운 결과 공개 연출
  • framer-motion 애니메이션: 스프링 물리학 기반의 자연스러운 게임 피드백
  • 모바일 최적화 3가지 전략: 애니메이션 스태거링, 스프링 파라미터 튜닝, Canvas 리사이즈 제어
  • 금융 차트 색상 컨벤션: 토스 증권과 동일한 초록(상승)/빨강(하락) 체계

프론트엔드 개발에서 성능 최적화는 대부분 "측정 → 병목 발견 → 해결"의 사이클입니다. 이번 프로젝트에서도 PC에서는 전혀 문제 없던 코드가 모바일 WebView에서 병목을 일으켰고, 그 원인을 하나씩 추적하면서 해결해 나갔습니다. "모바일 퍼스트"라는 말은 많이 하지만, 실제로 저사양 기기에서 테스트하기 전까지는 성능 문제를 알기 어렵다는 교훈을 다시 한번 얻었습니다.

마지막 편에서는 앱인토스 플랫폼 배포 과정을 다룹니다. 광고 SDK 연동, 네이티브 네비게이션 브릿지 설정, 그리고 앱스토어 심사에서 반려당한 경험과 대응 과정을 이야기하겠습니다.

👉 차트예측게임 바로가기