프론트엔드

[Vue.js 3.0] Vue 3 + UPbit API로 실시간 코인 시세 사이트 만들기 (1)

CyberI 2025. 4. 22. 09:32

안녕하세요.

이번 글에서는 앞선 포스팅에서 소개한 Vue 3와 UPbit API를 활용하여 간단한 예제를 직접 구현해보겠습니다.
실습 과정에서 Vue의 컴포넌트 구조와 컴포넌트간의 상호작용 방식에 대해서도 함께 알아보겠습니다.

 

목차

  1. 프로젝트 세팅과 UI 구성
  2. UPbit API 연동하여 종목명 표시하기

 

1-1. 프로젝트 생성 및 VSCode 확장 프로그램 소개

이전 포스팅에서 Node.js 설치와 프로젝트 생성 방법을 다뤘습니다.
이번에는 동일한 방식으로 Vue 프로젝트를 만들고, 로컬 환경에서 실행해보겠습니다.

 

[Vue.js 3.0] (1) Vue 소개 및 프로젝트 생성

안녕하세요. 이번 글에서는 웹 개발에서 중요한 역할을 하는 프론트엔드 프레임워크 Vue.js를 설명드리겠습니다. 우선 Vue.js에 대해 알아보고 프로젝트를 생성하며 간단한 예제를 만들어보고, 다

cyberx.tistory.com

 

프로젝트를 시작하기 전에, 개발 환경을 효율적으로 구성하는데 도움을 줄 수 있는 VSCode 확장 프로그램을 소개합니다.

 

| VSCode 확장프로그램 소개

1)    Vue – Official(Volar) : Vue 팀에서 공식적으로 제공하는 Vue 3 개발용 확장 (TypeScript 지원 포함)

2)    ESLint : 코드 문법 및 스타일 검사

3)    Prettier : 코드 포맷 정리 자동화

4)    Vue 3 Snippets : Vue3에 특화된 자동완성 제공

 

위와 같은 확장프로그램을 설치하면 코드 자동완성, 문법 오류 표시, 자동 포맷팅 등 다양한 기능을 통해 개발 생산성을 향상시킬 수 있습니다. 특히 Vue3를 지원하는 확장 프로그램들을 통해 최신 문법을 지원받고 일관된 코드 스타일을 유지할 수 있습니다.

 

 

1-2. UI 구성

UI 구성에 앞서, Vue 프로젝트에서는 컴포넌트를 역할에 따라 views/와 components/ 로 나누어 관리하는 것이 일반적입니다.

 

| views/와 components/의 차이

views/ : 페이지 단위 컴포넌트로 라우터에 직접 연결되는 대상. 화면 전체를 구성하며 여러 개의 하위 컴포넌트를 조합함

components/ : 재사용 가능한 UI로 라우터에 직접 연결되지 않으며, 다양한 화면에서 재사용이 가능함

 

이처럼 컴포넌트를 목적에 따라 나누는 방식은 프로젝트 구조가 체계적으로 정리되어 각 컴포넌트의 역할이 명확해지고 재사용 가능한 UI요소를 따로 분리함으로써 동일한 기능을 여러 화면에서 활용할 수 있어 개발 효율이 향상되는 장점을 가집니다.

 

또한 각 컴포넌트가 독립적으로 동작하도록 구성되기 때문에 기능을 수정하거나 추가하더라도 다른 부분에 영향을 최소화할 수 있기 때문에 유지보수가 수월합니다.

 

 

 이번 예제에서도 views/ 디렉토리에 메인 페이지를, components/ 디렉토리에 테이블 영역 컴포넌트를 분리하여 생성하겠습니다.

  • 메인 페이지 컴포넌트 생성 : views/upbit/upbitList.vue
  • 테이블 컴포넌트 생성 : components/products/productList.vue

 

upbitList.vue 파일의 예시코드입니다.

테이블 영역은 <ProductList /> 컴포넌트를 사용해 화면에 출력합니다.

<template>
    <div>
      <section id="upbit">
          <ProductList />
      </section>
    </div>
  </template>
  
  <script setup>
  import ProductList from "@/components/products/productList.vue";
  </script>

 

