본문으로 건너뛰기

업비트 오픈API로 거래소 페이지 만들어보기 - 거래 현황

O-h-y-o약 3 분

업비트 오픈API로 거래소 페이지 만들어보기 - 거래 현황

UpbitTrade.vue 파일을 새로 만들어줍니다.

지난번처럼 Trade API 를 먼저 호출해보겠습니다.

20개의 데이터를 가져오겠습니다.

Codes

// socket-upbit.ts
state: () ISocketState => ({
  ...
  reloadTrade: async () => {},
})
// global.d.ts
interface ISocketState {
  ...
  reloadTrade: Function;
}

interface ITradeResponse {
  change_price: number;
  sequential_id: number;
  prev_closing_price: number;
  timestamp: number;
  trade_price: number;
  trade_volume: number;
  ask_bid: string;
  market: string;
  trade_date_utc: string;
  trade_time_utc: string;
}
// UpbitTrade.vue

<script setup lang="ts">
import axios from "axios";
import { onMounted } from "vue";
import { useUpbitSocketStore } from "src/stores/socket-upbit";

const upbit = useUpbitSocketStore();

const getTradeAPI = async () => {
  const res = await axios.get(
    `https://api.upbit.com/v1/trades/ticks?market=${upbit.selectCoin}&count=20`
  );

  const data = res.data as ITradeResponse[];
};

onMounted(() => {
  getTradeAPI();
  upbit.reloadTrade = getTradeAPI;
});
</script>

API Response 예시



이번에는 받아온 데이터들의 키 값을 simple하게 바꿔주겠습니다.

Codes

<script setup lang="ts">
import axios from "axios";
import { ref, onMounted } from "vue";
import { useUpbitSocketStore } from "src/stores/socket-upbit";
import { convertTradeKeys } from "src/utils/rule";

const upbit = useUpbitSocketStore();

const getTradeAPI = async () => {
  const res = await axios.get(
    `https://api.upbit.com/v1/trades/ticks?market=${upbit.selectCoin}&count=20`
  );

  const data = res.data as ITradeResponse[];
  const convertedData = convertTradeKeys(data);
  upbit.tradeList.push(...convertedData);
};

onMounted(() => {
  getTradeAPI();
  upbit.reloadTrade = getTradeAPI;
});
</script>
// utils/rule.ts
export const convertTradeKeys = (
  obj: ITradeResponse | ITradeResponse[]
): any => {
  if (Array.isArray(obj)) {
    return obj.map((item) => convertTradeKeys(item));
  }

  const convertedObj: any = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      convertedObj[convertTradeType[key]] = obj[key];
    }
  }
  return convertedObj;
};

export const convertTradeType = {
  change_price: "cp",
  sequential_id: "sid",
  prev_closing_price: "pcp",
  timestamp: "tms",
  trade_price: "tp",
  trade_volume: "tv",
  ask_bid: "ab",
  market: "market",
  trade_date_utc: "td",
  trade_time_utc: "ttm",
};
// global.d.ts
// 차트를 만들때의 타입 그대로
interface ISocketTradeResponse {
  ab: string;
  c: string;
  cd: string;
  cp: number;
  pcp: number;
  sid: number;
  st: string;
  td: string;
  tms: number;
  tp: number;
  ttm: string;
  ttms: number;
  tv: number;
  ty: string;
}

convertTradeKeys라는 재귀함수를 이용하여 데이터 키 값들을 소켓으로 오는 simple 키 값으로 변경하였습니다.

데이터들을 정리를 하였으니 UI를 만들어 바로 적용시켜보겠습니다.

  • 체결시간(UTC)
  • 체결가격(KRW)
  • 체결량(BTC)
  • 체결금액(KRW)
