[프로젝트 회고] 삼성청년SW아카데미 2학기 평가 프로젝트(2), 금융 게임 서비스 주주핀
지그농2026. 3. 11. 23:34
주주핀(ZooZooFin) : 재밌게 금융 지식을 익히고, 쉽게 자산 증식을 배우는 웹앱 서비스 회고
프로젝트 기간 : 2024.08 ~ 2024.10 (8주) 서비스 유형 : Web 역할 : UX/UI, 프론트엔드 개발, 프로젝트 발표 팀 구성 : 백엔드 3명 프론트엔드 2명 AI 1명 (총 6명) 기술 스택 : JavaScript, React, npm, Zustand, Styled Components, Blender, Three.js, R3F, Apex Charts 평가 : 삼성 청년 SW 아카데미 2학기 평가 프로젝트 - 특화 프로젝트 (금융) 성과 : 삼성 청년 SW 아카데미 특화 프로젝트 우수상
들어가며
삼성 청년 SW 아카데미 2학기 특화 트랙은 팀이 주제를 직접 선정하는 방식으로 진행되었고, 저희 팀은 핀테크 트랙을 선택했습니다.(2024년 기준)
서비스 주제를 결정하는 과정에서 기술이나 금융 이론을 먼저 논의하지 않고, 대신 각자가 금융과 관련해 실제로 느끼고 있던 어려움을 공유했습니다.
팀원 모두 사회 초년생이었고, 금융 지식이 충분하지 않은 상태에서 주식 종목 선택이나 예,적금 가입 등 투자 판단을 내려야 하는 상황 자체가 큰 부담으로 다가오고 있었습니다.
'정보는 많지만, 무엇을 어떻게 봐야 할지는 잘 모르겠다'는 공통된 막막함이 있었습니다.
이 문제의식에서 출발해
"금융을 공부하고 싶은 누구나 부담없이, 쉽게 시작할 수 있는 방법은 없을까?"
라는 질문으로 시작한 프로젝트가 주주핀(ZooZooFIn)입니다.
1. 프로젝트 개요
주주핀(ZooZooFin)이란?
주주핀은 사회 초년생을 위한 금융 교육 게임형 웹앱 서비스입니다.
(왼쪽부터) 홈 화면, 주식 거래소 화면 진입, 주식 상세 정보 확인
2024년 기준 과거 2년의 실제 주식 데이터를 활용해 턴 기반 시나리오를 구성했고,
사용자는 주주시티라는 게임 속 가상의 세계에서동물 캐릭터가 되어 예금·적금·국내 주식·해외 주식·ETF·대출 등 현실 금융 요소를 직접 선택하고 그 결과를 마주하며 자산 관리를 간접적으로 경험합니다.
단순한 퀴즈나 강의 형식이 아니라, 주식 차트·재무제표·이동평균선·LSTM 기반 힌트 뉴스를 활용한 투자 의사결정 구조를 게임 안에 녹여냈습니다. 상품별 이자 계산 기능을 통해 리스크를 감안한 대출을 시도하거나, 안정적으로 턴을 마치기 위해 예적금 상품을 가입하는 등 다양한 전략을 직접 선택하고 결과를 확인할 수 있습니다. 모든 턴이 끝나면 게임 결과를 리포트로 제공하며, 안정형부터 공격형까지 실제 금융 리포트에서 사용하는 용어로 자신의 투자 성향을 확인할 수 있습니다.
여기에 Blender로 직접 제작한 3D 오브젝트와 Three.js · R3F 기반의 3D 인터페이스로 메인 화면을 구성해 서비스의 몰입감을 높였습니다.
선택-결과-학습이 자연스럽게 반복되는 게임 구조를 통해 금융 지식을 외워야할 정보가 아닌, 행동의 결과로 이해할 수 있도록 하는 자연스러운 금융 학습 경험을 프로젝트 주제로 삼았습니다.
2. 왜 이 서비스를 기획하게 되었나요 ?
기존 금융 관련 서비스를 조사하며 가장 크게 느낀 한계는 지속성이었습니다.
이를 극복하기 위해 실제 게이미피케이션을 도입한 서비스들이 이미 존재했지만,
실제 사용 흐름을 보면 결국 텍스트 중심의 퀴즈 풀이와 같은 학습 구조에 머무르는 경우가 많았습니다.
금융은 단순 지식이 아니라 의사결정의 연속에 가깝습니다. "이 종목에서 어떤 상품을 살까?", "지금 어떤 방식으로 대출을 받아야 될까?" 와 같은 선택은 결과를 직접 경험해보지 않으면 체감하기 어렵습니다.
그래서 팀의 핵심 문제를 다음과 같이 정의했습니다.
"금융 판단의 결과를, 실패를 포함해 안전하게 경험할 수 있다면 사용자는 더 오래, 더 몰입해서 학습하지 않을까?"
이 질문에 대한 답으로 아래 다섯 가지 요소를 갖춘 게임형 웹앱을 기획하게 되었습니다.
턴 기반 구조 — 선택과 결과가 턴 단위로 명확하게 이어지는 게임 흐름
실제 주식 데이터 기반 시나리오 — 과거 2년간의 실제 데이터를 활용해 현실감 있는 투자 환경 구성
리스크가 명확하게 드러나는 금융 선택지 — 대출·투자 결과가 수치로 즉시 반영되는 구조
이자 직접 계산 기능 — 예적금·대출 상품의 이자를 직접 계산하며 판단 근거를 형성
LSTM 기반 금융 뉴스 힌트 — 투자 전 의사결정을 돕는 긍정적·부정적 투자 가능성 힌트 제공
실제 주식 데이터를 기반으로 한 턴 기반 시나리오 안에서 사용자가 직접 채널을 열고, 차트와 재무제표를 읽고, 투자 결정을 내리는 구조를 설계했습니다. 학습이 게임의 목적이 아니라, 게임을 잘 하기 위해 자연스럽게 금융 지식을 습득하게 되는 구조를 만드는 것이 주주핀 프로젝트의 핵심 목표입니다.
3. 담당 역할
주주핀에서 메인 화면, 주식 거래소, 제3금융 대출 기능의 UX/UI 설계와 프론트엔드 개발을 담당하였습니다. 팀 대표로 두 번의 기획 평가와 최종 평가 발표를 진행했으며, 기획 발표 부문에서 우수 평가를 받았습니다.
기획 단계에서는 Figma를 활용해 서비스 전체 프로토 타입과 디자인 가이드를 정리하며, 금융 정보가 많은 서비스 특성상 사용자가 단계적으로 정보를 이해할 수 있도록 화면 구조와 인터랙션 우선순위를 설계했습니다.
개발 단계에서는 제한된 일정 내 핵심 기능을 완성하기 위해 애자일 방식으로 진행했으며, 화면별로 서로 다른 프론트엔드 과제에 집중했습니다.
먼저 메인 화면에서는 Blender로 제작한 3D 오브젝트를 Three.js와 R3F로 렌더링하며, 초기 로딩과 인터랙션 성능 저하 없이 사용자가 자유롭게 탐색할 수 있는 구조를 구현하는 데 주력했습니다. 다음으로 주식 거래소 화면에서는 3개의 주식 채널과 구매·판매 2가지 액션에서 발생하는 중복 UI와 상태 로직을 공통 컴포넌트로 추상화해, 기능 확장 시 코드 수정 범위를 최소화하고 유지보수성을 높였습니다.
4. 왜 이 기술 스택을 선택하게 되었나요?
먼저 프론트엔드 개발을 기준으로 기술 스택은 다음과 같습니다.
구분
기술 스택
Frontend
JavaScript, React
상태 관리
Zustand
스타일링
Styled Components
3D
Blender, Three.js, R3F
차트 시각화
Apex Charts
주주핀은 턴 기반 게임 로직과 그융 데이터 표현이 동시에 요구되는 서비스였습니다.
그래서 기술 선택의 기준을 다음과 같이 정리했습니다.
턴 기반 게임 상태를 명확하게 관리할 수 있는가
금융 데이터를 직관적으로 시각화할 수 있는가
학습 몰입도를 높일 수 있는 UI/인터랙션을 구현할 수 있는가
4-1) 상태 관리 전략 - Zustand 선택 배경
주주핀의 주식 거래소 기능은 현재 턴을 기준으로 주식 상한가, 환율, 보유 자산, 차트 데이터가 서로 영향을 주는 구조였습니다. 또한 해당 기능은 여러 프론트엔드 팀원이 동시에 개발을 진행하고 있었습니다.
게임 속 다양한 데이터
특히 당시 팀 내에는 React로 프론트엔드 개발을 처음 경험하는 팀원도 있었기 때문에, 상태 관리 도구 선택 시 학습 곡선과 가독성을 중요한 기준으로 고려했습니다.
이러한 배경에서 보일러플레이트가 적고, 상태 흐름을 직관적으로 파악할 수 있는 Zustand를 상태 관리 라이브러리로 선택했습니다. 주식 관련 상태를 store 폴더 아래에 기능 단위로 정리하여 여러 팀원이 동시에 작업하더라도 상태 흐름을 한눈에 파악할 수 있도록 구성했습니다.
(적용) 턴 기반 주식 시스템에서의 상태 집중 관리 필요성
주주핀의 주식 시스템은 다은과 같은 특징을 가지고 있었습니다.
턴에 따라 노출되는 주식 종류가 달라짐 (기본 : 국내 주식, 5턴 이상 : 해외 주식, 10턴 이상 : ETF 채널 개설)
환율, 상한가, 차트 데이터가 서로 영향을 줌
한 번의 선택이 이후 여러 턴의 상태에 연쇄적으로 영향을 미침
이러한 구조에서 각 컴포넌트가 개별적으로 상태를 관리할 경우 데이터 흐름을 추적하기 어려워질 것이라 판단했습니다.
이에 따라 주식 관련 상태를 하나의 Store로 집중 관리하고, 턴 기준 조건 분기와 데이터 로딩 책임을 Store 단에서 처리하도록 설계했습니다.
zoom.enabled : 사용자가 특정 구간을 확대해 직접 흐름을 탐색할 수 있도록 설정
toolbar.show : 툴바를 제거하여 차트 자체에 집중하도록 설정
stroke.curve : 'smooth', 라인 그래프 흐름을 부드럽게 표현하도록 설정
grid, axis labels : 숫자 해석이 필요하거나 축 설정이 필요할 때 설정할 수 있습니다. (주주핀에서는 라벨 제거)
dropShadow : 라인 그래프 자체에 그림자 효과
(한계) 차트 컴포넌트 책임에 대한 아쉬움
차트 컴포넌트 내부에서 데이터를 가공하여 Object.values로 전달하는 구조로 구현했다는 것이 아쉬움으로 남았습니다.
차트에 전달되는 데이터가 이미 “차트 전용 데이터 구조”로 정리되어 있었다면, 컴포넌트는 렌더링에만 집중할 수 있었을 것입니다.
이 경험을 통해 시각화 컴포넌트는 가능한 한 가공된 데이터를 전달받는 것이 유지보수에 유리하다는 기준을 갖게 되었고, 이후 프로젝트에서는 데이터 변환 책임을 Store 혹은 서버 상태 레이어로 분리하려는 방향성을 명확히 하게 되었습니다.
4-3) 몰입형 UI - Three.js + R3F + Blender
홈 화면은 서비스 진입 화면이자, 매 거래를 마치고 사용자가 가장 오래 머무르는 공간입니다.
캐릭터가 머무르는 방을 컨셉으로 하여 노트북을 클릭하면 현재 투자 현황을 확인할 수 있고, 침대를 클릭하면 다음 턴으로 넘어가는 구조로, 단순한 홈 화면이 아니라 게임의 중심으로 설계했습니다.
이런 역할을 하는 공간을 2D 랜딩 페이지나 정적인 메뉴 구조로 만드는 것은 맞지 않다고 판단했습니다. 사용자가 실제로 게임 세계 안에 있다는 몰입감을 주면서, 이후 서비스를 확장할 수 있는 구조를 처음부터 갖추는 것이 중요했습니다.
3D 환경이 금융 교육 게임 서비스에서 기술적으로 과할 수 있다는 고민도 있었습니다. 그러나 주주핀이 단순 학습 서비스가 아닌 게임 서비스로서의 정체성을 가져야 한다는 점에서 Three.js + R3F + Blender 스택을 도입했습니다.
기술
선택 이유
Three.js
웹 3D 표준 라이브러리, 풍부한 커뮤니티
React Three Fiber (R3F)
useState, useEffect 등 기존 React 훅을 그대로 활용하면서 3D 씬을 컴포넌트로 관리 가능
Blnder
오픈소스, GLTF 내보내기 지원, 자유로운 모델 구조 정의 가능
(적용1) Blender로 자체 모델 제작
(왼쪽에서부터) 목업으로 구도 확인 후 홈 화면 구도 설정과 조명 작업 처리
AI로 생성한 배경 이미지와 캐릭터를 바탕으로 구도를 먼저 잡고, Blender에서 방 모델을 직접 제작했습니다.
완성된 모델은 glb 형식으로 내보내 Three.js에서 불러오는 구조로 연결했습니다.
(왼쪽에서부터) 단계별 모델링 작업
나무 재질의 방 구조에 노트북이 놓인 책상, 침대 등 클릭 가능한 오브젝트들로 구성했습니다.
모델 구조는 Blender의 Collection 단위로 정리했는데, 이후 Three.js에서 오브젝트별 클릭 이벤트를 Group 단위로 바인딩 할 때 핵심적인 역할을 했습니다.
(적용2) Three.js 와 R3F로 리액트 환경에서 안정적으로 랜더링
R3F는 크게 두 패키지로 나뉩니다.
@react-three/fiber는 Three.js를 React에서 사용할 수 있게 해주는 코어 패키지이고, @react-three/drei는 그 위에서 자주 쓰이는 기능들을 편의 컴포넌트로 제공하는 유틸리티 패키지입니다. useGLTF나 OrbitControls같이 직접 구현하면 복잡한 기능들을 drei에서 가져와 사용했습니다.
import { useFrame, useLoader, useThree } from '@react-three/fiber';
// useFrame : 매 프레임마다 실행되는 렌더 루프
// useLoader : 텍스처 등 에셋을 병렬로 로드
// useThree : camera, renderer 등 Three.js 컨텍스트에 접근
import { useGLTF, OrbitControls } from '@react-three/drei';
// useGLTF : Blender에서 내보낸 .glb 파일 로드 + Suspense 통합
// OrbitControls: 줌·회전·팬 카메라 컨트롤
useGLTF로 모델을 로드하고, useLoader로 텍스처를 병렬 로드한 뒤, scene.traverse로 계층 구조를 순회하며 Blender의 재질 이름을 기준으로 텍스처와 물성을 적용했습니다.
(왼쪽에서부터) Blender에서 재질 설정 하지 않았을 경우, 재질 설정한 모델 로드, 실제 텍스쳐 로드
Blender에서 내보낸 파일은 기본 재질 정보만 포함되기 때문에, 웹 환경에서 실제 텍스처를 직접 로드해 교체하지 않으면 시각적 퀄리티가 크게 떨어집니다. scene.traverse로 씬 전체를 순회하며 Blender에서 설정한 재질 이름을 기준으로 각 Mesh를 찾아 텍스처와 노말맵을 교체하는 방식으로 이 문제를 해결했습니다.
또한, 사용자가 방 안을 자유롭게 탐색할 수 있도록 OrbitControls로 줌·회전·팬을 제공했지만, 범위를 무제한으로 열어두면 방 뒤편이나 천장처럼 어색한 각도가 노출되는 문제가 있었습니다. 이를 방지하기 위해 수직각·수평각·줌 거리를 각각 제한해 사용자가 의도한 시점 안에서만 탐색하도록 설계했습니다.
R3F의 useGLTF는 내부적으로 React Suspense와 통합되어 있어, 모델이 완전히 로드되기 전까지 렌더링 자체를 중단시킵니다. 덕분에 텍스처나 지오메트리가 미완성인 상태의 씬이 순간적으로 노출되는 문제 없이, 로딩이 완료된 시점에 씬 전체를 한 번에 표시할 수 있었습니다.
프로젝트 초기에는 서비스에 등장하는 캐릭터를 3D 모델링과 애니메이션으로 구현하는 것을 목표로 시작했습니다.
하지만 팀 전체가 Blender를 처음 사용한 상태에서 모델링과 애니메이션 제작을 동시에 진행하다 보니, 짧은 개발 기간 내 자연스러운 모션과 안정적인 퀄리티의 캐릭터를 구현하는 데 한계가 있었습니다.
특히 캐릭터 애니메이션의 자연스러움과 모델 완성도 측면에서 기대한 수준에 미치지 못하는 부분이 있었고, 서비스 전체 완성도를 고려했을 때 캐릭터 표현 방식에 대한 방향을 재검토할 필요가 있다고 판단했습니다.
결과적으로 캐릭터는 2D 이미지를 3D 공간의 평면 메시에 텍스처로 적용하는 방식으로 변경했습니다. 이 방식은 구현 난이도를 낮추면서도 프로젝트 일정 내에 서비스 전체를 완성할 수 있는 현실적인 선택이었으며, 동시에 방 내부의 아기자기한 분위기와도 잘 어울린다고 판단해 최종 구현 방식으로 결정했습니다.
(한계 2) 초기 렌더링 과정에서의 리소스 로딩 문제와 최적화 한계
또한 성능 측면에서도 개선이 필요한 부분이 있었습니다.
모바일 저사양 기기나 네트워크 환경이 좋지 않은 경우3D 모델 로딩 지연이나 프레임 저하가 발생하는 현상을 확인했습니다.
특히 3D 모델과 텍스처를 함께 사용하는 과정에서초기 렌더링 시 일부 오브젝트가 검정색으로 표시되는 문제가 발생했습니다.
(왼쪽) 텍스처 로딩 이전 상태 (오른쪽) 텍스처 적용 이후 정상 렌더링 화면
Three.js 환경에서 모델과 텍스처 리소스가 비동기적으로 로드되는 구조로 인해 발생한 현상으로, 모델은 먼저 렌더링되지만 텍스처 로딩이 완료되지 않은 상태에서는 재질이 정상적으로 적용되지 않아 일시적으로 검정색으로 표시되었습니다.
이 현상은 네트워크 환경이 느리거나 모바일 기기에서는 텍스처 로딩 시간이 길어지면서 더욱 두드러지게 나타났습니다. 이를 해결하기 위해 React Three Fiber의 Suspense를 활용해 모델과 텍스처 리소스가 모두 로딩된 이후 씬이 렌더링되도록 구성하여, 텍스처가 적용되지 않은 상태의 화면이 사용자에게 노출되지 않도록 개선했습니다.
다만 이번 프로젝트에서는 Blender 모델링부터 Three.js · React Three Fiber 기반 3D 구현을 처음 시도하는 과정이었기 때문에, 텍스처 프리로드나 모델 경량화, 리소스 로딩 전략과 같은 추가적인 성능 최적화까지는 충분히 적용하지 못한 아쉬움이 남았습니다.
이 경험을 통해 웹 환경에서 3D 콘텐츠를 적용할 때는 모델 제작뿐 아니라 텍스처 용량, 리소스 로딩 방식, 초기 렌더링 전략까지 함께 고려해야 한다는 점을 배울 수 있었습니다.
5. 기술적 도전 과제
5-1) 조건이 많은 주식 거래 UI에서의 상태 분리
(1) 문제 상황
주식 거래소 화면은 단순한 목록 UI가 아니라 여러 조건이 동시에 작동하는 구조였습니다. 주식 채널(국내·해외·ETF), 주식 분야(IT·금융·에너지 등), 거래 타입(구매·판매)의 조합에 더해, 턴 진행에 따라 접근 가능한 채널이 달라지는 게임 규칙까지 UI에 반영해야 했습니다.
이 모든 조건을 하나의 상태에서 처리하면 렌더링 로직이 복잡해지고, 조건이 추가될수록 유지보수가 어려워질 것이 분명했습니다. 그래서 구현보다 상태의 역할을 먼저 정의하는 것을 우선했습니다.
(2) 해결 전략 : 상태의 역할 분리
주식 거래소 화면의 상태를 세 가지로 나눴습니다.
상태 유형
관리 위치
역할
게임, 도메인 상태
Zustand Store
주식 데이터, 환율, 보유 주식, 턴
UI 상태
컴포넌트 내부 (useState)
토글, 모달, 선택값
렌더링
공통 컴포넌트
데이터를 받아 UI 표시
(3) Store 레벨 : 게임 상태 관리
게임 진행에 영향을 주는 상태는 Zustand Store에서 통합 관리했습니다. 주식 데이터는 턴에 따라 접근 가능한 채널이 달라지는 구조였기 때문에, turn 값을 기준으로 조건부 데이터 로딩을 구현했습니다.
Store에서 데이터 접근 조건을 관리하도록 구성하면서, UI 컴포넌트는 데이터 구조를 신경 쓰지 않고 렌더링에만 집중할 수 있었습니다.
(4) 컴포넌트 레벨 : UI 상태 관리
토글·모달·선택 상태 같은 UI 상태는 컴포넌트 내부에서 관리했습니다. useEffect를 활용해 채널 변경과 분야 변경을 서로 다른 책임으로 분리한 것을 목표로 채널 변경은 데이터 교체만, 분야 변경은 UI 초기화만 담당하도록 역할을 분리하였습니다.
// 채널 변경 → 목록 데이터 교체
useEffect(() => {
if (channel === '국내 주식') setStockItems(domesticStocks);
else if (channel === '해외 주식') setStockItems(overseasStocks);
else if (channel === 'ETF') setStockItems(ETFStocks);
}, [channel]);
// 분야 변경 → UI 상태 초기화
useEffect(() => {
setOpen([false, false, false]);
}, [field]);
(5) 공통 컴포넌트로 렌더링 책임 위임
렌더링 로직은 StockTitleContainer 공통 컴포넌트에 위임했습니다. 부모 컴포넌트는 조건 처리에만, 자식 컴포넌트는 UI 표현에만 집중하는 구조입니다.
이 구조를 통해 거래 UI에서 코드 재사용률 50% 이상을 확보했고, 채널·분야·거래 타입이 조합되는 복잡한 화면을 컴포넌트 조합으로 해결할 수 있었습니다.
조건이 많은 UI를 구현할 때 상태의 역할을 먼저 정의하는 것이 전체 구조를 단순하게 만든다는 점을 이 화면에서 직접 체감했습니다. 처음부터 구현에 뛰어드는 대신 상태를 어디서 어떻게 관리할지 설계하는 과정이, 결과적으로 개발 속도와 코드 품질 모두에 영향을 준다는 것을 배울 수 있었습니다.
(7) 개선 방향
구현 이후 코드를 다시 돌아보면서 세 가지 아쉬운 부분을 발견했습니다.
첫 번째는 useEffect 의존성 누락 문제입니다.
채널 변경을 감지하는 useEffect에 Store 데이터가 의존성 배열에 빠져 있어, Store에서 데이터가 업데이트되어도 stockItems에 즉시 반영되지 않을 수 있습니다.
// 개선: Store 데이터도 의존성에 포함
useEffect(() => {
if (channel === '국내 주식') setStockItems(domesticStocks);
else if (channel === '해외 주식') setStockItems(overseasStocks);
else if (channel === 'ETF') setStockItems(ETFStocks);
}, [channel, domesticStocks, overseasStocks, ETFStocks]);
두 번째는 useEffect 의존성 체인 문제입니다.
채널 변경과 분야 변경을 각각 다른 useEffect로 분리한 구조는 역할 분리 의도는 좋지만, 채널이 바뀔 때 분야 초기화도 함께 일어나야 하는 경우 두 useEffect가 연쇄적으로 실행되면서 렌더링이 두 번 발생할 수 있습니다. 이런 경우에는 아래와 같이 이벤트 핸들러 안에서 관련 상태를 한 번에 처리하는 방식을 고려할 수 있을 것 같습니다.
현재 채널별 필터링 로직이 컴포넌트 안에 직접 작성되어 있어, 같은 조건이 여러 곳에서 반복될 경우 유지보수가 어려워집니다. Custom Hook으로 분리하면 로직을 재사용하면서 컴포넌트는 UI 표현에만 집중할 수 있습니다. useMemo를 함께 적용하면stocks나field가 바뀌지 않는 한 필터링 연산을 재실행하지 않아 렌더링 성능에도 도움이 됩니다.
제3금융 화면은 단순한 입력 폼이 아니라 계산 로직, 상태 변화, UI 흐름이 동시에 연결되는 구조였습니다. 사용자가 대출 금액이나 기간을 입력하면 상환 금액이 즉시 계산되어 미리보기 화면에 반영되어야 했고, 대출 승인 시에는 신용도가 즉시 감소하는 게임 로직도 함께 동작했습니다.
특히 이 화면에서는 복리 기반 상환 계산, 선이자 차감 방식, 입력값 변경 시 실시간 재계산, 대출 승인 시 신용도 하락, 입력 → 미리보기 → 결과로 이어지는 단계의 UI 흐름이 동시에 작동해야 했습니다.
이 로직을 컴포넌트 내부에서 모두 처리하면 계산 로직, 상태 관리, UI 흐름이 한 곳에 섞이고, 계산 공식이 변경되거나 대출 조건이 추가될 때 컴포넌트 복잡도가 빠르게 증가할 것이기 때문에 구현 초기 단계에서 세 가지 책임을 계층별로 분리하는 구조를 먼저 설계했습니다.
계층
관리 위치
역할
계산
유틸 함수
순수 계산만 수행
게임, 도메인 상태
Zustand Store
대출액, 기간, 신용도
UI 상태
컴포넌트 내부 (useState)
단계 전환, 모달
(2) 계산 계층 : 순수 함수로 분리
대출 계산은 컴포넌트나 Store 내부에서 직접 처리하지 않고 별도의 유틸 함수로 분리했습니다. 같은 입력에 항상 같은 결과를 반환하고, 외부 상태를 변경하지 않는 순수 함수 구조로 작성했습니다.
제 3금융 대출 로직은 복리 기반 원리금 균등 상환 계산으로, 연이율을 월 이율로 변환한 뒤 원리금 균등 상환 공식을 적용해 월 상환 금액, 총 상환 금액, 총 이자를 계산합니다.
또한 선이자 방식으로 대출 싱행 시 이자를 먼저 차감하는 구조였습니다. 사용자는 신청 금액 전체를 받는 것이 아니라 이자를 차감한 netPrincipal만 실제로 지급받습니다. 이 계산 결과는 UI에서 신청 금액·실제 지급 금액·총 상환 금액을 각각 표시하는 데 사용됩니다.
계산, 상태, UI를 계층별로 분리한 구조 덕분에 계산 공식이 바뀌어도 유틸 함수만 수정하면 됐고, 컴포넌트는 단계 전환 로직에만 집중할 수 있었습니다.
복잡한 계산 로직이 포함된 화면일수록 계산, 상태, UI의 책임을 처음부터 명확히 나누는 것이 전체 구조를 단순하게 유지하는 핵심이라는 점을 이 화면에서 직접 체감했습니다.
(7) 개선 방향
구현 이후 코드를 다시 돌아보면서 세 가지 개선 가능성을 확인했습니다.
첫 번째는 임시 상태의 관리 위치 문제입니다.
loanDraft와 calculatedRepayment는 대출 신청 화면에서만 존재하는 임시 상태임에도 전역 Store에서 관리하고 있습니다. Store는 여러 화면에서 공유되어야 하는 영구 상태(userCredit, totalDebt)를 담는 곳이기 때문에, 이 화면에서만 쓰이는 임시 상태를 함께 올리는 것은 책임 범위를 벗어난 설계입니다. loanDraft와 calculatedRepayment는 컴포넌트 로컬 상태나 useReducer로 관리하고, Store에는 게임 영구 상태만 남기는 것이 더 적합합니다.
// 개선: 임시 상태는 컴포넌트에서 useReducer로 관리
const [loanState, dispatch] = useReducer(loanReducer, {
draft: { type: 'compound', principal: 1000000, interestRate: 15, months: 12 },
calculated: null,
});
// Store에는 게임 영구 상태만
const { userCredit, totalDebt, confirmLoan } = useLoanStore();
두 번째는 confirmLoan 안에서 서버 요청과 로컬 상태 업데이트가 뒤섞인 문제입니다.
현재 구조는 서버 요청이 실패해도 userCredit과 totalDebt가 이미 변경된 상태가 될 수 있습니다. 게임에서 신용도가 잘못 반영되면 치명적인 버그로 이어지기 때문에, 서버 저장 성공을 확인한 뒤에 로컬 상태를 업데이트하는 순서로 처리하는 것이 안전합니다.
confirmLoan: async () => {
set({ isLoading: true, error: null });
try {
// 서버 저장 먼저
const response = await apiClient.post('/loan/apply', { ... });
// 성공 확인 후 로컬 상태 업데이트
const creditPenalty = calculatedRepayment.riskLevel;
set({
userCredit: Math.max(0, get().userCredit - creditPenalty),
totalDebt: get().totalDebt + calculatedRepayment.totalRepayment,
isLoading: false,
});
} catch (error) {
set({ error: '대출 처리 중 오류가 발생했습니다.', isLoading: false });
}
},
세 번째는 대출 조건 업데이트 로직의 중복 제거입니다.
updateLoanPrincipal과 updateLoanMonths는 변경되는 필드만 다를 뿐 내부 구조가 거의 동일합니다. 하나의 함수로 통합하면 계산 로직이 한 곳에서만 관리되어 유지보수가 쉬워집니다.
웹 환경에서 3D를 렌더링하는 경험 자체가 처음이었습니다. Blender 모델링부터 GLTF 익스포트, React 환경에서의 씬 구성, Raycaster 기반 클릭 이벤트 처리, 조명 설계까지 전 과정을 직접 다루면서 3D 씬 그래프 구조와 렌더링 흐름에 대한 이해를 쌓을 수 있었습니다.
특히 Three.js를 직접 사용하는 대신 R3F를 선택하면서 React 컴포넌트 구조 안에서 3D를 다루는 방식을 익혔고, useGLTF와 Suspense의 통합 구조처럼 라이브러리가 어떤 문제를 어떻게 해결하는지를 직접 확인하는 경험이 됐습니다. 단순히 동작하는 3D 화면을 만드는 것을 넘어, 씬 그래프 계층 구조를 설계하고 오브젝트별 이벤트를 Group 단위로 바인딩하는 구조를 고민하면서 3D 개발에 대한 시각을 넓힐 수 있었습니다.
6-2) 컴포넌트 설계 — 재사용성의 기준
주식 거래소 화면에서 3개의 채널과 2가지 액션을 공통 컴포넌트로 추상화하는 과정을 통해, 재사용성은 코드를 줄이는 것이 목표가 아니라 컴포넌트가 책임지는 범위를 명확히 하는 것에서 시작된다는 것을 배웠습니다. Figma 설계 단계에서 먼저 컴포넌트 구조를 정의하고 개발에 들어간 것이 실제로 효과가 있었고, 구현 전 설계가 개발 속도와 코드 품질 모두에 영향을 준다는 것을 확인했습니다.
6-3) 상태 설계 — 역할 분리와 계층화
주식 거래소와 제3금융 화면을 구현하면서 상태 관리에 대한 시각이 달라졌습니다. 처음에는 동작하는 코드를 만드는 데 집중했지만, 구현 이후 코드를 돌아보는 과정에서 loanDraft 같은 임시 상태를 전역 Store에 올린 것이 책임 범위를 벗어난 설계였다는 것을 발견했습니다. 상태를 어디서 관리할지는 단순히 동작 여부가 아니라 그 상태가 어디서 필요한지를 기준으로 결정해야 한다는 것을 이 프로젝트에서 처음으로 체감했습니다.
7. 회고
Keep
기획 단계에서 Figma 프로토타입을 통해 공통 컴포넌트 구조를 먼저 정의하고 개발에 들어간 것이 주식 거래소 화면에서 실질적인 효과를 발휘했습니다. 설계가 먼저 있으면 개발 중에 방향을 잃지 않는다는 것을 이 프로젝트에서 확인했고, 기획 발표 우수 평가와 최종 우수상이라는 결과가 그 방향이 맞았다는 것을 확인해줬습니다.
처음 다루는 기술 스택임에도 3D 인터페이스와 실제 주식 데이터 기반 시나리오를 결합해 팀 목표였던 "선택-결과-학습이 자연스럽게 반복되는 구조"를 끝까지 유지하며 구현해낸 것도 의미 있는 경험이었습니다.
Problem
Blender 작업 범위를 기획 초기에 제대로 잡지 못했습니다. 캐릭터까지 3D로 구현하는 것을 목표로 시작했지만 일정 안에 원하는 퀄리티를 내지 못했고, 결과적으로 2D 이미지로 대체하는 방향을 선택했습니다. 기술 스택의 러닝 커브를 기획 단계에서 함께 고려했다면 처음부터 현실적인 범위를 잡을 수 있었을 것입니다.
임시 상태를 전역 Store에 올린 설계 실수도 아쉬운 부분입니다. loanDraft처럼 특정 화면에서만 쓰이는 상태와 게임 영구 상태를 같은 Store에서 관리하는 구조로 만든 것은 구현 당시 동작하는 코드를 완성하는 데 집중하다 보니 놓친 부분이었습니다. 상태 설계를 구현 전에 더 신중하게 고민했어야 했다는 아쉬움이 남습니다.
Try
기술 스택 선정과 작업 범위는 기획 단계에서 러닝 커브와 일정을 함께 놓고 팀원들과 결정할 것입니다. 그리고 상태 설계는 구현 전에 "이 상태가 어디서 필요한가"를 먼저 정의하는 방식으로 접근할 것입니다. 동작하는 코드를 빠르게 만드는 것도 중요하지만, 구조를 먼저 잡는 것이 결과적으로 더 빠르다는 것을 이 프로젝트에서 배웠습니다.
마치며
주주핀은 기획부터 3D 모델링과 구현, 상태 관리 설계, 발표까지 프로젝트 전 과정을 주도적으로 담당한 프로젝트였습니다.
금융이라는 다소 진입장벽이 높은 주제를 어떻게 하면 사용자가 자연스럽게 받아들일 수 있을까를 고민하며 서비스를 설계했고, 그 결과물이 우수상으로 이어진 경험은 사용자 경험을 중심에 두고 기술적 구현 방향을 결정하는 것이 맞는 접근임을 확인해준 계기가 되었습니다.
개발 과정에서는 상태 관리 설계, 계산 로직 구조화, 컴포넌트 역할 분리 등 프론트엔드 구조를 고민하며 구현했고, QA 과정까지 진행하면서 실제 서비스 개발과 유사한 환경에서 다양한 경험을 쌓을 수 있었습니다.
동시에 프로젝트를 회고하면서 초기에 설계하지 못했던 구조적인 문제나, 더 나은 상태 관리 방식, 코드 추상화 방법 등 여러 개선 가능성도 확인할 수 있었습니다. 특히 구현에 집중하다 보니 초기 설계 단계에서 충분히 고려하지 못했던 부분들이 있었고, 이를 복기하면서 코드 구조와 설계의 중요성을 다시 한 번 체감할 수 있었습니다.
웹 프로젝트로 프론트엔드 개발에 처음 도전한 만큼 부족한 부분도 많았지만, 팀원들의 도움과 협력 속에서 프로젝트를 완성할 수 있었고 그 과정 자체가 삼성 청년 SW 아카데미 교육 기간 동안 가장 기억에 남는 경험으로 남았습니다.