upbitList.vue 파일은 메인 페이지 역할을 하므로 Vue Router를 통해 해당 컴포넌트를 URL 경로에 연결합니다. 이를 위해 src/router/index.js 파일에 라우팅 설정을 추가합니다. 아래 예시는 초기 진입시 /main으로 이동한 코드입니다.

import { createRouter, createWebHistory } from 'vue-router'
import upbitList from '@/views/upbit/upbitList.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      redirect: { name: 'main' }
    },
    {
        path: `/main`,
        name: 'main',
        component: upbitList,
        props: true
    },
  ],
})

export default router

 

App.vue 파일은 프로젝트의 루트 컴포넌트로 라우터를 통해 연결된 페이지를 출력하는 역할을 합니다.  <router-view /> 태그를 통해 현재 경로에 해당하는 컴포넌트를 동적으로 렌더링합니다.

productList.vue 파일에는 테이블 마크업을 추가하고 스타일을 자유롭게 적용하여 UI구성을 완료합니다.

<template>
    <div class="contentsArea">
      <table class="checkTable">
        <colgroup>
          <col width="140px" />
          <col width="110px" />
          <col width="90px" />
        </colgroup>
        <thead>
          <tr>
            <th class="alignCenter" scope="col">한글명</th>
            <th class="alignCenter" scope="col">현재가</th>
            <th class="alignCenter" scope="col">전일대비</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td> 
              <strong></strong>
            </td>
            <td class="alignRight">
            </td>
            <td class="alignRight">
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </template>

 

여기까지 메인 페이지와 테이블 컴포넌트를 분리하여 구성해보았습니다. 다음 단계에서는 업비트 API를 연동하여 실제 데이터를 불러오고, 테이블에 정보를 표시하는 기능을 구현해보겠습니다.

 

2-1. UPbit API 와 WebSocket 소개

 

이번 예제에서는 업비트에서 제공하는 종목 코드 조회 API와 현재가를 조회하는 WebSocket을 사용합니다. 연결된 사이트에서 요청과 응답 형식의 예시를 확인할 수 있습니다.

 

1) 종목 코드 조회

 

업비트 개발자 센터

 

docs.upbit.com

 

2) 실시간 시세 WebSocket

 

업비트 개발자 센터

 

docs.upbit.com

  • 현재가를 조회합니다.
  • WebSocket URL : wss://api.upbit.com/websocket/v1

 

 

2-2. 종목 코드 조회 API 호출 및 상태 관리 (axios + Pinia)

앞선 포스팅에서 소개한 Pinia와 axios 통신을 활용하여 API를 호출하고 해당 값을 전역에서 사용해볼 차례입니다.

axios는 별도의 파일을 만들고 API를 요청하는 로직은 api/ 디렉토리 하위에 별도 파일로 분리해 구성했습니다.

  • axios :  utils/axios.js 파일 생성
import axios from "axios";
export const awaitApi = async (method, url, _headers, data) => {
  const axios = createAxios(_headers);

  if (method == "GET") {
    try {
      const response = await axios.get(url);
      const responseData = response.data;

      return returnAwaitApi(responseData);
    } catch (e) {
      return returnAwaitApi(e);
    }
  } else if (method == "POST") {
    try {
      const response = await axios.post(url, data);
      const responseData = response.data;

      return returnAwaitApi(responseData);
    } catch (e) {
      return returnAwaitApi(e);
    }
  }
};

const returnAwaitApi = (data) => {
  if (data instanceof Error) {
    return {
      success: false,
      result: null,
      error: data,
    };
  }

  return {
    success: true,
    result: data,
    error: false,
  };
};

const createAxios = (_headers) => {
  return axios.create({
    headers: _headers,
  });
};

 

  • API : api/upbitApi.js 파일 생성

UPbit API종목 정보를 가져오기 위해 getProducts() 함수를 구현합니다.

import { awaitApi } from "@/utils/axios";
export default {
  async getProducts() {
    return await awaitApi("GET", "https://api.upbit.com/v1/market/all");
  },
};

 

