프론트엔드

[React] React + Upbit API로 실시간 코인 시세 사이트 만들기 (1)

CyberI 2025. 7. 4. 15:17

안녕하세요.

이번 글에서는 앞선 포스팅에서 소개한 React 와 Upbit API 를 활용하여 간단한 예제를 직접 구현해보겠습니다.

 

목차

1. 프로젝트 세팅 

2. Upbit API 와 WebSocket 연동

 

프로젝트 세팅

UI 구성에 앞서 React 프로젝트에서는 각 기능별로 폴더를 나눠서 관리하는것이 일반적입니다. 이렇게 구조화하면 유지보수와 확장에 유리하고, 협업시에도 각자의 역할이 명확해집니다. 아래는 대표적인 폴더구조 예시입니다.

 

components

재사용 가능한 UI 요소들을 보관하는 폴더입니다.

라우팅과 직접 연결되지 않는, 작고 반복적인 컴포넌트들을 정의합니다.

 

pages

라우팅에 대응되는 페이지 단위 컴포넌트를 보관합니다.

URL 경로와 직접 연결되며, 각 화면의 중심역할을 합니다.

 

hooks

프로젝트 전반에서 사용할 수 있는 커스텀 훅들을 정의합니다.

비즈니스 로직 또는 공통 로직을 재사용할 때 유용합니다.

 

utils

포맷팅 , 날짜계산 , 숫자변환 등 공통 유틸 함수들을 모아둡니다.

 

router

라우터를 설정 및 라우팅 관련 로직을 보관합니다.

 

store

전역 상태 관리를 위한 상태 저장소를 구성하는 폴더입니다.

 

이처럼 컴포넌트와 기능별로 폴더를 구분하면 구조가 체계적으로 정리되어 각 컴포넌트의 역할이 명확해지고, 재사용 가능한 컴포넌트들을 별도로 관리함으로써, 동일한 기능을 여러 화면에서 사용할 수 있어 개발 효율이 향상되는 장점을 가집니다.

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

이번 예제에서는 위에서 설명한 구조를 기준으로 컴포넌트와 파일들을 분리하여 프로젝트를 구성해보겠습니다.

 

App.tsx

App 은 프로젝트의 루트 컴포넌트로 전체 애플리케이션의 진입점 역할을 수행합니다.

router 를 사용하여 페이지 간 전환이 이루어지며 , RouterProvider 태그를 통해 라우팅 기 능을 전역으로 전용합니다.

화면 전환은 router 설정에 따라 이루어지고, url 경로에 따라 알맞은 페이지 컴포넌트를 렌더링합니다.

import { RouterProvider } from 'react-router-dom';
import {router} from '@/router';
function App() {
  return (
    <RouterProvider router = {router}/>
  )
}

 

Router

router 에서는 createBrowserRouter 를 사용하여 라우팅 설정을 객체로 정의하고  이를 외부로 내보냅니다.

루트 경로로 접속 시 <UpbitPage / > 컴포넌트가 화면에 렌더링됩니다.

import { createBrowserRouter } from "react-router-dom";
import UpbitPage from "./pages/UpbitPage";
export const router = createBrowserRouter([
  {
    path: "/",
    element: <UpbitPage />,
  },
]);

 

UpbitPage.tsx

UpbitPage 는 해당 예제의 메인 화면 컴포넌트입니다. UpbitPage 는 크게 세가지 UI 영역 으로 구성할 예정입니다.

 

* SearchBar: 코인 검색 입력 필드

* TabSelector: 코인 시장 구분 탭 (KRW, BTC 등 )

* ProductTable: 화면에 보여질 코인 목록 테이블 ( 필터링된 코인 리스트 )

 

UpbitPage에서는 Upbit 의 API 와 WebSocket 을 통해 코인목록 및 실시간데이터를 가져 오도록 구현합니다. 페이지가 마운트되거나, 탭이 변경될때마다 API 를 호출하여 코인목록을 가져와 현재 선택된 탭에 해당하는 코인만 필터링하여 저장합니다.

import { useEffect } from 'react';
import { useUpbitStore } from '@/stores/upbitStore';
import { useUpbitSocket } from '@/hooks/useUpbitSocket';

import SearchBar from '@/components/upbit/SearchBar';
import TabSelector from '@/components/upbit/TabSelector';
import ProductTable from '@/components/upbit/ProductTable';

const UpbitPage = () => {
  const tab = useUpbitStore((state) => state.tab);
  const setProducts = useUpbitStore((state) => state.setProducts);

  // 실시간 WebSocket 연결 시작
  useUpbitSocket();

  useEffect(() => {
  // 코인 리스트 가져오기
    const fetchProducts = async () => {
      const response = await fetch('https://api.upbit.com/v1/market/all?isDetails=false');
      const data = await response.json();
      const filtered = data.filter((item: any) => item.market.startsWith(tab)).map((item: any) => ({code: item.market, korean_name: item.korean_name,}));
    
      setProducts(tab, filtered);
    }

    fetchProducts();
  }, [tab,setProducts])

  return (
    <div className="upbit">
      <section>
        <SearchBar />
        <TabSelector />
        <ProductTable />
      </section>
    </div>
  )
}

