프론트엔드

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

CyberI 2025. 7. 11. 14:00

안녕하세요.

이번 글에서는 앞선 포스팅에서 구현한 API와 WebSocket 연동이후에 데이터를 받아 화면에 보여주는 부분을 구현해보겠습니다.

 

목차

  1. 데이터 가공과 UI 수정
  2. 검색 및 탭기능 구현

데이터 가공과 UI 수정

ProductTable.tsx

실시간 데이터를 화면에 보여주기 위해 ProductTable 컴포넌트를 구현해보겠습니다.

ProductTable은 현재 선택된 마켓 탭과 검색 조건에 따라 화면에 보여줄 코인 목록(visible)과 실시간 시세 데이터를 테이블 형식으로 렌더링하는 컴포넌트입니다.

ProductTable내부에서는 Zustand의 useUpbitStore을 사용하여 visible 배열상태로 구독하고, 해당 배열을 순회하며 ProductItem컴포넌트를 동적으로 생성하도록 구현합니다.

visible배열에 변화가생기면 ProductTable 컴포넌트는 리렌더링이 되고, 이로인해 ProductItem 컴포넌트도 다시 렌더링됩니다.

ProductItem 컴포넌트에는 props를 사용하여 code값을 전달합니다.

React.memo를 사용하여 불필요한 렌더링을 방지하도록 설정합니다.

import './ProductTable.css';
import React from 'react';
import { useUpbitStore } from '@/stores/upbitStore';
import ProductItem from '@/components/upbit/ProductItem';

