튜토리얼 중급

TanStack Query 5 거래 데이터 획득 최선의 사례|React 서버 상태 관리 완벽 가이드

Sentinel Team · 2026-03-04
TanStack Query 5 거래 데이터 획득 최선의 사례|React 서버 상태 관리 완벽 가이드

TanStack Query 5 거래 데이터 획득 최선의 사례

빠른 탐색: 이 글은 TanStack Query 5가 거래 시스템에 완벽하게 적용되는 방식을 심층적으로 다루며, 기본 쿼리부터 고급 캐싱 전략까지 암호화폐 거래 데이터 획득의 권위 있는 가이드를 제공합니다. 예상 읽기 시간 14분.


왜 거래 시스템에는 TanStack Query가 필요할까요?

자동화 거래 시스템에서 데이터 획득은 가장 복잡한 도전 중 하나입니다. 다음을 동시에 처리해야 합니다:

전통적인 useEffect + fetch 패턴은 이러한 요구사항 앞에서 빠르게 무너집니다. TanStack 공식 문서에 따르면, React Query는 "서버 상태의 비동기 캐시 관리자"로 설계되었습니다 —— 바로 거래 시스템이 필요로 하는 것입니다.

거래 시스템의 데이터 도전 과제

| 데이터 유형 | 업데이트 빈도 | 캐싱 전략 | 도전 과제 |

|:---|:---:|:---|:---|

| 실시간 시세 | 1-10회/초 | 짧은 캐싱 (5초) | 고빈도 업데이트 성능 |

| 히스토리 K선 | 분당 | 장기 캐싱 (1시간) | 데이터 크기 |

| 사용자 포지션 | 실시간 | 즉시 무효화 | 일관성 요구 |

| 거래 내역 | 추가 시 | 페이지네이션 캐싱 | 페이지네이션 동기화 |

| 전략 리스트 | 매시간 | 백그라운드 업데이트 | 오래된 데이터 위험 |


TanStack Query 5 핵심 개념

서버 상태 vs 클라이언트 상태

┌─────────────────────────────────────────────────────────┐
│                    애플리케이션 상태                      │
├─────────────────────────┬───────────────────────────────┤
│      클라이언트 상태     │         서버 상태              │
│  (Zustand/Redux)        │      (TanStack Query)         │
├─────────────────────────┼───────────────────────────────┤
│ • UI 토글 상태          │ • API 응답 데이터             │
│ • 폼 입력값             │ • 캐시의 수명 주기 관리        │
│ • 애니메이션 상태       │ • 백그라운드 업데이트 및 재검증 │
│ • 테마 설정             │ • 오류 재시도 및 요청 취소     │
│                         │ • 페이지네이션과 무한 스크롤    │
└─────────────────────────┴───────────────────────────────┘

핵심 통찰: 거래 시스템은 실시간 가격을 제외한 모든 서버 데이터를 TanStack Query에 관리하고, UI 상태는 Zustand로 처리해야 합니다.

Query Key의 디자인 철학

Query Key는 TanStack Query의 핵심으로, 데이터 식별과 캐싱을 결정합니다:

// ❌ 오류: 너무 단순
const { data } = useQuery({
  queryKey: ['bots'],
  queryFn: fetchBots,
});

// ✅ 올바름: 모든 의존 파라미터 포함
const { data } = useQuery({
  queryKey: ['bots', { status: 'active', page: 1, limit: 20 }],
  queryFn: () => fetchBots({ status: 'active', page: 1, limit: 20 }),
});

거래 시스템의 Query Key 디자인 원칙:

// 실시간 가격 - 거래 쌍과 시간 프레임 포함
['prices', 'BTC/USDT', '1m']  // 1분 K선

// 봇 리스트 - 필터 조건 포함
['bots', { status: 'running', sort: 'pnl_desc' }]

// 거래 내역 - 페이지네이션과 시간 범위 포함
['trades', { botId: '123', page: 1, from: '2024-01-01', to: '2024-01-31' }]

// 사용자 데이터 - 사용자 ID 포함 (사용자 전환 시 캐시 무효화)
['user', userId, 'profile']

거래 시스템 실전: Hooks 디자인 패턴

