[실시간 트레이딩 대시보드 만들기 - 03] 초당 20회 업데이트되는 호가창, 렌더링 횟수를 6,900배 줄인 방법

2026. 2. 12. 12:10·Frontend Development/[실습] 실시간 트레이딩 대시보드 만들기
728x90
반응형

업비트 실시간 대시보드 시리즈 마지막 편입니다. 호가창(Order Book)을 3가지 방식으로 구현하고 실제 성능을 비교해봤습니다.

전체 소스코드는 GitHub에 공개되어 있습니다. /benchmark 페이지에서 3가지 방식의 성능 차이를 직접 확인할 수 있습니다.

git clone https://github.com/kimkuns91/upbit-realtime-dashboard.git
cd upbit-realtime-dashboard
npm install && npm run dev
# http://localhost:3000/benchmark 접속


1. 왜 호가창이 어려운가?

호가창은 프론트엔드에서 구현 난이도가 가장 높은 컴포넌트 중 하나입니다. 이유는 단순합니다.

  • 업비트 WebSocket으로 초당 10~20회 호가 데이터가 들어옵니다
  • 매수 15호가 + 매도 15호가 = 30개 행이 동시에 바뀝니다
  • 잔량 바 너비, 가격, 수량이 매번 달라집니다
  • 이 모든 게 버벅임 없이 부드럽게 보여야 합니다

이걸 어떻게 처리하느냐에 따라 성능 차이가 극적으로 벌어집니다. 직접 3가지 방식을 구현해서 비교해봤습니다.

2. 방식 1 — Naive (useState)

가장 직관적인 방법입니다. WebSocket 메시지가 올 때마다 setState를 호출합니다.

const NaiveOrderBook = ({ market }: { market: string }) => {
  const [orderbook, setOrderbook] = useState<UpbitOrderbook | null>(null);

  useEffect(() => {
    const callbacks: SocketCallbacks = {
      onOrderbook: (data) => {
        if (data.code === market) {
          setOrderbook({ ...data }); // 매번 새 객체 → 매번 리렌더링
        }
      },
    };
    upbitSocket.addListener(callbacks);
    return () => upbitSocket.removeListener(callbacks);
  }, [market]);

  // 매 렌더마다 30개 행 전부 VDOM diffing...
  return (
    <>
      {units.map((u, i) => (
        <BenchmarkRow key={i} price={u.ask_price} size={u.ask_size} ... />
      ))}
    </>
  );
};

결과: 매 WS 메시지마다 setState가 호출되고, 매번 30개 행에 대한 VDOM diffing이 발생합니다. 실제로 벤치마크를 돌려보니 렌더링 횟수가 13,800회까지 올라갔습니다. React DevTools Profiler를 열면 렌더링이 쉬지 않고 찍힙니다.

3. 방식 2 — Throttle (lodash throttle + useState)

업데이트 빈도를 줄이면 되지 않을까 하는 아이디어입니다. lodash.throttle로 setState 호출을 100ms마다 제한합니다.

const ThrottledOrderBook = ({ market }: { market: string }) => {
  const [orderbook, setOrderbook] = useState<UpbitOrderbook | null>(null);

  const throttledSet = useMemo(
    () => throttle((data: UpbitOrderbook) => {
      setOrderbook({ ...data });
    }, 100),
    []
  );

  useEffect(() => {
    const callbacks: SocketCallbacks = {
      onOrderbook: (data) => {
        if (data.code === market) throttledSet(data);
      },
    };
    upbitSocket.addListener(callbacks);
    return () => {
      upbitSocket.removeListener(callbacks);
      throttledSet.cancel(); // 메모리 릭 방지
    };
  }, [market, throttledSet]);
  // ...
};

결과: throttle 덕분에 렌더링 횟수가 10,616회로 줄어듭니다. Naive보다 약 23% 줄었지만, 여전히 만 단위의 React 리렌더링이 발생합니다. VDOM diffing 비용 자체는 그대로입니다.

4. 방식 3 — Optimized (useRef + requestAnimationFrame)

근본적인 해결책은 React 렌더링 사이클을 우회하는 것입니다.

핵심 아이디어는 이렇습니다. 데이터를 useState가 아닌 useRef에 저장하고, requestAnimationFrame 주기에 맞춰 DOM을 직접 업데이트합니다.

먼저 이 패턴을 범용 훅으로 추상화했습니다.