Code
// UpbitTrade.vue
<template>
  <div class="trade-wrap">
    <ul class="trade-title">
      <li
        v-for="title in [
          '체결시간',
          '체결가격(KRW)',
          '체결량(BTC)',
          '체결금액(KRW)',
        ]"
        :key="title"
      >
        {{ title }}
      </li>
    </ul>

    <div class="trade-area">
      <ul class="trade-info-wrap">
        <li v-for="(data, i) in upbit.tradeList" :key="i" class="trade-info">
          <div class="time-zone">
            <span>{{ dayjs(data.tms).format("MM.DD") }}</span>
            <span>{{ dayjs(data.tms).format("HH:mm") }}</span>
          </div>

          <div class="trade-price">{{ data.tp.toLocaleString() }}</div>

          <div :class="data.ab">{{ data.tv }}</div>

          <div>
            {{ Math.round(data.tp * data.tv).toLocaleString() }}
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";
import axios from "axios";
import dayjs from "dayjs";
import { useUpbitSocketStore } from "src/stores/socket-upbit";
import { convertTradeKeys } from "src/utils/rule";

const upbit = useUpbitSocketStore();

const getTradeAPI = async () => {
  const res = await axios.get(
    `https://api.upbit.com/v1/trades/ticks?market=${upbit.selectCoin}&count=20`
  );

  const data = res.data as ITradeResponse[];
  const convertedData = convertTradeKeys(data);
  upbit.tradeList.push(...convertedData);
};

onMounted(() => {
  getTradeAPI();
  upbit.reloadTrade = getTradeAPI;
});
</script>

<style scoped lang="scss">
.trade-wrap {
  width: 100%;

  .trade-area {
    max-height: 500px;
    overflow-y: scroll;
  }
}

.trade-title {
  display: flex;
  border-bottom: 1px solid #eee;
  list-style-type: none;
  margin: 0;
  padding: 0;

  & :first-child {
    text-align: left;
    width: 10%;
    min-width: 100px;
  }

  > li {
    padding: 5px 10px;
    font-size: 1.2px;
    color: rgb(63, 63, 63);
    width: 25%;
    text-align: right;
    margin: 0;
  }
}

.trade-info-wrap {
  padding: 0;
  margin: 0;
}

.trade-info {
  width: 100%;
  display: flex;
  justify-content: space-between;

  & :first-child {
    text-align: left;
    width: 10%;
    min-width: 100px;
    flex: 0;
  }

  div {
    font-size: 11px;
    flex: 1;
    display: block;
    text-align: right;
    padding: 5px 10px;

    &.trade-price {
      font-weight: 700;
    }
  }

  &:nth-child(2n) {
    background-color: #eeeeee;
  }
}
.time-zone {
  > span {
    margin-right: 7px;

    &:last-child {
      font-size: 10px;
      color: #777;
    }
  }
}

html[data-theme="dark"] {
  .trade-title {
    border-bottom: 1px solid #222;

    > li {
      color: #ddd;
    }
  }
  .trade-info:nth-child(2n) {
    background-color: #161616;
  }
}
</style>



이제 차트를 만들때 연결했던 Trade Socket 데이터를 가져와서 TradeList 에 넣어주겠습니다.

  • 체결시간(UTC)
  • 체결가격(KRW)
  • 체결량(BTC)
  • 체결금액(KRW)
Code
// socket-upbit.ts
state: () ISocketState => ({
  ...
  tradeList: ISocketTradeResponse[];
})

actions:{
  connectTradeSocket() {
    tradeSocket = new WebSocket("wss://api.upbit.com/websocket/v1");

    tradeSocket.onopen = (e: any) => {
      tradeSocket.send(
        `${JSON.stringify([
          { ticket: "trade" },
          { type: "trade", codes: ["KRW-BTC"] },
          { format: "SIMPLE" },
        ])}`
      );
    };

    tradeSocket.onmessage = async (payload: any) => {
      const r = (await new Response(
        payload.data
      ).json()) as ISocketTradeResponse;

      this.tradeData = r;

      this.tradeList.unshift(r);

      if (this.tradeList.length > 50) {
        this.tradeList.pop();
      }
    };
  },
}

