[실시간 통신] 주기 폴링
폴링부터 WebRTC까지, 실시간 통신 방식을 하나씩 정리하는 시리즈의 첫 번째 글입니다.
널리 알려진 방식이기 때문에 새로운 기술을 소개하기 보다는, 실제 프로젝트에서 어떤 문제를 해결하기 위해 선택했고 사용하면서 어떤 하계를 느꼈는지에 초점을 맞췄습니다.
실시간 데이터 받아오기
프로젝트를 진행할 때 새로운 요구사항이 하나 있었습니다. 팀원 하나가 회의나 문서를 만들 때 다른 사용자의 화면에도 해당 데이터가 자동으로 반영되어야 했습니다. 기존에는 페이지를 새로고침 하거나 컴포넌트가 리마운트 될 때 데이터를 가져왔습니다.
그래서 다른 사용자가 방금 회의를 만들어도 내 화면에는 바로 나타나지 않았습니다. 처음에는 문제를 단순 데이터 조회 문제라고 생각했습니다. 그러나 진짜 문제는 "서버 상태가 변경되었을 때 클라이언트가 이를 어떻게 인지 하느냐" 였습니다.
우선 실시간으로 데이터를 전달하는 방법부터 찾아 봤습니다. 대표적인 방법은 폴링부터 SSE, WebSocket 등 여러 가지 방법이 있었습니다.
저희는 현재 서비스에 필요한 수준을 먼저 생각했습니다. 저희 서비스에서는 밀리초 단위의 실시간성이 필요하지 않았습니다. 사용자가 화면을 보고 있는 동안 새로운 데이터가 어느 정도만 빠르게 반영되면 충분했습니다. 그래서 굳이 처음부터 복잡한 통신 방식을 도입하지 않기로 했습니다.
가장 먼저 사용한 것은 폴링이었습니다. 기존 REST API를 그대로 사용할 수 있고 구현도 가장 단순하기에 비용적으로 적합하다고 판단했습니다. 폴링을 충분히 사용해본 후 실제 한계를 직면하면 다음 방식을 검토하기로 했습니다.
폴링이란?
폴링(Polling)은 보통 주기 폴링(Short Polling)을 뜻합니다. 주기 폴링은 일정한 간격으로 서버에 새로운 데이터를 요청하는 방식입니다. 예를 들어 5초마다 데이터를 조회해 최신 데이터가 있는지 확인합니다.
useEffect(() => {
const intervalId = setInterval(() => {
refetch();
}, 5000);
return () => clearInterval(intervalId);
}, [refetch]);구조는 매우 단순합니다.
클라이언트가 일정한 간격으로 API를 호출하고 서버는 기존 REST API와 동일하게 응답합니다.
별도의 지속 연결을 유지하지 않아도 되고, 기존에 사용하던 HTTP API를 그대로 활용할 수 있다는 것도 장점입니다.
프로젝트 초반에 이런 간편함은 중요합니다. 새로운 프로토콜을 도입하거나 서버 구조를 바꾸지 않아도 기능을 빠르게 검증할 수 있기 때문입니다.
그리고 직접 적용해 보면서 또 하나의 장점을 알 수 있었습니다. 문제가 발생했을 때 확인하기 쉽습니다. 브라우저 개발자 도구의 Network탭을 열어 보면 어떤 요청이 언제 발생했는지 바로 확인할 수 있습니다. 응답 상태 코드와 응답 데이터도 그대로 확인할 수 있기 때문에 문제에 대한 추적이 쉽습니다.
실시간 통신이라고 해서 처음부터 복잡한 기술을 도입할 필요가 없다는 것을 알게 되었습니다.
압도적인 요청
문제는 폴링의 구조에 있었습니다.
5초마다 한 번씩 요청을 한다면 한 시간 동안 발생하는 요청 수는 다음과 같습니다.
3600초 / 5초 = 720회
사용자가 한 명일 땐 한 시간에 720번 요청합니다. 만약 6명이 같은 화면을 보고 있다면 요청은 4,320회가 됩니다.
물론 여기서 4,320번이라는 숫자 자체만으로 폴링이 나쁜 방식이라고 할 수는 없습니다. 더 중요한 문제는 그 요청 중 실제로 새로운 데이터를 가져오는 경우가 얼마나 되는 것입니다.
회의가 진행되는 동안은 데이터가 전혀 변경되지 않는 상황입니다. 이 때도 클라이언트는 5초마다 서버에 요청을 합니다. 서버는 요청을 처리하고 데이터를 조회한 후 응답합니다. 그런데 대부분의 경우 결과는 전과 동일할 것입니다.
즉, 데이터가 변경되지 않아도 반복적으로 요청합니다. 폴링의 문제는 여기서 시작됩니다.
ETag
여기서 우리는 한 가지 최적화 방법을 고려해 볼 수 있습니다. HTTP의 ETag와 조건부 요청을 사용하면 데이터가 변경되지 않은 경우 전체 본문을 다시 보내주지 않을 수 있습니다.
예를 들어 서버가 이전 응답의 ETag와 동일한 값을 확인하면 304 Not Modified를 반환할 수 있습니다. 이렇게 하면 불필요한 응답 데이터 전송량을 줄일 수 있습니다.
구분할 점이 있다면 응답 크기를 줄이는 것과 요청 자체를 줄이는 것은 다릅니다. 5초마다 폴링한다면 여전히 5초마다 서버에 요청을 보내야 합니다.
즉, ETag는 폴링의 네트워크 비용을 줄일 수는 있지만, 주기적으로 서버에 요청하는 구조를 개선할 수 없습니다. 이 문제는 폴링을 선택할 이유에 대해 더 깊은 고민을 하게 만들었습니다.
폴링으로 충분한가?
이런 단점들이 존재한다고 해서 폴링을 쓰면 안된다는 또 아닙니다. 데이터가 자주 변경되지 않거나 어느 정도의 지연이 허용되는 화면에서는 충분히 실용적입니다.
예를 들어 30초 주기로 폴링한다면 시간당 요청 수는 120회입니다. 5초 주기와 비교하면 요청 수를 1/6로 줄일 수 있습니다.
특히 기존 REST API를 그대로 활용할 수 있다는 점은 초기 개발에서 큰 장점입니다.
결국 중요한 것은 "폴링이 좋은가"가 아니라 "현재 요구사항에서 폴링으로 충분한가"입니다.
사용하지 않는 동안 요청 줄이기
폴링을 적용하고 나서 불필요하게 요청이 발생하는 상황도 조금 줄여볼 수 있었습니다.
사용자가 다른 탭으로 이동했거나 브라우저 창을 보지 않는 상태라면 계속 데이터를 갱신할 필요가 없는 화면도 있습니다.
React Query에서는 윈도우에 다시 포커스가 돌아왔을 때 혹은 네트워크가 재연결 됐을 때 데이터를 다시 가져오도록 설정할 수 있습니다.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
},
});이 방법은 사용자가 화면을 보고 있지 않은 동안 발생하는 일부 불필요한 요청을 줄일 수 있습니다.
다만 이 방법이 폴링 자체를 대체할 수는 없습니다. 실시간으로 변경 사항을 계속 받아야 한다면 여전히 폴링과 같은 실시간 통신 방식이 필요합니다.
폴링의 한계
실제로 사용해본 폴링은 생각보다 괜찮았습니다. 구현하기 쉽고, 디버깅도 편했습니다.
하지만 구조적으로 해결하기 어려운 문제가 하나 있습니다. 주기가 존재하는 한 데이터가 변경된 순간 바로 알 수 없습니다.
5초마다 요청한다면 최악의 경우 데이터가 변경된 직후 서버 상태가 변경되고, 다음 요청까지 최대 5초를 기다려야 합니다.
요청 주기를 1초로 줄이면 치대 지연도 1초로 줄어들지만 요청 수는 5배 늘어납니다.
지연 시간을 줄이려면 더 자주 요청을 해야 하고, 더 자주 요청하면 불필요한 요청이 늘어납니다.
이런 구조는 실시간 데이터가 중요한 채팅에서는 특히 문제가 됩니다. 메시지를 보낸 뒤 5초를 기다려야 한다면 사용자 입장에서는 실시간이라고 느끼기 어렵습니다.
다음 편에서는 주기 폴링의 한계를 보완하기 위해 등장한 롱 폴링(Long Polling)이 어떻게 동작하는지 정리해 보겠습니다.