기본 쿼리: useBots

// hooks/useBots.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/api/client';

// Query Keys 중앙 관리
export const botKeys = {
  all: ['bots'] as const,
  lists: () => [...botKeys.all, 'list'] as const,
  list: (filters: BotFilters) => [...botKeys.lists(), filters] as const,
  details: () => [...botKeys.all, 'detail'] as const,
  detail: (id: string) => [...botKeys.details(), id] as const,
};

// 봇 리스트 가져오기
export function useBots(filters: BotFilters = {}) {
  return useQuery({
    queryKey: botKeys.list(filters),
    queryFn: () => api.bots.list(filters),
    staleTime: 1000 * 30, // 30초 내에는 신선하다고 간주
    gcTime: 1000 * 60 * 5, // 5분 가비지 컬렉션
  });
}

// 단일 봇 가져오기
export function useBot(id: string) {
  return useQuery({
    queryKey: botKeys.detail(id),
    queryFn: () => api.bots.get(id),
    enabled: !!id, // ID가 있을 때만 쿼리
    staleTime: 1000 * 10, // 10초 신선 시간
  });
}

데이터 수정: useCreateBot

// hooks/useCreateBot.ts
export function useCreateBot() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: api.bots.create,
    
    // 낙관적 업데이트
    onMutate: async (newBot) => {
      // 진행 중인 재획득 취소
      await queryClient.cancelQueries({ queryKey: botKeys.lists() });
      
      // 이전 값 저장
      const previousBots = queryClient.getQueryData(botKeys.lists());
      
      // 캐시 낙관적 업데이트
      queryClient.setQueryData(botKeys.lists(), (old) => 
        old ? [...old, { ...newBot, id: 'temp-id', status: 'creating' }] : old
      );
      
      return { previousBots };
    },
    
    // 오류 시 롤백
    onError: (err, newBot, context) => {
      queryClient.setQueryData(botKeys.lists(), context?.previousBots);
    },
    
    // 완료 시 재획득
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: botKeys.lists() });
    },
  });
}

더 많은 상태 관리를 알고 싶으시면 Zustand vs Redux 가이드를 참조하세요.


실시간 데이터: 구독과 폴링 전략

WebSocket 통합 패턴

// hooks/usePriceSubscription.ts
import { useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { wsManager } from '@/api/websocket';

export function usePriceSubscription(symbol: string) {
  const queryClient = useQueryClient();
  
  // 초기 데이터 쿼리
  const { data: price } = useQuery({
    queryKey: ['prices', symbol],
    queryFn: () => api.prices.getLatest(symbol),
    staleTime: Infinity, // WebSocket이 업데이트를 담당
  });
  
  useEffect(() => {
    // WebSocket 구독
    const unsubscribe = wsManager.subscribe(
      `price:${symbol}`,
      (newPrice) => {
        // 캐시 직접 업데이트
        queryClient.setQueryData(
          ['prices', symbol],
          newPrice
        );
      }
    );
    
    return () => unsubscribe();
  }, [symbol, queryClient]);
  
  return price;
}

폴링 전략: useIntervalQuery

// hooks/useIntervalQuery.ts
export function useIntervalQuery(
  queryKey: string[],
  queryFn: () => Promise<T>,
  interval: number = 5000
) {
  return useQuery({
    queryKey,
    queryFn,
    refetchInterval: interval,
    refetchIntervalInBackground: false, // 백그라운드에서는 폴링하지 않음
    refetchOnWindowFocus: true, // 창으로 돌아올 때 재획득
  });
}

// 사용: 5초마다 포지션 업데이트
function usePositions() {
  return useIntervalQuery(
    ['positions'],
    api.positions.getAll,
    5000
  );
}

캐싱 전략: 거래 장면 최선의 사례

staleTime과 gcTime의 선택

| 데이터 유형 | staleTime | gcTime | 이유 |

|:---|:---:|:---:|:---|

| 실시간 가격 | Infinity | 5초 | WebSocket 업데이트, 짧은 보존 |

| 사용자 데이터 | 5분 | 30분 | 상대적으로 안정적 |

| 봇 리스트 | 30초 | 5분 | 중간 빈도 업데이트 |

| 히스토리 K선 | 1시간 | 24시간 | 데이터 크기 큼, 장기 캐싱 |

| 거래 내역 | 0 | 5분 | 빈번한 추가, 즉시 재획득 |

페이지네이션과 무한 스크롤

// hooks/useTradeHistory.ts
export function useTradeHistory(botId: string) {
  return useInfiniteQuery({
    queryKey: ['trades', botId],
    queryFn: ({ pageParam = 1 }) => 
      api.trades.list({ botId, page: pageParam, limit: 50 }),
    getNextPageParam: (lastPage, pages) => {
      return lastPage.hasMore ? pages.length + 1 : undefined;
    },
    staleTime: 1000 * 60, // 1분
  });
}

// 컴포넌트에서 사용
function TradeHistory({ botId }: { botId: string }) {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = 
    useTradeHistory(botId);
  
  const trades = data?.pages.flatMap((page) => page.trades) ?? [];
  
  return (
    <div>
      {trades.map((trade) => (
        <TradeRow key={trade.id} trade={trade} />
      ))}
      
      {hasNextPage && (
        <button 
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? '로딩 중...' : '더 보기'}
        </button>
      )}
    </div>
  );
}