export const useThrottledRef = <T>({ onUpdate }: { onUpdate: (data: T) => void }) => {
  const dataRef = useRef<T | null>(null);
  const hasNewDataRef = useRef(false);
  const onUpdateRef = useRef(onUpdate);
  onUpdateRef.current = onUpdate;

  useEffect(() => {
    const tick = () => {
      // 새 데이터가 있을 때만 콜백 호출
      if (hasNewDataRef.current && dataRef.current !== null) {
        onUpdateRef.current(dataRef.current);
        hasNewDataRef.current = false;
      }
      requestAnimationFrame(tick);
    };
    const id = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(id);
  }, []);

  // setState 대신 이걸 호출 → 리렌더링 없음
  const updateData = useCallback((newData: T) => {
    dataRef.current = newData;
    hasNewDataRef.current = true;
  }, []);

  return { updateData };
};

그리고 호가창에서 DOM을 직접 조작합니다.

const OptimizedOrderBook = ({ market }: { market: string }) => {
  const containerRef = useRef<HTMLDivElement>(null);

  const updateDOM = useCallback((data: UpbitOrderbook) => {
    const container = containerRef.current;
    if (!container) return;

    const askRows = container.querySelectorAll('[data-row="ask"]');
    askRows.forEach((row, i) => {
      const priceEl = row.querySelector('[data-field="price"]');
      const barEl = row.querySelector('[data-field="bar"]') as HTMLElement;
      if (priceEl) priceEl.textContent = formatPrice(units[i].ask_price);
      if (barEl) barEl.style.width = `${(units[i].ask_size / maxSize) * 100}%`;
    });
  }, []);

  const { updateData } = useThrottledRef<UpbitOrderbook>({
    onUpdate: updateDOM,
  });

  useEffect(() => {
    const callbacks: SocketCallbacks = {
      onOrderbook: (data) => {
        if (data.code === market) updateData(data);
      },
    };
    upbitSocket.addListener(callbacks);
    return () => upbitSocket.removeListener(callbacks);
  }, [market, updateData]);
  // ...
};

결과: React 리렌더링은 마운트 시 1~2회뿐입니다. 이후 모든 업데이트는 rAF 콜백에서 DOM을 직접 수정하므로 VDOM diffing 비용이 0입니다.

5. CSS 플래싱 효과

가격이 바뀌었을 때 배경색이 잠깐 번쩍이는 효과도 넣었습니다. JavaScript로 스타일을 제어하면 리플로우가 발생하지만, CSS class 토글 방식은 GPU 가속을 탈 수 있습니다.

@keyframes flash-bid {
  0% { background-color: rgba(239, 68, 68, 0.3); }
  100% { background-color: transparent; }
}

.flash-bid {
  animation: flash-bid 0.5s ease-out;
}
// 새로운 가격이 감지되면 CSS class 토글
row.classList.remove('flash-bid');
void row.offsetWidth; // reflow 트리거 (애니메이션 재시작)
row.classList.add('flash-bid');

6. 벤치마크 결과

프로젝트의 /benchmark 페이지에서 3가지 방식을 동시에 돌린 결과입니다.

방식 FPS 렌더링 횟수 비고
Naive (useState) 120 13,800회 매 WS 메시지마다 리렌더링
Throttle (100ms) 120 10,616회 줄었지만 여전히 만 단위
Optimized (useRef+rAF) 120 2회 마운트 시에만 렌더링

흥미로운 점은 FPS가 셋 다 120으로 동일하다는 것입니다. 최신 브라우저와 하드웨어가 좋아져서 FPS 자체는 잘 떨어지지 않습니다.

그렇다면 왜 최적화가 필요할까요? 렌더링 횟수를 보면 답이 나옵니다. Naive는 13,800회, Optimized는 단 2회. 6,900배 차이입니다. FPS가 동일하더라도 불필요한 리렌더링은 CPU 자원을 낭비하고, 다른 탭이나 인터랙션에 영향을 줍니다. 저사양 기기나 모바일에서는 이 차이가 FPS 드랍으로 직결됩니다.

Chrome DevTools Performance 탭에서 직접 확인해보시는 것을 추천합니다.

7. 언제 React 상태를 우회해야 하는가?

모든 곳에 useRef+rAF를 쓰라는 이야기는 아닙니다.

useState가 적합한 경우:

  • 사용자 인터랙션 기반 업데이트 (클릭, 입력)
  • 초당 1~2회 이하의 저빈도 업데이트
  • 여러 자식 컴포넌트에 데이터를 props로 내려야 할 때

