Skip to content
sieun edited this page Dec 5, 2024 · 58 revisions
Juga 이미지

실시간 주식 데이터를 활용한 모의투자 경험을 통해 주식 투자에 대해 배울 수 있는 서비스

📈 개요

실제 주식 시장의 데이터를 실시간으로 활용하여 가상으로 투자 경험을 쌓을 수 있는 서비스이다. 사용자는 모의 자산을 활용해 주식을 거래하고, 투자 성과를 바탕으로 주식에 대해 재미있게 공부할 수 있다.

🗂️ 기술 스택

Common

Infra

DevOps

🎯 기능 설명

✔️ 메인 페이지

메인 페이지에서는 각종 주가 지수, 급상승 급하락 종목, 그리고 실시간 증권 뉴스를 확인할 수 있어요.

main

FE

  • 주가 지수의 실시간 데이터를 위해 Socket.io를 이용해 실시간 데이터 제공했습니다.
  • 라이브러리 없이 주가 지수 차트를 구현했습니다.
  • Top5 주식을 종목에 맞게 보기 위해 Nav를 생성했는데 사용자의 UX측면에서 Nav의 버튼의 Interaction이 필요해 보였습니다.
    • 처음에는 각각의 버튼의 border를 넣어서 표시했다 하지만 부드러운 UI를 위해서는 애니메이션이 더 적합하다고 생각했습니다.
    • 하지만 border를 이용할 경우 이동하는 애니메이션을 구현하기 어려웠습니다. 따라서 아래 표시선이 이동하는 슬라이딩 애니메이션을 구현하기 위해서는 표시선(막대선)을 위한 별도의 엘리먼트를 추가했습니다.
    • 구현 과정
    • Top5 주식에서 사용자가 어떤 버튼(ex: 전체, 코스피 등)을 클릭 했는지 저장하기 위해 useState와 search param을 이용하는 방식 중에 고민을 진행했습니다.
      • useState를 이용하는 방법이 가장 직관적으로 생각할 수 있는 방법이었지만, 새로고침을 할때마다 사용자가 입력한 정보가 사라진다는 문제가 있었습니다.
      • 새로고침을 할 때마다 사용자가 입력한 내용이 사라지는 것은 사용자 경험 측면에서 좋지 않다고 판단하여 search param을 이용해 사용자가 어떤 버튼을 클릭했는지 저장하여 화면에 표시했습니다.

BE

  • 한국투자 Open API 요청에 필요한 Access Token을 싱글톤으로 관리할 필요성을 느꼈습니다.
    • 필요한 부분을 나누어 개발하였기 때문에, 초기에는 각 서비스 로직별로 Access Token을 발급받아 사용하였습니다.
    • 그러나 동일한 세션 내에서 Access Token 발급 제한 시간이 설정되어 있어, 발급 실패로 인한 에러가 자주 발생하였습니다.
    • 이를 해결하기 위해 Access Token을 싱글톤으로 관리하는 방식으로 변경하였습니다.
    • 싱글톤 방식으로 관리하면서 한 세션에 대해 하나의 Access Token만 발급받고 이를 관리하게 되어, 기존에 발생하던 에러가 더 이상 발생하지 않게 되었습니다.
  • 주가 지수와 관련된 데이터를 실시간으로 조회할 수 있게 하기 위한 웹소켓 로직을 구현했습니다.
    • 한국 투자 증권에서 주가 지수 차트와 관련된 실시간 웹소켓을 지원하지 않는 문제가 있었습니다. Cron을 통해 API를 주기적으로 호출하여 데이터를 가져온 뒤, 클라이언트로는 socket.io로 보내주는 방식으로 구현해 문제를 해결했습니다.
    • 처음에는 한국 투자 증권과 백엔드 서버 사이의 연결 또한 socket.io 라이브러리를 활용해 구현하려고 했으나, 연결이 되지 않는 문제가 발생했습니다. 한국 투자 증권 내부적으로 socket.io가 아닌 웹소켓으로만 구현이 되어 있었기 때문에 ws 모듈을 사용해 해결했습니다.
    • 자세한 과정은 링크에서 보실 수 있습니다.
  • 주식과 관련된 뉴스를 제공하기 위해 네이버 뉴스 API를 사용하여 뉴스 정보를 가져왔습니다.
    • 네이버 뉴스 API의 일일 호출 횟수가 제한되어 있어, Cron을 활용하여 일정 주기마다 API를 호출하고 DB에 저장할 수 있도록 구현하였습니다.
    • 실제로 FE에 데이터를 전달할 때는 DB에 저장된 값을 이용하여 전달할 수 있도록 처리하였습니다.