오류 처리와 재시도 전략

거래 시스템의 오류 분류

// 사용자 정의 재시도 로직
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: (failureCount, error: any) => {
        // 재시도하지 않는 오류
        if (error?.response?.status === 401) return false;
        if (error?.response?.status === 403) return false;
        if (error?.response?.status === 404) return false;
        
        // 최대 3회 재시도
        return failureCount < 3;
      },
      retryDelay: (attemptIndex) => {
        // 지수 백오프
        return Math.min(1000 * 2 ** attemptIndex, 30000);
      },
    },
  },
});

오류 경계와 다운그레이드

// hooks/useBotWithFallback.ts
export function useBotWithFallback(id: string) {
  const { data, error, isError } = useBot(id);
  
  // 오류 시 로컬 캐시 또는 기본값 표시
  const fallbackBot = useMemo(() => ({
    id,
    name: '로딩 중...',
    status: 'unknown',
  }), [id]);
  
  return {
    bot: isError ? fallbackBot : data,
    error,
    isError,
  };
}

성능 최적화: 거래 장면의 특수 고려사항

셀렉터 최적화

// ❌ 오류: 매번 새 객체 반환
function useActiveBots() {
  const { data } = useBots();
  return data?.filter((bot) => bot.status === 'active');
}

// ✅ 올바름: 셀렉터 캐싱 사용
import { useMemo } from 'react';

function useActiveBots() {
  const { data } = useBots();
  
  return useMemo(() => 
    data?.filter((bot) => bot.status === 'active'),
    [data]
  );
}

대량 데이터의 가상화

// react-window와 TanStack Query 결합
import { FixedSizeList } from 'react-window';

function LargeTradeList({ botId }: { botId: string }) {
  const { data } = useTradeHistory(botId);
  const trades = data?.pages.flatMap((p) => p.trades) ?? [];
  
  const Row = ({ index, style }: { index: number; style: any }) => (
    <div style={style}>
      <TradeRow trade={trades[index]} />
    </div>
  );
  
  return (
    <FixedSizeList
      height={600}
      itemCount={trades.length}
      itemSize={60}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

성능 최적화를 더 알고 싶으시면 Vite 5 + PWA 거래 애플리케이션 성능 최적화를 참조하세요.


실전 사례: Sentinel Bot의 데이터 아키텍처

Query Client 구성

// providers/query-provider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 거래 시스템 기본 구성
      staleTime: 1000 * 30, // 30초
      gcTime: 1000 * 60 * 5, // 5분
      retry: 3,
      retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
    mutations: {
      retry: 1, // 수정 작업은 1회만 재시도
    },
  },
});

사용자 정의 Hooks 라이브러리 구조

src/hooks/
├── queries/           # 데이터 쿼리 Hooks
│   ├── useBots.ts
│   ├── useStrategies.ts
│   ├── usePrices.ts
│   └── useTrades.ts
├── mutations/         # 데이터 수정 Hooks
│   ├── useCreateBot.ts
│   ├── useUpdateBot.ts
│   └── useDeleteBot.ts
├── subscriptions/     # 실시간 구독 Hooks
│   └── usePriceSubscription.ts
└── composite/         # 복합 Hooks
    └── useBotWithStats.ts

