실시간성이 필요하지만 WebSocket까지는 필요 없는 기능들이 있다. 주문 상태 확인, 결제 처리 상태, 파일 업로드 진행률 같은 것들. 이럴 때 폴링(Polling)을 쓴다.
서버에 주기적으로 "야, 상태 바뀐 거 있어?" 하고 물어보는 것. 간단하지만 제대로 안 하면 성능 문제가 생긴다.
import { useEffect, useState } from 'react';
function usePolling<T>(
fetchFn: () => Promise<T>,
interval: number
) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const poll = async () => {
try {
const result = await fetchFn();
setData(result);
} catch (err) {
setError(err as Error);
}
};
poll(); // 첫 호출
const intervalId = setInterval(poll, interval);
return () => clearInterval(intervalId);
}, [fetchFn, interval]);
return { data, error };
}
// 사용
function OrderStatus({ orderId }: { orderId: string }) {
const { data: order } = usePolling(
() => fetch(`/api/orders/${orderId}`).then(res => res.json()),
3000 // 3초마다
);
return <div>주문 상태: {order?.status}</div>;
}
문제점
function usePolling<T>(
fetchFn: () => Promise<T>,
interval: number
) {
const [data, setData] = useState<T | null>(null);
const [isPolling, setIsPolling] = useState(false);
useEffect(() => {
let isMounted = true;
let timeoutId: NodeJS.Timeout;
const poll = async () => {
if (!isMounted) return;
try {
const result = await fetchFn();
if (isMounted) {
setData(result);
}
} catch (err) {
console.error(err);
} finally {
// 이전 요청이 끝난 후에 다음 타이머 설정
if (isMounted) {
timeoutId = setTimeout(poll, interval);
}
}
};
poll();
return () => {
isMounted = false;
clearTimeout(timeoutId);
};
}, [fetchFn, interval]);
return { data };
}
setInterval 대신 setTimeout을 재귀적으로 써서 이전 요청 완료 후 다음 요청이 나가도록 함
**2. 탭이 백그라운드일 때도 폴링함 (**브라우저 탭이 보이지 않을 때는 폴링을 멈춰야 한다.)
function usePolling<T>(
fetchFn: () => Promise<T>,
interval: number,
options: { pauseWhenHidden?: boolean } = {}
) {
const [data, setData] = useState<T | null>(null);
const [isVisible, setIsVisible] = useState(!document.hidden);
useEffect(() => {
if (!options.pauseWhenHidden) return;
const handleVisibilityChange = () => {
setIsVisible(!document.hidden);
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [options.pauseWhenHidden]);
useEffect(() => {
if (options.pauseWhenHidden && !isVisible) return;
let isMounted = true;
let timeoutId: NodeJS.Timeout;
const poll = async () => {
if (!isMounted) return;
try {
const result = await fetchFn();
if (isMounted) setData(result);
} catch (err) {
console.error(err);
} finally {
if (isMounted) {
timeoutId = setTimeout(poll, interval);
}
}
};
poll();
return () => {
isMounted = false;
clearTimeout(timeoutId);
};
}, [fetchFn, interval, isVisible, options.pauseWhenHidden]);
return { data };
}