junyeokk
Blog
React Ecosystem·2025. 12. 04

React Query refetchInterval

서버 데이터가 바뀌었는지 클라이언트가 알 수 있는 방법은 크게 두 가지다. 서버가 클라이언트에게 알려주거나(push), 클라이언트가 주기적으로 물어보거나(poll). WebSocket이나 SSE 같은 push 방식이 실시간성에서는 우월하지만, 모든 데이터에 실시간 채널을 붙이는 건 과하다. 알림 목록처럼 "몇 초 정도 늦어도 괜찮은" 데이터는 폴링이 더 현실적인 선택이다.

React Query의 refetchInterval은 이 폴링을 선언적으로 구현하게 해준다. setInterval + fetch + 상태 관리를 직접 엮는 대신, 쿼리 옵션 하나로 주기적 재요청을 켤 수 있다.


기본 사용법

useQuery 옵션에 refetchInterval을 밀리초 단위로 지정하면 된다.

tsx
const { data } = useQuery({
  queryKey: ['notifications', workspaceId],
  queryFn: () => fetchNotifications(workspaceId),
  refetchInterval: 5000, // 5초마다 재요청
});

이게 전부다. 컴포넌트가 마운트되어 있는 동안 5초 간격으로 queryFn이 다시 호출되고, 새 데이터가 오면 컴포넌트가 리렌더링된다. 언마운트되면 폴링도 자동으로 멈춘다.

내부적으로 React Query는 setInterval을 사용하되, 쿼리의 라이프사이클에 맞춰 자동으로 정리해준다. 이전 요청이 아직 진행 중이면 중복 요청을 보내지 않고, staleTime이나 gcTime 같은 다른 캐시 설정과도 자연스럽게 통합된다.


setInterval + fetch와의 차이

직접 폴링을 구현하면 이런 코드가 된다:

tsx
function Notifications() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const res = await fetch('/api/notifications');
        setData(await res.json());
      } catch (e) {
        setError(e);
      }
    };

    fetchData(); // 초기 로드
    const id = setInterval(fetchData, 5000);
    return () => clearInterval(id);
  }, []);

  // loading, error, data 처리...
}

문제점이 여러 개 있다:

  1. 중복 요청 방지가 없다. 이전 fetch가 5초 넘게 걸리면 다음 interval이 겹친다.
  2. 캐시가 없다. 같은 데이터를 여러 컴포넌트에서 보여줄 때 각각 독립적으로 폴링한다.
  3. 에러 복구가 수동이다. 네트워크 에러 후 재시도 로직을 직접 짜야 한다.
  4. 포커스/네트워크 상태를 모른다. 탭이 백그라운드에 있어도 계속 요청한다.

React Query는 이 모든 걸 처리해준다. 같은 queryKey를 쓰는 컴포넌트가 여러 개 있어도 요청은 하나만 나가고, 결과는 캐시를 통해 공유된다. 에러가 나면 설정된 retry 정책에 따라 자동 재시도한다.


옵션 상세

밀리초 숫자

가장 기본적인 형태. 고정 간격으로 폴링한다.

tsx
refetchInterval: 5000  // 5초
refetchInterval: 30000 // 30초
refetchInterval: 1000  // 1초 (주의: 서버 부하)

false

폴링을 끈다. 기본값이 false이므로 명시적으로 쓸 일은 드물지만, 조건부로 폴링을 토글할 때 사용한다.

tsx
refetchInterval: isPollingEnabled ? 5000 : false

함수

쿼리 상태에 따라 간격을 동적으로 결정할 수 있다. 함수는 최신 query 객체를 인자로 받는다.

tsx
refetchInterval: (query) => {
  // 에러 상태면 폴링 중지
  if (query.state.error) return false;

  // 데이터가 특정 조건을 만족하면 폴링 중지
  if (query.state.data?.isComplete) return false;

  // 정상이면 5초 간격
  return 5000;
}

이 패턴이 유용한 경우:

  • 비동기 작업 완료 대기: 파일 변환이나 배포 같은 긴 작업의 상태를 폴링하다가, 완료되면 멈추기
  • 점진적 백오프: 연속 에러 시 간격을 늘리기
  • 조건부 중지: 특정 데이터 상태에서 더 이상 폴링이 필요 없을 때
tsx
// 점진적 백오프 예시
refetchInterval: (query) => {
  const errorCount = query.state.errorUpdateCount;
  if (errorCount === 0) return 3000;
  if (errorCount < 3) return 5000;
  if (errorCount < 5) return 15000;
  return false; // 5번 이상 실패하면 중지
}

refetchIntervalInBackground

기본적으로 refetchInterval은 브라우저 탭이 포커스를 잃으면 폴링을 일시 중지한다. 사용자가 다른 탭을 보고 있는데 계속 요청을 보내는 건 자원 낭비이기 때문이다.

하지만 채팅 앱이나 모니터링 대시보드처럼 백그라운드에서도 데이터를 최신으로 유지해야 하는 경우가 있다. 이때 refetchIntervalInBackgroundtrue로 설정한다.