✔️ 검색

키워드를 포함한 종목에 대해 검색을 할 수 있어요. 더해서, 최근 검색 기록을 통해 사용자 경험을 개선하려 했어요.

search

FE

  • debounce를 이용하여 불필요한 검색 API 호출 제어하도록 했습니다.
  • 검색어에 맞게 highlight를 추가하여 UX 개선했습니다.
  • 일부 환경에서 한글 Composition 관련 오류 발생했습니다.
    • 초기에는 Safari 브라우저에서만 발생한다고 판단하여 Safari 브라우저와 Chrome 브라우저의 한글 Composition 처리 방식 차이를 조사했습니다. (자세한 내용은 링크 참고)
    • 현재는 근본적인 해결은 불가능하다고 판단하여 Known Issue로 둔 후에 UI적으로 문제 없이 보이는 방향으로 수정한
  • 사용자가 '카카오'를 'zkzkdh'라고 검색해도 가능하도록 만들었습니다.
    • 빈배열일 경우 한글로 변환하여 쿼리를 요청하는 로직을 추가하여 구현했습니다. (자세한 내용은 링크 참고)

BE

  • Redis를 활용한 검색 로그 처리
    • 검색어 입력, 수정과 조회가 빈번하게 말생하는 최근 검색어 데이터를 RDBMS 대신 Redis에 저장하였습니다.

✔️ 로그인

카카오 소셜 로그인을 지원하고 있어요. 간단한 테스트만을 원하시는 사용자를 위한 테스트 계정도 준비되어 있어요.

login

FE

  • vite proxy server
    • 로컬에서 서버로 API를 요청할 때 쿠키가 자동으로 담기지 않는 문제가 있었습니다. 로컬 도메인과 서버 도메인이 달랐고 브라우저의 SOP(Same-Origin-Policy)에 따라 도메인이 다르면 쿠키가 전송되지 않는 것이었습니다.
    • 이 문제를 vite에서 제공해주는 proxy server를 통해 해결할 수 있었습니다. 이를 통해 로컬에서도 원활하게 api 테스트 할 수 있는 환경을 구축해 개발 생산성을 높일 수 있었습니다.
  • zustand를 활용한 로그인 상태 관리
    • 전역 상태로 로그인 여부를 isLogin 변수로 저장합니다.
    • 로그인이 성공했을 때 isLogin 변수를 true로 설정하고 로그아웃이 성공했을때 false로 설정합니다.
    • 처음 페이지에 들어오거나 새로고침이 될 때는 전역상태 정보가 초기화되므로 checkAuth라는 쿠키를 활용해 로그인 여부를 판별하는 API를 활용해 isLogin 상태를 설정합니다.

BE

  • JWT 토큰 기반 인증
    • JWT 토큰을 사용해 AuthGuard를 구성하고 일반 로그인을 구현하였습니다.
  • kakao OAuth 구현
    • 카카오 로그인 API를 연동하여 소셜 로그인을 구현하였습니다.
    • Authorization Code를 받아 서버에서 Access Token으로 교환한 후 로그인을 처리하였습니다.
    • 프론트엔드에서는 리다이렉트된 카카오 페이지에서 로그인을 진행하고 콜백 URL로 받은 인가 코드를 서버로 전달하는 흐름을 통해 구현하였습니다.

✔️ 주식 상세 페이지

주식 상세 페이지에서 특정 종목에 대한 차트와 실시간 체결가를 확인할 수 있어요. 로그인 된 사용자는 가상 머니로 거래도 가능해요.

detail1