export default UpbitPage;

 

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

 

Upbit API 와 WebSocket 연동

 

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

 

1. 종목코드조회 정보

https://docs.upbit.com/kr/reference/general-info

 

업비트에서 거래 가능한 종목 목록을 조회합니다.

API URL :  https://api.upbit.com/v1/market/all

 

2. 실시간 시세 WebSocket 정보

https://docs.upbit.com/kr/reference/general-info

 

현재가를 조회합니다.

WebSocket URL : wss://api.upbit.com/websocket/v1

 

종목코드조회 API 호출 및 상태관리

종목코드조회 API 를 호출하기 전에 상태관리에 대해 알아보겠습니다.

 

upbitStore.ts

현재 예제에서는 Zustand 라이브러리를 사용하여 가볍고 간단하게 전역 상태를 관리하도록 구성하였습니다.

Product, MarketTab, UpbitState 상태들을 관리하고 setTab, setSearch, setProducts, updateTicker 함수들을 통해 상태를 갱신합니다.

import { create } from 'zustand';

export type Product = {
  code: string;
  korean_name: string;
  trade_price?: number;
  change_rate?: number;
  change?: string;
};

export type MarketTab = 'KRW' | 'BTC' | 'USDT';

type UpbitState = {
  tab: MarketTab; // 선택된 탭
  search: string; // 검색어
  products: Record<MarketTab, Product[]>; // 탭별 코인 목록(원본 데이터 저장)
  visible: Product[]; // 화면에 보여줄 코인 목록(검색, 필터링된 목록)
  tickers: Record<string, any>; // 실시간 가격 정보
  prevPrices: Record<string, number>; // 이전 가격 (가격 변화 감지용)

  setTab: (tab: MarketTab) => void;
  setSearch: (keyword: string) => void;
  setProducts: (tab: MarketTab, list: Product[]) => void;
  updateTicker: (data: any) => void;
};

export const useUpbitStore = create<UpbitState>((set, get) => ({
  tab: 'KRW',
  search: '',
  products: { KRW: [], BTC: [], USDT: [] },
  visible: [],
  tickers: {},
  prevPrices: {},

  // 탭 변경 함수
  setTab: (tab) => {
    console.log('현재탭 : ' + tab);
    set({ tab });
  },

  // 검색어 입력 시 호출
  setSearch: (keyword) => {
    const tab = get().tab;
    const all = get().products[tab];
    const filtered = keyword
      ? all.filter((p) => p.korean_name.includes(keyword))
      : all;
    set({ search: keyword, visible: filtered });
  },

  // 코인 목록 저장 (API 호출 후)
  setProducts: (tab, list) => {
    set((state) => {
      const next = { ...state.products, [tab]: list };
      const visible =
        tab === state.tab
          ? state.search
            ? list.filter((p) => p.korean_name.includes(state.search))
            : list
          : state.visible;
      return { products: next, visible };
    });
  },

  // 실시간 데이터 업데이트
  updateTicker: (data) => {
    const { tickers, prevPrices } = get();
    const prev = tickers[data.code]?.trade_price ?? 0;
    const current = data.trade_price ?? 0;

    if (prev === current) return; // 가격변동 없으면 set 안 함

    set({
      tickers: { ...tickers, [data.code]: data },
      prevPrices: { ...prevPrices, [data.code]: prev },
    });
  },
}));
//?? 0 -> 값이 undefined나 null이 나왔을 때 비교오류 방지용 (기본값으로 0을 설정)

 

다음으로 종목코드조회 API 호출단계입니다. 이전에 확인했던 UpbitPage 로직을 다시 살펴 보겠습니다.

fetch 함수를 사용하여 Upbit API 를 호출하고, 모든 코인 목록을 받아와 JSON 형식으로 파싱합니다.

파싱한 데이터중 현재 탭에 해당하는 데이터들만 필터링 후, 마켓코드와 코인이름 데이터를 꺼내서 setProducts 함수를 통해 저장합니다.

종목코드조회 API 호출을 위해 UseEffect 를 사용하여 컴포넌트가 마운트되거나 Tab 이 변경될 때 실행되도록 설정하였습니다.

useEffect(() => {
    // 코인 리스트 가져오기
    const fetchProducts = async () => {
      const response = await fetch('https://api.upbit.com/v1/market/all?isDetails=false');
      const data = await response.json();
      const filtered = data.filter((item: any) => item.market.startsWith(tab)).map((item: any) => ({code: item.market, korean_name: item.korean_name,}));
    
      setProducts(tab, filtered);
    }

    fetchProducts();
  }, [tab,setProducts])

 

LifeCycle

useEffect 를 알아보기에 앞서 React의 LifeCycle 에 대해 알아보도록 하겠습니다.

