Skip to content

Seongkyun-Yu/upbit-downbit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Downbit 프로젝트 ViewCount

downbit_image
이미지 클릭시 YouTube로 연결됩니다

downbit.ml에서 배포된 프로젝트 내역을 확인하실 수 있습니다.
두나무의 요청으로 배포를 중단했습니다.


Development motivation

Upbit의 실제 거래 데이터를 통해

많은 데이터 수신시 프론트 엔드의 뷰를 최적화 하는 방법을 학습하고자

이번 프로젝트를 시작하였습니다.


Skill

HTML5 CSS3 JavaScript
React Redux Styled-Components
Amazon AWS


Requirements

  • Library
    접기/펼치기 버튼
    React v.16
    axios: 0.20.0
    d3: 5.15.1
    react-redux: 7.2.1
    redux-saga v.1.1.3
    redux-thunk v.2.3.0
    react-router-dom v.5.2.0
    axios v.0.19.2
    websocket: 1.0.32
    react-fast-compare: 3.2.0
    react-financial-charts: 1.0.0-alpha.16
    decimal.js: 10.2.1
    hangul-js: 0.2.6
    lodash: 4.17.20
    moment-timezone: 0.5.31
    styled-components: 5.2.0
    styled-normalize: 8.0.7
    styled-reset": 4.3.0
    @fortawesome/free-brands-svg-icons: 5.15.1
    @fortawesome/free-solid-svg-icons: 5.15.1
    @fortawesome/react-fontawesome: 0.1.12

Getting Started

$ git clone https://github.com/Seongkyun-Yu/upbit-clone.git
$ yarn install
$ yarn start


Main Feature (프로젝트의 모든 기능을 혼자 개발했습니다)

  • 실시간 가격, 거래량 등의 데이터 수신 및 차트 랜더링
  • 실시간 호가창, 거래내역 랜더링
  • 코인 초성, 심볼 검색
  • 매수 총액에 따른 구매수량 자동 조절, 가격 변경에 따른 구매 총액 자동 변경
  • 호가창 클릭시 자동 가격 입력
  • 반응형

프로젝트 구조

├── node_modules
├── public
│   ├── blueLogo.png
│   ├── whiteLogo.png
│   ├── favicon.png
│   └── index.html
├── build
├── src
│   ├── Api
│   │   └── api.js
│   ├── Components
│   │   ├── Global
│   │   │   ├── Header.js
│   │   │   ├── Footer.js
│   │   │   └── Loading.js
│   │   └── Main
│   │       ├── ChartDataConsole.js
│   │       ├── CoinInfoHeader.js
│   │       ├── CoinList.js
│   │       ├── CoinListItem.js
│   │       ├── MainChart.js
│   │       ├── Orderbook.js
│   │       ├── OrderbookCoinInfo.js
│   │       ├── OrderbookItem.js
│   │       ├── OrderInfo.js
│   │       ├── OrderInfoAskBid.js
│   │       ├── OrderInfoTradeList.js
│   │       ├── TradeList.js
│   │       └── TradeListItem.js
│   ├── Pages
│   │   └── Main.js
│   ├── Container                         <-- HOC
│   │   ├── withLatestCoinData.js
│   │   ├── withLoadingData.js
│   │   ├── withMarketNames.js
│   │   ├── withOHLCData.js
│   │   └── ...etc
│   ├── Lib
│   │   ├── asyncUtil.js                  <-- redux-saga, thunk factory pattern
│   │   └── utils.js                      <-- etc utils
│   ├── Reducer
│   │   ├── index.js
│   │   ├── coinReducer.js
│   │   └── loadingReducer.js
│   ├── Router
│   │   └── MainRouter.js
│   ├── styles
│   │   ├── fonts
│   │   ├── GlobalStyle.js
│   │   └── theme.js
│   ├── App.js
│   └── index.js
├── README.md
├── LICENSE
├── package.json
├── yarn.lock
└── .gitignore

프로젝트 관련 생각들