const ProductTable = () => {
  const visible = useUpbitStore((state) => state.visible);

  return (
    <div className="contentsArea">
      <table className="checkTable">
        <colgroup>
          <col width="140px" />
          <col width="110px" />
          <col width="90px" />
        </colgroup>
        <thead>
          <tr>
            <th>한글명</th>
            <th>현재가</th>
            <th>전일대비</th>
          </tr>
        </thead>
        <tbody>
          {visible.map((product) => (
            <ProductItem key={product.code} code={product.code} />
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default React.memo(ProductTable);

 

Props란

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

  • 단방향 데이터 흐름(부모 컴포넌트 > 자식 컴포넌트)
  • 자식 컴포넌트에서는 props 수정 불가
  • props의 타입 지정가능
// 부모 컴포넌트
const Parent = () => {
  return <Child name="Alice" age={25} />;
};

// 자식 컴포넌트
const Child = ({ name, age }: { name: string; age: number }) => {
  return (
    <div>
      <p>이름: {name}</p>
      <p>나이: {age}</p>
    </div>
  );
};

 

다음 예제와 같이 부모컴포넌트에서 자식컴포넌트로 데이터를 전달할 수 있습니다.

자식컴포넌트는 props를 함수의 매개변수처럼 받아서 사용합니다.

 

State란
컴포넌트의 내부에서 생성되고 관리되는 동적인데이터입니다. 이 값이 변경되면 컴포넌트가 자동으로 렌더링됩니다.

  • useState를 사용해 상태변수 선언 가능
  • setState를 통해 변경 가능
  • 컴포넌트 자체에서 생성하고, 관리함
  • 값이 변경되면 컴포넌트가 자동으로 리렌더링됨
import { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0); // count라는 state 변수 선언

  const handleClick = () => {
    setCount(count + 1); // state 변경 → 리렌더링 발생
  };

  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={handleClick}>+1 증가</button>
    </div>
  );
};

 

다음 예제와 같이 useState를 사용해 상태변수를 선언하고, setCount를 통해 상태를 변경할 수 있습니다.

 

ProductItem.tsx

개별 코인의 정보를 표시하기위해 ProductItem 컴포넌트를 구현해보겠습니다.

ProductItem는 한개 코인의 정보를 표시하는 단일 행 컴포넌트입니다.

ProductTable컴포넌트에서 props로 전달받은 code(종목코드) 값을 기반으로 해당 코인의 실시간 정보만 상태에서 선택적으로 구독합니다.

UseEffect를 사용하여 가격이 변화되었는지 감지하고, 가격이 변화되었을 경우 UI에 하이라이트 효과(빨간색/파란색 테두리)를 잠시 보여주도록 설정합니다.

ProductTable과 동일하게 React.memo를 사용하여 불필요한 렌더링을 방지하도록 설정합니다.

 

import './ProductItem.css';
import React from 'react';
import { useEffect, useRef, useState } from 'react';
import { useUpbitStore } from '@/stores/upbitStore';
import { formatPriceByTab } from '@/utils/formatPrice';

interface Props {
  code: string;
}

const ProductItem = ({ code }: Props) => {
  //선택적 구독
  const ticker = useUpbitStore((state) => state.tickers[code]);
  const prev = useUpbitStore((state) => state.prevPrices[code]);
  const tab = useUpbitStore((state) => state.tab);
  const product = useUpbitStore((state) =>
    state.products[tab].find((p) => p.code === code),
  );

  const [highlight, setHighlight] = useState<'rise' | 'fall' | null>(null);
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);

  const changeRate = ticker?.signed_change_rate;
  const tradePrice = ticker?.trade_price;
  const change = ticker?.change; // "RISE" | "FALL" | "EVEN"

  const formattedPrice = formatPriceByTab(tradePrice, tab);

  const formattedRate =
    typeof changeRate === 'number' ? `${(changeRate * 100).toFixed(2)}` : '-';

  const textColorClass =
    change === 'RISE' ? 'text-danger' : change === 'FALL' ? 'text-primary' : '';

  // 가격 변화 감지
  useEffect(() => {
    if (!prev || !tradePrice || prev === tradePrice) return;

    if (tradePrice > prev) setHighlight('rise');
    else setHighlight('fall');

    // 200ms 동안 테두리 표시용
    if (timeoutRef.current) clearTimeout(timeoutRef.current);
    timeoutRef.current = setTimeout(() => {
      setHighlight(null);
    }, 200);
  }, [tradePrice, prev]);

  if (!product) return null;

  return (
    <tr>
      <th>{product.korean_name}</th>
      <td className={textColorClass}>
        <div
          className={`alignRight ${highlight === 'rise' ? 'highlight-red' : highlight === 'fall' ? 'highlight-blue' : ''}`}
        >
          {formattedPrice}
        </div>
      </td>
      <td className={`alignRight  ${textColorClass}`}>{formattedRate}</td>
    </tr>
  );
};

export default React.memo(ProductItem);

 

마켓정보에 따라 가격 포맷팅을 다르게 설정합니다.

/**
 *  탭별 가격 포맷팅
 * - KRW: 쉼표 (toLocaleString)
 * - BTC: 소수점 8자리
 * - USDT: 소수점 3자리
 */
import type { MarketTab } from '@/stores/upbitStore'

export const formatPriceByTab = (price: number, tab: MarketTab): string => {
    if (typeof price !== 'number') return '0.00'
  
    if (price > 1) {
      return price.toLocaleString('en')
    }
  
    let fixed = 2
    if (tab === 'BTC') fixed = 8
    else if (tab === 'USDT') fixed = 3
  
    return price.toFixed(fixed)
  }

 

이렇게 해서 코인목록과 실시간 시세를 보여줄 테이블영역의 구현이 완료되었습니다.

마지막으로 마켓을 선택할 수 있는 탭 영역과 찾고자 하는 코인들을 검색할 수 있는 검색영역을 구현해보겠습니다.

 

검색 및 탭기능 구현

TabSelector.tsx

사용자가 코인 마켓 탭 (KWR, BTC, USDT) 을 선택할 수 있도록 제공하는 컴포넌트입니다.

사용자가 특정 탭을 클릭하면 전역상태의 탭 정보를 업데이트하고, 이를 감지한 UpbitPage에서 종목코드조회 API를 호출하여 코인목록을 다시 받아옵니다.

import './TabSelector.css'
import React from 'react'
import { useUpbitStore , MarketTab } from '@/stores/upbitStore'

const tabs: MarketTab[] = ['KRW', 'BTC', 'USDT']

const TabSelector = () => {

  const tab = useUpbitStore((state) => state.tab)
  const setTab = useUpbitStore((state) => state.setTab)

  return (
    <div className="tab-container">
      <ul className="tab-list">
        {tabs.map((type) => (
          <li
            key={type}
            className={tab === type ? 'active' : ''}
            onClick={() => {  if (tab !== type) setTab(type)}}
          >
            {type}
          </li>
        ))}
      </ul>
    </div>
  )
}

export default React.memo(TabSelector)

 

SearchBar.tsx

코인을 검색할 수 있는 컴포넌트입니다.

사용자가 검색영역에 검색어를 입력하면 search, visible 상태가 업데이트되고, 업데이트된 상태를 기반으로 코인목록이 필터링되어 화면에 반영됩니다.

import './SearchBar.css';
import React from 'react';
import { useUpbitStore } from '@/stores/upbitStore';

const SearchBar = () => {
 
  const search = useUpbitStore((state) => state.search);
  const setSearch = useUpbitStore((state) => state.setSearch);

  return (
    <div className="search-upbit">
      <input
        type="text"
        placeholder="검색어 입력"
        value={search}
        onChange={(e) => setSearch(e.target.value)}
      />
    </div>
  );
};

export default React.memo(SearchBar);

 

여기까지 주요 기능 구현이 완료되었습니다.

구현한 기능을 바탕으로 새로운 기능을 시도해보고, 성능 개선이나 디자인 향상을 통해 프로젝트를 한 층 더 발전시켜보는것도 좋은 학습이 될 것입니다.

 

 

React 의 기능들을 더 깊이 이해하고 싶다면 바랍니다 . 제목 없음 https://ko.react.dev/ 도 함께 참고해보시기 바랍니다.