FE

  • chart
    • 라이브러리 없이 하나의 Canvas를 이용해 전체 차트를 그리니 많은 문제들이 있었습니다.
    • 주식 차트의 기능을 구현하기 위해서는 하나의 차트로는 힘들다고 생각했습니다.
    • 예를 들어 높낮이 조절하는 기능을 위해서는 2개의 차트의 높이를 조절해야하는데 Canvas내에서 Interaction을 구현하는 것은 어렵다고 판단했습니다.
    • 문제를 해결하기 위해 주식 차트 중 가장 많이 사용되는 TradingView 라이브러리를 분석했고, 결과적으로 차트를 5개의 Canvas로 분리하였고 차트 높낮이 조절, 마우스 위치에 따른 값 표기, 이동 평균선 표시, 차트 확대, 시간대별 차트 보기 기능을 구현했습니다.
    • 라이브러리 없이 직접 구현한 이유
  • Socket 갯수 제한으로 인한 구독 해지 요청
    • 외부 API 제한으로 인해 Socket 이벤트 구독 해지 요청이 필요했습니다.
    • 따라서 useEffect의 return문을 이용해 구독 해지 API 요청을 보내는 방식으로 구현했습니다.
    • 구현 과정에서 useQuery의 데이터가 로딩이 될 때마다 렌더링이 발생하며 return문이 실행되는 문제 발견하여 useQuery를 부모 요소에서 진행하여 데이터를 전달하는 방식으로 useEffect의 return문이 실행되는 문제를 해결했습니다.
  • 주식 매수/매도
    • 사용자가 입력한 가격을 상한가와 하한가 사이로 제한하기 위해 onBlur 이벤트를 활용했습니다. onBlur 이벤트는 input 태그의 focus가 사라질 때 호출되며, 이를 활용해 사용자가 입력한 가격이 상한가를 초과하거나 하한가보다 낮을 경우 다음과 같은 동작을 구현했습니다.
      1. 입력 값이 상한가를 초과하면, 상한가로 값을 갱신합니다.
      2. 입력 값이 하한가보다 낮으면, 하한가로 값을 갱신합니다.
      3. 위 조건을 만족하지 않으면, 입력 값을 그대로 유지합니다.
    • 사용자가 상한가/하한가 제한을 넘어선 값을 입력했을 때는 경고 메시지를 띄워 사용자가 가격 제한을 이해할 수 있도록 UX를 개선했습니다.
    • 매도 창에서 사용자가 매도 결정을 신중히 내릴 수 있도록 예상 손익, 예상 수익률 정보를 제공했습니다.
      • 예상 손익: (현재 입력 가격 - 평균 매수 가격) * 매도 수량
      • 예상 수익률: ((현재 입력 가격 - 평균 매수 가격) / 평균 매수 가격) * 100
    • 이 기능은 사용자가 매도 시 발생할 수 있는 잠재적 이익이나 손실을 명확히 인지하도록 돕는 데 중점을 두었습니다.

BE

  • 주식 상세페이지에서 사용할 한국투자 Open API WebSocket 연결이 필요했습니다.
    • 한국투자 Open API WebSocket에 연결을 시도했을 때, 연결이 아예 되지 않거나 연결이 되어도 값이 하나도 전달되지 않는 문제가 발생했습니다.
    • 문제의 원인을 찾아보니 다른 로직에서 동일한 WebSocket 요청을 이미 하고 있어서 발생했던 문제였습니다.
    • 따라서 각 로직에 서로 영향을 끼치지 않게 하기 위해 세션을 두 개로 분리해서 WebSocket 연결을 했습니다.
    • 위 방식으로 진행하던 중, 동일 요청에 대해 두 개의 세션을 하나로 합쳐 최대한 세션을 절약하는 것이 좋겠다는 의견이 나왔습니다. 해당 의견을 반영하여 세션을 하나로 합치는 방식으로 변경하였습니다.
    • 이 과정에서 한국투자 Open API WebSocket에 잘못된 구독 해제 요청을 하지 않도록, 사용 중인 종목 코드에 대한 연결별로 count를 세는 로직을 추가하였습니다. 이를 통해 모든 연결이 종료된 경우에만 구독 해제를 수행하도록 수정하였습니다.
  • 주식 상세 페이지에서 실시간으로 변동되는 값을 FE에 전달하기 위해 Socket.IO와 SSE 중 어떤 방식을 사용할지 고민하였습니다.
    • 기존 코드에서는 Socket.IO를 사용하고 있었으나, 전체 코드에서 양방향 통신이 필요하지 않다는 점에서 SSE를 고려하였습니다.
    • 그러나 HTTP/1.1 기준 크롬 브라우저에서는 최대 6개의 연결만, HTTP/2 기준으로는 기본적으로 100개의 연결만 지원되는 제한이 있었습니다.
    • 또한, Nginx에서 추가 설정이 필요한 문제가 있어, 기존에 사용하던 Socket.IO 방식을 다시 채택하기로 결정하였습니다.
  • 주식 상세 페이지에서 진행되는 매수 및 매도 기능을 구현하기 위해 고민했습니다.
    • 한국 투자 증권의 웹소켓 자원은 하나의 계좌 당 41개로 제한되어 있었기 때문에, 모든 종목들에 대한 구독을 계속 유지하기에는 한계가 있다고 생각했습니다. 그래서, 체결이 완료되어 미체결 주문이 남아있지 않은 종목에 대해서는 한국 투자 증권과의 구독을 끊도록 구현해 해결했습니다. 자세한 과정은 링크에서 보실 수 있습니다.
    • 한국 투자 증권과 백엔드 서버 사이의 연결이 끊어지는 문제가 빈번하게 발생했습니다. PINGPONG 로직과 재연결 로직을 추가해 해결했습니다.
    • 웹소켓 세션 관리를 위해 로드 밸런싱을 추가함에 따라, 같은 종목의 실시간 체결가가 여러 서버로 전송되어 주문 체결이 중복으로 일어나는 현상이 발생했습니다. lock을 활용해 해결했습니다.