Technical Issue: Optimization

  • 1초에 최대 150개의 데이터가 전송되어 상태를 변경시킴

    • Push 방식의 WebSocket을 Redux-Saga를 이용하여 Pull 방식으로 변경

    • Redux-Saga의 eventChannel을 이용하여 버퍼 생성

    • 0.5초에 한 번 버퍼를 확인하여 중복된 데이터 제거 후 변경내역을 상태에 한번에 업데이트

    • import { call, put, select, flush, delay } from "redux-saga/effects";
      import { buffers, eventChannel } from "redux-saga";
      
      // 소켓 만들기
      const createSocket = () => {
        const client = new W3CWebSocket("wss://api.upbit.com/websocket/v1");
        client.binaryType = "arraybuffer";
      
        return client;
      };
      
      // 소켓 연결용
      const connectSocekt = (socket, connectType, action, buffer) => {
        return eventChannel((emit) => {
          socket.onopen = () => {
            socket.send(
              JSON.stringify([
                { ticket: "downbit-clone" },
                { type: connectType, codes: action.payload },
              ])
            );
          };
      
          socket.onmessage = (evt) => {
            const enc = new TextDecoder("utf-8");
            const arr = new Uint8Array(evt.data);
            const data = JSON.parse(enc.decode(arr));
      
            emit(data);
          };
      
          socket.onerror = (evt) => {
            emit(evt);
          };
      
          const unsubscribe = () => {
            socket.close();
          };
      
          return unsubscribe;
        }, buffer || buffers.none());
      };
      
      // 웹소켓 연결용 사가
      const createConnectSocketSaga = (type, connectType, dataMaker) => {
        const SUCCESS = `${type}_SUCCESS`;
        const ERROR = `${type}_ERROR`;
      
        return function* (action = {}) {
          const client = yield call(createSocket);
          const clientChannel = yield call(
            connectSocekt,
            client,
            connectType,
            action,
            buffers.expanding(500)
          );
      
          while (true) {
            try {
              const datas = yield flush(clientChannel); // 버퍼 데이터 가져오기
              const state = yield select();
      
              if (datas.length) {
                const sortedObj = {};
                datas.forEach((data) => {
                  if (sortedObj[data.code]) {
                    // 버퍼에 있는 데이터중 시간이 가장 최근인 데이터만 남김
                    sortedObj[data.code] =
                      sortedObj[data.code].timestamp > data.timestamp
                        ? sortedObj[data.code]
                        : data;
                  } else {
                    sortedObj[data.code] = data; // 새로운 데이터면 그냥 넣음
                  }
                });
      
                const sortedData = Object.keys(sortedObj).map(
                  (data) => sortedObj[data]
                );
      
                yield put({
                  type: SUCCESS,
                  payload: dataMaker(sortedData, state),
                });
              }
              yield delay(500); // 500ms 동안 대기
            } catch (e) {
              yield put({ type: ERROR, payload: e });
            }
          }
        };
      };
  • 반응형으로 제작시 보이지 않는 컴포넌트를 랜더링 처리

    • display: none으로 처리해도 DOM에는 사라지지 않기 때문에 상태 변경시 랜더링 시도함

    • width 값을 측정하여 조건이 맞을 경우에만 컴포넌트를 랜더링 하게 함

    • throttle 사용으로 과도한 width값 측정 방지

    • import React, { useCallback, useEffect, useState } from "react";
      import { throttle } from "lodash";
      
      const withSize = () => (OriginalComponent) => (props) => {
        const [widthSize, setWidthSize] = useState(window.innerWidth);
        const [heightSize, setHeightSize] = useState(window.innerHeight);
      
        const handleSize = useCallback(() => {
          setWidthSize(window.innerWidth);
          setHeightSize(window.innerHeight);
        }, []);
      
        useEffect(() => {
          window.addEventListener("resize", throttle(handleSize, 200));
          return () => {
            window.removeEventListener("resize", handleSize);
          };
        }, [handleSize]);
      
        return (
          <OriginalComponent
            {...props}
            widthSize={widthSize}
            heightSize={heightSize}
          />
        );
      };
      export default withSize;
  • 초기 차트 데이터를 얼마나 가져와야 하는지에 대한 문제

    • 200개의 캔들을 먼저 가져오고 필요할 시 추가로 요청 후 랜더링

Todo

  • WebSocket 통신
  • 기본 Reducer 제작
  • Thunk Factory Pattern 제작
  • Saga Factory Pattern 제작
  • 캔들 차트 드로잉
  • 호가 차트 드로잉
  • 주문 창 구현

About

upbit 비스무리하게 만들어 보기

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Languages