// utils/rule.ts
export const debounce = (callback: Function, limit = 100) => {
  let timeout: NodeJS.Timeout;
  return function (...args: any) {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      callback.apply(this, args);
    }, limit);
  };
};
// UpbitTrade.vue
<template>
  <div class="trade-wrap">
    <ul class="trade-title">
      <li
        v-for="title in [
          '체결시간(UTC)',
          '체결가격(KRW)',
          '체결량(BTC)',
          '체결금액(KRW)',
        ]"
        :key="title"
      >
        {{ title }}
      </li>
    </ul>

    <div class="trade-area" @scroll="scrolling">
      <ul class="trade-info-wrap">
        <li v-for="(data, i) in upbit.tradeList" :key="i" class="trade-info">
          <div class="time-zone">
            <span>{{ dayjs(data.tms).format("MM.DD") }}</span>
            <span>{{ dayjs(data.tms).format("HH:mm") }}</span>
          </div>

          <div class="trade-price">{{ data.tp.toLocaleString() }}</div>

          <div :class="data.ab">{{ data.tv }}</div>

          <div>
            {{ Math.round(data.tp * data.tv).toLocaleString() }}
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";
import axios from "axios";
import dayjs from "dayjs";
import { useUpbitSocketStore } from "src/stores/socket-upbit";
import { convertTradeKeys, debounce } from "src/utils/rule";

const upbit = useUpbitSocketStore();
const btnSelect = ref(0);

const getTradeAPI = async () => {
  const res = await axios.get(
    `https://api.upbit.com/v1/trades/ticks?market=${
      upbit.selectCoin
    }&count=20&${
      upbit.tradeList.at(-1) ? `${"cursor=" + upbit.tradeList.at(-1).sid}` : ""
    }`
  );

  const data = res.data as ITradeResponse[];
  const convertedData = convertTradeKeys(data);
  upbit.tradeList.push(...convertedData);
};

const scrolling = debounce((a: any) => {
  const percent = Math.floor(
    (a.target.scrollTop / (a.target.scrollHeight - a.target.clientHeight)) * 100
  );

  if (percent > 90) {
    getTradeAPI();
  }
});

onMounted(() => {
  getTradeAPI();
  upbit.reloadTrade = getTradeAPI;
});
</script>

<style scoped lang="scss">
.trade-wrap {
  width: 100%;

  .trade-area {
    max-height: 500px;
    overflow-y: scroll;
  }
}

.trade-title {
  display: flex;
  border-bottom: 1px solid #eee;
  list-style-type: none;
  margin: 0;
  padding: 0;

  & :first-child {
    text-align: left;
    width: 10%;
    min-width: 100px;
  }

  > li {
    padding: 5px 10px;
    font-size: 1.2px;
    color: rgb(63, 63, 63);
    width: 25%;
    text-align: right;
    margin: 0;
  }
}

.trade-info-wrap {
  padding: 0;
  margin: 0;
}

.trade-info {
  width: 100%;
  display: flex;
  justify-content: space-between;

  & :first-child {
    text-align: left;
    width: 10%;
    min-width: 100px;
    flex: 0;
  }

  div {
    font-size: 11px;
    flex: 1;
    display: block;
    text-align: right;
    padding: 5px 10px;

    &.trade-price {
      font-weight: 700;
    }
  }

  &:nth-child(2n) {
    background-color: #eeeeee;
  }
}
.time-zone {
  > span {
    margin-right: 7px;

    &:last-child {
      font-size: 10px;
      color: #777;
    }
  }
}

html[data-theme="dark"] {
  .trade-title {
    border-bottom: 1px solid #222;

    > li {
      color: #ddd;
    }
  }
  .trade-info:nth-child(2n) {
    background-color: #161616;
  }
}
</style>

무한스크롤 기능을 추가하였지만, API를 호출하는 조건이 스크롤 퍼센트 90% 이상일 때 이기에 해당 조건을 변경하거나 갯수 제한을 두어야합니다.

또한 계속해서 데이터를 추가만 한다면 브라우저 렉이 발생할 수 있으므로 소켓 데이터가 추가될때 tradeList 의 마지막 데이터를 없애주어야합니다.