자주 묻는 질문 FAQ

Q1: TanStack Query 5와 SWR의 차이는 무엇인가요?

A: 둘 다 우수한 데이터 획득 라이브러리입니다:

거래 시스템은 TanStack Query를 권장하는데, 페이지네이션, 무한 스크롤, 오프라인 지원이 더 성숙하기 때문입니다.

Q2: 고빈도 업데이트되는 실시간 데이터는 어떻게 처리하나요?

A: 권장 조합 전략:

  1. WebSocket: 실시간 푸시 업데이트
  2. TanStack Query: 초기 데이터와 상태 관리
  3. Zustand: 초고빈도 데이터 (예: 틱별 거래)

WebSocket 실시간 거래 모니터링 개발 가이드를 참조하세요.

Q3: staleTime과 cacheTime의 차이는 무엇인가요? (v5에서는 gcTime)

A:

거래 장면 권장: 실시간 데이터는 staleTime: Infinity 설정하고 WebSocket이 업데이트를 제어하도록 합니다.

Q4: TanStack Query Hooks는 어떻게 테스트하나요?

A: renderHookQueryClientProvider 사용:

import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const createWrapper = () => {
  const queryClient = new QueryClient();
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

it('should fetch bots', async () => {
  const { result } = renderHook(() => useBots(), {
    wrapper: createWrapper(),
  });
  
  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data).toHaveLength(3);
});

Q5: 여러 컴포넌트가 동일한 Query Key를 구독하면 중복 요청이 발생하나요?

A: 아니요. TanStack Query는 자동으로 중복을 제거하고, 동일한 Query Key에는 하나의 요청만 전송하며 모든 구독자가 결과를 공유합니다.

Q6: 데이터를 강제로 재획득하려면 어떻게 하나요?

A: 다양한 방법:

const queryClient = useQueryClient();

// 방법 1: 오래된 것으로 표시 (다음 사용 시 재획득)
queryClient.invalidateQueries({ queryKey: ['bots'] });

// 방법 2: 즉시 재획득
queryClient.refetchQueries({ queryKey: ['bots'] });

// 방법 3: Hook에서 사용
const { refetch } = useBots();
refetch();

Q7: 오프라인 지원은 어떻게 구현하나요?

A: TanStack Query v5는 내장 오프라인 지원:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: 'offlineFirst', // 오프라인 시 캐시 사용
    },
    mutations: {
      networkMode: 'offlineFirst',
      retry: Infinity, // 오프라인 시 지속적으로 재시도
    },
  },
});

Q8: Zustand와 어떻게 분업하나요?

A: 황금 법칙:

// 조합 사용 예시
function TradingDashboard() {
  // 서버 상태
  const { data: bots } = useBots();
  
  // 클라이언트 상태
  const selectedBotId = useUIStore((s) => s.selectedBotId);
  const setSelectedBot = useUIStore((s) => s.setSelectedBot);
  
  return (
    <div>
      {bots?.map((bot) => (
        <BotCard
          key={bot.id}
          bot={bot}
          isSelected={bot.id === selectedBotId}
          onClick={() => setSelectedBot(bot.id)}
        />
      ))}
    </div>
  );
}

결론 및 최선의 사례 요약

TanStack Query 5는 거래 시스템 데이터 획득의 최선의 선택으로, 다음을 해결합니다:

즉시 실행 체크리스트


추가 읽기:


작성자: Sentinel Team

마지막 업데이트: 2026-03-04

기술 검증: Sentinel Bot 프로덕션 환경 실전 경험 기반


거래 시스템의 데이터 아키텍처를 최적화하고 있나요? Sentinel Bot의 TanStack Query 기반 인터페이스를 지금 경험하거나, Hooks 템플릿을 다운로드하여 빠르게 시작하세요.

Sentinel Bot 무료 체험 | Hooks 템플릿 다운로드 | 기술 컨설팅


관련 문서

동일 시리즈 추가 읽기

교차 시리즈 추천