스토어를 생성하고 반응형 상태값과, api를 호출하여 값을 가공하는 함수를 생성합니다. 실습에서는 우선 KRW마켓의 종목만을 사용하겠습니다. KRW 마켓 종목은 객체 형태로 저장하여 종목 코드(key)로 빠르게 접근할 수 있도록 구성합니다.

  • 스토어 생성 : stores/upbit.js
import { ref } from "vue";
import { defineStore } from "pinia";
import upbitApi from "@/api/upbitApi.js";

export const useUpbitStore = defineStore("upbitStore", () => {
    const consts = {
        markets: {
            KRW: 'KRW'
        }
    }

    const allProducts = ref([])
    const krwProducts = ref({})
 
    const productGroup = ref({
        ALL: allProducts,
        KRW: krwProducts
    })

    async function getProducts() {
        const { result } = await upbitApi.getProducts();

        allProducts.value = result.map((item) => {
            return {
                key: item.market,
                item: item,
            };
        });

        allProducts.value.forEach(item => {
            const marketName = item.key.split('-')[0]

            if (marketName === consts.markets.KRW) {
                krwProducts.value[item.key] = item
            }
        })
    }

    return {
        consts,
        productGroup,
        getProducts
    }
});

 

2-3. 데이터바인딩

productList.vue 파일에서는 getProducts() 함수를 호출하여 종목 데이터를 불러옵니다. 이는 upbitStore에서 API를 통해 데이터를 가져오는 비동기 함수로 다음과 같이 사용됩니.

import { useUpbitStore } from "@/stores/upbit.js";
const upbitStore = useUpbitStore()
await upbitStore.getProducts();

 

다음으로 각 종목의 데이터를 표시할 테이블의 행 영역을 별도의 컴포넌트로 분리합니다.

  • 테이블 행 영역 컴포넌트 생성 : components/products/productItem.vue

 

분리된 컴포넌트에 데이터를 전달하기 위해 props를 사용합니다. 이는 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 방식입니다.

 

| props란

부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 방식

  • 단방향 데이터 흐름 (부모 -> 자식)
  • 자식에서 수정 불가(읽기 전용)
  • 타입 지정이 가능함(안전한 데이터 처리가 가능)

 

데이터를 컴포넌트 간에 직접 전달하는 방식에는 props와 emit이 있습니다. 두 방식의 차이를 간단히 비교해보고 앞서 사용한 전역 상태 관리 방식인 store와의 차이도 함께 살펴보겠습니다.

 

| props vs emit vs store

  • props : 부모->자식으로 데이터를 전달하기 위함 :value=”data”형태로 명시하는 것이 필요
  • emit : 자식->부모로 이벤트를 전달하기 위함 $emit(‘event’) 형식으로 사용
  • store : 전역 상태로 접근이 가능하며 여러 컴포넌트에서 상태 공유를 목적으로 사용함 어디서든 직접 접근이 가능함

 

부모 컴포넌트인 productList.vue에서 자식 컴포넌트인productItem.vue로 :product=”product” 형식을 사용하여 개별종목 데이터를 props로 전달합니다. 자식 컴포넌트에서는 defineProps로 해당값을 전달받아 사용합니다.

  • productList.vue
<tr v-for="(product, index) in productGroup['KRW']" :key="index">
	<ProductItem :product="product" />
</tr>
// 스토어 값을 가져오기 위해 추가합니다.
import { useUpbitStore } from '@/stores/upbit.js'
import {storeToRefs} from "pinia";
const upbitStore = useUpbitStore()
const { productGroup } = storeToRefs(upbitStore)
  • productItem.vue
<template>
  <td>
    <strong>{{ product.item.korean_name }}</strong>
  </td>
  <td class="alignRight">
  </td>
  <td class="alignRight">
  </td>
</template>
<script setup>
import { ref, computed, watch } from "vue";
import { useUpbitStore } from "@/stores/upbit.js";
import {storeToRefs} from "pinia";
const props = defineProps({
  product: {
    type: Object,
  }
});
</script>

 

이로써 전체 종목 중 KRW로 거래 가능한 종목을 필터링하여 종목명을 테이블에 표시하는 작업까지 완료하였습니다.

다음 시간에는 WebSocket을 통해 실시간 시세의 변동을 수신하고 테이블에 반영해 실시간 UI 업데이트를 구현해보겠습니다.