React의 컴포넌트는 다음 세가지의 생애 주기를 가집니다.

 

* Mount = 컴포넌트가 처음 DOM에 나타날 때 ( 화면에 처음 렌더링될 때 )

* Update = props 또는 state 가 변경되어 컴포넌트가 다시 렌더링될 때

* Unmount = 컴포넌트가 DOM 에서 제거될 때 ( 화면에서 제거될때 )

 

UseEffect 를 사용하면 LifeCycle의 특정 시점마다 작업을 수행할 수 있습니다.

 

UseEffect 란

React 컴포넌트의 생명주기 시점에 특정 작업 ( 사이드 이펙트 )을 수행할 수 있게 해주는 Hook 입니다.

useEffect(() => {
  // 실행할 코드 (side effect)
  
  return () => {
    // (선택) 컴포넌트 언마운트 시 실행될 정리(clean-up) 함수
  };
}, [의존성배열]);

 

UseEffect 는 다음과 같은 구조이고 , 의존성배열에 따라 실행 시점을 설정할 수 있습니다.

의존성배열이 빈경우 ( [] ) 컴포넌트가 처음 마운트될 때 한 번만 실행되고, [A, B]를 작성하 면 A 또는 B 값이 변경될 때 실행됩니다. return 안에 함수는 컴포넌트가 언마운트 될 때 실행 됩니다. 두번째 인수인 의존성배열이 생략되었을 경우에는 매 렌더링될 때마다 실행되므로 비효율적입니다.

 

WebSocket 연결

실시간 시세를 받아오기위해 WebSocket 을 연결해보겠습니다.

먼저 UseEffect 를 사용하여 최초 마운트가 되었을 때 WebSocket 을 연결하도록 로직을 구현합니다.

최초 구독은 visible 기준 즉 , 현재 화면에 보여줄 코인목록을 기준으로 전송합니다.

서버에서 오는 실시간 시세정보는 updateTicker 함수를 통해 전역상태에 반영되도록 설정 하였습니다.

탭또는 검색어입력등의 이유로 화면에 보여줄 코인목록이 변경되었을 경우, 코인목록에 맞 는실시간 시세정보를 받아와야 하기 때문에 다시 구독을 전송하도록 구현합니다.

// WebSocket 커스텀훅

import { useEffect, useRef } from 'react';
import { useUpbitStore } from '@/stores/upbitStore';

export const useUpbitSocket = () => {
  const socketRef = useRef<WebSocket | null>(null);
  const upbitApi = 'wss://api.upbit.com/websocket/v1';

  const visible = useUpbitStore((state) => state.visible);
  const updateTicker = useUpbitStore((state) => state.updateTicker);

  // WebSocket 연결은 최초 마운트 시 한 번만 실행
  useEffect(() => {
    console.log('[WebSocket] 연결 시작');
    socketRef.current = new WebSocket(upbitApi);

    socketRef.current.onopen = () => {
      console.log('[WebSocket] 연결 완료');

      // 최초 visible 기준으로 구독 전송
      const codes = useUpbitStore.getState().visible.map((coin) => coin.code); //getState() 로 접근시 이때는 값 보임
      sendSubscribeMsg(codes);
    };

    socketRef.current.onmessage = async (e) => {
      const data = await new Response(e.data).json();
      updateTicker(data);
    };

    socketRef.current.onclose = () => {
      console.log('WebSocket 연결 종료');
    };

    socketRef.current.onerror = (e) => {
      console.error('WebSocket 에러 :', e);
    };

    return () => {
      socketRef.current?.close();
    };
  }, []);

  // visible이 바뀔 때마다 send()
  useEffect(() => {
    if (!visible.length) return;
    //console.log(`socketRef.current?.readyState ----> ${socketRef.current?.readyState}` )
    if (socketRef.current?.readyState !== WebSocket.OPEN) return; // WebSocket.OPEN == 1

    const codes = visible.map((coin) => coin.code);
    sendSubscribeMsg(codes);
  }, [visible]);

  // 구독 전송 함수
  const sendSubscribeMsg = (codes: string[]) => {
    const msg = JSON.stringify([
      { ticket: 'react-upbit' },
      { type: 'ticker', codes },
      { format: 'DEFAULT' },
    ]);

    try {
      socketRef.current?.send(msg);
    } catch (err) {
      console.error('WebSocket 전송 실패:', err);
    }
  };
};

 

결론적으로 현재 탭 ( 마켓 )에 맞는 종목코드조회 API 를 요청 후 , 받아온 데이터를 전역상태에 저장하고, 상태에 따라 해당 코인들의 실시간 가격을 웹소켓으로 구독하고, 받아온 실시간 데이터를 상태에 반영하게 됩니다.

 

여기까지 종목코드조회 API 와 WebSocket 연동에 대해 알아보았습니다.

 

다음시간에는 받아온 실시간 데이터를 실제 화면에 보여주는 부분을 구현해보겠습니다.