tsx
useQuery({
  queryKey: ['chat-messages', roomId],
  queryFn: () => fetchMessages(roomId),
  refetchInterval: 3000,
  refetchIntervalInBackground: true, // 탭이 비활성이어도 폴링 계속
});

이 옵션을 켜면 탭이 백그라운드에 있어도 지정된 간격으로 계속 요청을 보낸다. 서버 부하에 영향을 줄 수 있으므로 꼭 필요한 경우에만 사용하는 게 좋다.


다른 refetch 옵션과의 관계

React Query에는 데이터를 다시 가져오는 트리거가 여러 개 있다. refetchInterval은 이것들과 독립적으로 동작하면서, 동시에 켜져 있으면 합산되는 방식이다.

옵션트리거 시점기본값
refetchInterval고정 시간 간격false
refetchOnWindowFocus탭 포커스 복귀true
refetchOnReconnect네트워크 재연결true
refetchOnMount컴포넌트 마운트true

예를 들어 refetchInterval: 30000refetchOnWindowFocus: true가 둘 다 켜져 있으면:

  • 30초마다 자동으로 재요청
  • 사용자가 다른 탭 갔다가 돌아올 때도 재요청
  • 두 트리거가 겹치면 React Query가 알아서 중복을 제거

staleTime과의 관계도 중요하다. staleTime: 60000으로 설정하면 데이터가 60초 동안 fresh 상태로 유지되는데, refetchIntervalstaleTime과 무관하게 지정된 간격으로 재요청한다. 즉 staleTime: 60000 + refetchInterval: 5000이면, 데이터가 fresh 상태여도 5초마다 재요청이 나간다.


폴링 vs WebSocket: 언제 뭘 쓸까

둘 다 서버 데이터를 클라이언트에 반영하는 방법이지만, 특성이 다르다.

폴링이 적합한 경우:

  • 데이터 변경 빈도가 낮고, 몇 초 지연이 허용되는 경우
  • 서버에 WebSocket 인프라가 없는 경우
  • 구현 복잡도를 낮추고 싶은 경우
  • 사용 예: 알림 개수, 주문 상태, 배포 진행률

WebSocket이 적합한 경우:

  • 밀리초 단위 실시간성이 필요한 경우
  • 서버 → 클라이언트 단방향 푸시가 빈번한 경우
  • 사용 예: 채팅, 공동 편집, 주식 시세

하이브리드도 가능하다. 핵심 기능은 WebSocket으로 실시간 처리하고, 부가 기능은 폴링으로 처리하는 방식이다. YouViCo처럼 드로잉/피드백은 Socket.IO로 실시간 동기화하면서, 알림 목록은 5초 폴링으로 처리하는 게 좋은 예다.


주의할 점

폴링 간격은 신중하게

1초 간격 폴링은 사실상 초당 1회 API 호출이다. 동시 접속자가 100명이면 초당 100회, 1000명이면 초당 1000회. 대부분의 경우 5~30초면 충분하다.

tsx
// ❌ 과도한 폴링
refetchInterval: 1000

// ✅ 합리적인 폴링
refetchInterval: 5000  // 알림 (약간의 지연 허용)
refetchInterval: 30000 // 대시보드 통계 (분 단위도 OK)

불필요한 리렌더링 방지

데이터가 바뀌지 않았어도 refetchInterval에 의해 쿼리가 재실행되면 참조가 바뀌어 리렌더링이 발생할 수 있다. structuralSharing 옵션(기본 true)이 이를 방지해주긴 하지만, select로 데이터를 변환하는 경우 매번 새 객체가 생길 수 있다.

tsx
// ❌ 매번 새 배열 생성 → 매 폴링마다 리렌더링
const { data } = useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  refetchInterval: 5000,
  select: (data) => data.filter(n => !n.read), // 매번 새 배열
});

// ✅ useMemo나 안정적인 select 사용
const { data } = useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  refetchInterval: 5000,
});
const unread = useMemo(() => data?.filter(n => !n.read), [data]);

컴포넌트 언마운트 시 자동 정리

refetchInterval은 해당 쿼리를 구독하는 컴포넌트가 모두 언마운트되면 자동으로 멈춘다. clearInterval을 직접 호출할 필요가 없다. 이게 setInterval을 직접 쓰는 것보다 React Query 폴링이 안전한 이유 중 하나다.


정리

  • refetchInterval은 선언적 폴링이다. setInterval + 상태 관리를 직접 엮는 대신 쿼리 옵션 하나로 주기적 재요청, 중복 방지, 캐시 공유를 모두 얻는다.
  • 함수 형태로 넘기면 쿼리 상태에 따라 간격을 동적으로 조절하거나 폴링을 중지할 수 있다. 비동기 작업 완료 대기, 점진적 백오프 등에 유용하다.
  • 폴링과 WebSocket은 양자택일이 아니다. 실시간성이 중요한 데이터는 push, 지연이 허용되는 데이터는 poll로 하이브리드 구성하는 게 현실적이다.

관련 문서