✔️ 랭킹

데일리 수익률과 총 자산을 기준으로 상위 10명의 사용자의 순위 및 내 순위를 확인할 수 있어요.

image

BE

  • Redis Sorted Set을 활용한 실시간 랭킹
    • RDBMS의 ORDER BY 쿼리가 빈번하게 발생하여 서버의 부하가 많이 발생하는 문제를 해결하기 위해 Redis의 Sorted Set 자료 구조를 도입하였습니다.
    • 점수를 score로 사용하여 자동으로 정렬되는 Sorted Set의 특성을 활용해 정렬 연산이 반복되지 않고 랭킹을 조회할 수 있었습니다.
    • 인메모리에 랭킹 정보가 존재하지 않을 때에만 RDBMS의 ORDER BY 쿼리를 발생시켜 서버의 부하를 줄였습니다. 자세한 이야기는 링크에서 보실 수 있습니다.

✔️ 마이페이지

현재 내 보유 자산과 수익률을 실시간으로 확인할 수 있어요. 현재 대기 중인 주문과 즐겨찾기에 등록한 종목을 마이페이지에서 확인할 수 있어요.

mypage3

FE

  • 마이페이지의 각 탭(보유자산현황, 주문요청현황, 즐겨찾기, 내정보)을 URL의 쿼리 매개변수로 관리하여 상태를 유지했습니다. React Router의 searchParams를 활용해 ?section=account, ?section=order, ?section=bookmark, ?section=info와 같은 방식으로 탭 상태를 설정했습니다.
  • 사용자가 탭을 클릭하면 URL의 section 값을 변경하여 현재 활성화된 탭 상태를 명시적으로 표시했습니다. 이렇게 하면 브라우저를 새로고침하거나 뒤로 가기/앞으로 가기를 눌러도 사용자가 이전에 보고 있던 탭 상태를 유지할 수 있습니다. 또한, 특정 탭 상태를 URL에 저장하기 때문에 해당 링크를 복사하거나 북마크로 저장하면 원하는 탭으로 바로 접근할 수 있습니다.

BE

  • 사용자의 주식 자산은 보유 종목의 현재가에 따라 달라지기 때문에, 해당 부분을 실시간으로 구현할 때 고민이 있었습니다. 마이페이지 API를 조회하는 시점에서 DB를 업데이트해주고 업데이트된 결과를 반환하도록 구현해 문제를 일부 해결했습니다. 자세한 과정은 링크에 기재되어 있습니다.

📜 개발 일지

⚠️ 트러블 슈팅

❗ 규칙

🗒️ 기록

기획
회의록
데일리스크럼
그룹 멘토링
그룹 회고

😲 개별 멘토링

고동우
김진
서산
이시은
박진명
Clone this wiki locally