useRef+rAF가 적합한 경우:

  • 초당 10회 이상의 고빈도 데이터 스트림
  • 업데이트할 DOM 노드가 많을 때 (호가창 30행 등)
  • 화면 표시만 하고 다른 로직에 영향을 주지 않는 데이터
  • 불필요한 리렌더링을 최소화해야 하는 경우

트레이드오프는 분명합니다. React의 선언적 모델을 포기하는 대신 성능을 얻습니다. DOM을 직접 조작하므로 코드가 복잡해지고, React DevTools에서 상태를 추적하기 어려워집니다. 하지만 호가창처럼 고빈도 + 다수 DOM 노드 조합에서는 이 패턴이 거의 유일한 해답입니다.

정리

핵심 교훈은 하나입니다. 고빈도 실시간 데이터에서는 React 렌더링 사이클이 병목이 됩니다. 이를 인지하고, 적절한 지점에서 React를 우회하는 판단이 프론트엔드 성능 최적화의 핵심입니다.

다음 글에서는 이 프로젝트를 만들면서 느낀 점과, 실제 프로덕션 트레이딩 플랫폼을 만든다면 어떤 점들을 더 신경써야 하는지 정리해봤습니다.

728x90
반응형
저작자표시 비영리 변경금지 (새창열림)

'Frontend Development > [실습] 실시간 트레이딩 대시보드 만들기' 카테고리의 다른 글

[실시간 트레이딩 대시보드 만들기 - 04] 회고 — 연습용 프로젝트와 실전의 거리  (0) 2026.02.12
[실시간 트레이딩 대시보드 만들기 - 02] TradingView Charts로 실시간 캔들 차트 구현  (0) 2026.02.12
[실시간 트레이딩 대시보드 만들기 - 01] WebSocket 파이프라인 설계와 Blob 파싱  (0) 2026.02.12
'Frontend Development/[실습] 실시간 트레이딩 대시보드 만들기' 카테고리의 다른 글
  • [실시간 트레이딩 대시보드 만들기 - 04] 회고 — 연습용 프로젝트와 실전의 거리
  • [실시간 트레이딩 대시보드 만들기 - 02] TradingView Charts로 실시간 캔들 차트 구현
  • [실시간 트레이딩 대시보드 만들기 - 01] WebSocket 파이프라인 설계와 Blob 파싱
Kun Woo Kim
Kun Woo Kim
안녕하세요, 김건우입니다! 웹과 앱 개발에 열정적인 전문가로, React, TypeScript, Next.js, Node.js, Express, Flutter 등을 활용한 프로젝트를 다룹니다. 제 블로그에서는 개발 여정, 기술 분석, 실용적 코딩 팁을 공유합니다. 창의적인 솔루션을 실제로 적용하는 과정의 통찰도 나눌 예정이니, 궁금한 점이나 상담은 언제든 환영합니다.
  • Kun Woo Kim
    WhiteMouseDev
    김건우
  • 깃허브
    포트폴리오
    velog
  • 전체
    오늘
    어제
  • 공지사항

    • [인사말] 이제 티스토리에서도 만나요! WhiteMouse⋯
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 분류 전체보기 (151) N
      • Frontend Development (68) N
        • [실습] 실시간 트레이딩 대시보드 만들기 (4) N
      • Backend Development (28)
      • AI · ML (4)
        • Computer Vision (4)
      • Algorithm (35)
        • 백준 (11)
        • 프로그래머스 (18)
        • 알고리즘 (5)
      • Infra (1)
      • 자료구조 (4)
      • Language (6)
        • JavaScript (6)
      • 자격증 공부 (1)
        • GCP Developer (1)
      • Tools (1) N
  • 링크

    • Github
    • Portfolio
    • Velog
  • 인기 글

  • 태그

    Human-in-the-Loop
    MSA
    API Gateway
    claudecode
    Vision-Language-Model
    개발자도구
    Qwen2.5-VL
    mlops
    YOLO
    Infra
    딥러닝
    AI
    backend
    src layout
    SRC
    Nextjs
    flat layout
    Object Detection
    객체탐지
    AgentTeams
    frontend development
    모델비교
    transformer
    컴퓨터비전
    multiagent
    tailwindcss
    rt-detr
    CNN
    Nginx
    AI개발
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Kun Woo Kim
[실시간 트레이딩 대시보드 만들기 - 03] 초당 20회 업데이트되는 호가창, 렌더링 횟수를 6,900배 줄인 방법
상단으로

티스토리툴바