JavaScript 폴링(Polling) - 실시간 데이터 업데이트의 시작점
웹 애플리케이션을 개발하다 보면 이런 요구사항을 접하게 됩니다.
“채팅 메시지를 실시간으로 보여주세요.” “주식 가격이 변하면 즉시 화면에 업데이트되어야 합니다.” “알림이 오면 바로 표시해주세요.”
그런데 문제가 있습니다. HTTP는 클라이언트가 요청해야만 서버가 응답하는 구조입니다. 서버가 먼저 “야, 새 메시지 왔어!”라고 알려줄 수 없죠.
저도 처음 실시간 기능을 구현해야 했을 때 막막했습니다. “서버가 먼저 연락할 수 없다면, 어떻게 실시간으로 데이터를 받을 수 있지?” 그때 찾은 해결책이 바로 폴링(Polling)이었습니다.
폴링은 마치 친구에게 “지금 도착했어?”라고 계속 물어보는 것과 같습니다. 가장 단순하지만, 실시간 기능을 구현하는 첫 번째 방법이죠.
이 문서에서는 폴링이 무엇인지, 어떻게 구현하는지, 그리고 언제 사용해야 하는지 처음부터 끝까지 알아보겠습니다.
목차
- 왜 폴링을 이해해야 할까요?
- 먼저, HTTP의 한계를 이해하기
- 폴링이란 무엇인가?
- Short Polling (짧은 폴링)
- Long Polling (긴 폴링)
- 실전 예제로 배우는 폴링
- 함정과 주의사항
- 폴링 최적화하기
- 폴링의 대안들
- 결론: 폴링을 언제 어떻게 사용할까?
- 참고 자료
왜 폴링을 이해해야 할까요?
1. 실시간 기능을 구현하는 가장 간단한 방법입니다
// ❌ 이렇게는 안 됩니다 - 서버가 먼저 알려줄 수 없습니다
server.notifyClient('새 메시지가 있습니다');
// ✅ 폴링: 클라이언트가 계속 물어봅니다
setInterval(() => {
fetch('/api/messages/new')
.then(response => response.json())
.then(data => {
if (data.hasNew) {
showNotification('새 메시지가 있습니다');
}
});
}, 5000); // 5초마다 확인
WebSocket이나 Server-Sent Events를 배우기 전에, 폴링을 이해하면 실시간 통신의 기본 개념을 잡을 수 있습니다.
2. 간단한 실시간 기능에는 충분합니다
// 예: 작업 진행 상태 확인
async function checkJobStatus(jobId) {
const response = await fetch(`/api/jobs/${jobId}/status`);
const { status, progress } = await response.json();
updateProgressBar(progress);
if (status !== 'completed') {
setTimeout(() => checkJobStatus(jobId), 2000); // 2초 후 다시 확인
}
}
파일 업로드 진행 상태, 백그라운드 작업 모니터링 등에는 폴링만으로도 충분합니다.
3. 폴링의 원리를 이해해야 대안을 선택할 수 있습니다
폴링의 장단점을 이해하면, 언제 WebSocket으로 전환해야 하는지, 언제 Server-Sent Events를 사용해야 하는지 판단할 수 있습니다.
먼저, HTTP의 한계를 이해하기
HTTP는 요청-응답(Request-Response) 모델입니다.
클라이언트 (브라우저) 서버
│ │
│ ──── 요청(Request) ───────>│
│ │ (데이터 처리)
│ <──── 응답(Response) ─────│
│ │
│ 끝! │
이것이 의미하는 것:
- 클라이언트가 먼저 요청해야만 서버가 응답합니다
- 서버는 먼저 연락할 수 없습니다
- 한 번 응답하면 연결이 끊어집니다
그렇다면 어떻게 “실시간”으로 데이터를 받을까요?
“클라이언트가 계속 물어보면 되지 않을까?”
바로 이것이 폴링입니다!
폴링이란 무엇인가?
폴링(Polling)은 클라이언트가 주기적으로 서버에 요청을 보내 새로운 데이터가 있는지 확인하는 기법입니다.
일상생활 비유
택배를 기다리는 상황을 떠올려보세요.
폴링 방식:
나: "택배 왔어요?" (1분 후)
문 앞: "없어요."
나: "택배 왔어요?" (1분 후)
문 앞: "없어요."
나: "택배 왔어요?" (1분 후)
문 앞: "네, 있어요!"
WebSocket 방식 (비교):
나: "택배 오면 알려주세요." (연결 유지)
문 앞: "알겠습니다." (대기)
...
문 앞: "택배 왔습니다!" (즉시 알림)
폴링은 계속 물어보는 방식, WebSocket은 한 번 요청하고 기다리는 방식입니다.
폴링의 기본 패턴
// 가장 기본적인 폴링 패턴
function startPolling() {
setInterval(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => {
console.log('새 데이터:', data);
updateUI(data);
});
}, 3000); // 3초마다 요청
}
startPolling();
Short Polling (짧은 폴링)
Short Polling은 일정한 간격으로 서버에 요청을 보내는 가장 단순한 폴링 방식입니다.
동작 방식
클라이언트 서버
│ │
│ ─── 요청 ─────────>│
│ │ (즉시 응답)
│ <─── 응답 ────────│
│ │
│ (3초 대기) │
│ │
│ ─── 요청 ─────────>│
│ │ (즉시 응답)
│ <─── 응답 ────────│
│ │
│ (3초 대기) │
│ │
│ ─── 요청 ─────────>│
구현 예제
기본 구현
// ✅ Short Polling 기본 구현
function shortPolling() {
const pollInterval = 5000; // 5초마다
setInterval(async () => {
try {
const response = await fetch('/api/notifications');
const data = await response.json();
if (data.notifications.length > 0) {
displayNotifications(data.notifications);
}
} catch (error) {
console.error('폴링 실패:', error);
}
}, pollInterval);
}
shortPolling();
시작/중지 기능 추가
// ✅ 시작/중지 가능한 폴링
class Poller {
constructor(url, interval = 5000) {
this.url = url;
this.interval = interval;
this.timerId = null;
}
start() {
// 이미 실행 중이면 무시
if (this.timerId) {
console.log('이미 폴링이 실행 중입니다');
return;
}
// 즉시 한 번 실행
this.poll();
// 주기적으로 실행
this.timerId = setInterval(() => {
this.poll();
}, this.interval);
}
stop() {
if (this.timerId) {
clearInterval(this.timerId);
this.timerId = null;
console.log('폴링이 중지되었습니다');
}
}
async poll() {
try {
const response = await fetch(this.url);
const data = await response.json();
this.onData(data);
} catch (error) {
this.onError(error);
}
}
// 오버라이드 가능한 콜백
onData(data) {
console.log('받은 데이터:', data);
}
onError(error) {
console.error('폴링 에러:', error);
}
}
// 사용 예
const poller = new Poller('/api/messages', 3000);
poller.onData = (data) => {
updateMessages(data.messages);
};
poller.start();
// 나중에 중지
// poller.stop();
Short Polling의 장단점
장점:
- ✅ 구현이 매우 간단합니다
- ✅ 모든 브라우저와 서버에서 작동합니다
- ✅ 디버깅이 쉽습니다 (개발자 도구에서 요청 확인 가능)
단점:
- ❌ 불필요한 요청이 많습니다 (데이터가 없어도 계속 요청)
- ❌ 서버 부하가 큽니다
- ❌ 실시간성이 떨어집니다 (간격만큼 지연)
- ❌ 네트워크 비용이 높습니다
Long Polling (긴 폴링)
Long Polling은 서버가 새 데이터가 생길 때까지 응답을 지연시키는 방식입니다.
동작 방식
클라이언트 서버
│ │
│ ─── 요청 ─────────>│
│ │ (대기... 새 데이터 없음)
│ │ (대기... 새 데이터 없음)
│ │ (대기... 새 데이터 생김!)
│ <─── 응답 ────────│
│ │
│ ─── 즉시 다시 요청 >│
│ │ (대기...)
구현 예제
클라이언트 측
// ✅ Long Polling 구현
async function longPolling() {
try {
const response = await fetch('/api/messages/poll', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// 받은 데이터 처리
handleNewData(data);
} catch (error) {
console.error('Long polling 에러:', error);
// 에러 발생 시 3초 후 재시도
await new Promise(resolve => setTimeout(resolve, 3000));
}
// 즉시 다시 폴링 (재귀 호출)
longPolling();
}
// 시작
longPolling();
서버 측 (Node.js/Express 예제)
// ✅ Long Polling 서버 구현
const express = require('express');
const app = express();
// 대기 중인 클라이언트들을 저장
const waitingClients = [];
// Long Polling 엔드포인트
app.get('/api/messages/poll', (req, res) => {
// 30초 타임아웃 설정
const timeout = setTimeout(() => {
// 타임아웃되면 빈 응답 전송
res.json({ messages: [] });
// 대기 목록에서 제거
const index = waitingClients.indexOf(res);
if (index > -1) {
waitingClients.splice(index, 1);
}
}, 30000);
// 클라이언트를 대기 목록에 추가
waitingClients.push({ res, timeout });
});
// 새 메시지가 생겼을 때 호출되는 함수
function notifyClients(newMessage) {
// 대기 중인 모든 클라이언트에게 응답
while (waitingClients.length > 0) {
const { res, timeout } = waitingClients.shift();
// 타임아웃 취소
clearTimeout(timeout);
// 새 메시지 전송
res.json({ messages: [newMessage] });
}
}
// 예: 새 메시지 도착 (다른 API 엔드포인트)
app.post('/api/messages', (req, res) => {
const newMessage = req.body;
// 메시지 저장
saveMessage(newMessage);
// 대기 중인 클라이언트들에게 알림
notifyClients(newMessage);
res.json({ success: true });
});
개선된 Long Polling (재연결 로직 포함)
// ✅ 재연결 로직이 있는 Long Polling
class LongPoller {
constructor(url, options = {}) {
this.url = url;
this.isRunning = false;
this.retryDelay = options.retryDelay || 3000;
this.maxRetries = options.maxRetries || 3;
this.currentRetries = 0;
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.currentRetries = 0;
this.poll();
}
stop() {
this.isRunning = false;
}
async poll() {
if (!this.isRunning) return;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 35000); // 35초 타임아웃
const response = await fetch(this.url, {
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
// 성공 시 재시도 카운터 리셋
this.currentRetries = 0;
// 데이터 처리
this.onData(data);
// 즉시 다시 폴링
this.poll();
} catch (error) {
this.onError(error);
this.currentRetries++;
if (this.currentRetries <= this.maxRetries) {
// 지연 후 재시도
console.log(`${this.retryDelay}ms 후 재시도... (${this.currentRetries}/${this.maxRetries})`);
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
this.poll();
} else {
console.error('최대 재시도 횟수 초과. 폴링 중지.');
this.stop();
}
}
}
onData(data) {
console.log('받은 데이터:', data);
}
onError(error) {
console.error('Long polling 에러:', error);
}
}
// 사용 예
const longPoller = new LongPoller('/api/messages/poll', {
retryDelay: 5000,
maxRetries: 5,
});
longPoller.onData = (data) => {
if (data.messages && data.messages.length > 0) {
displayMessages(data.messages);
}
};
longPoller.start();
Long Polling의 장단점
장점:
- ✅ Short Polling보다 실시간성이 좋습니다
- ✅ 불필요한 요청이 줄어듭니다 (데이터가 없으면 대기)
- ✅ 서버 부하가 상대적으로 적습니다
단점:
- ❌ 서버 구현이 복잡합니다 (요청을 오래 유지해야 함)
- ❌ 많은 클라이언트가 동시에 대기하면 서버 리소스 소모
- ❌ 타임아웃 관리가 필요합니다
- ❌ 프록시나 방화벽에서 연결을 끊을 수 있습니다
실전 예제로 배우는 폴링
예제 1: 작업 진행 상태 모니터링
파일 업로드나 데이터 처리 같은 백그라운드 작업의 진행 상태를 확인하는 경우입니다.
// ✅ 작업 진행 상태 폴링
async function uploadFileAndMonitor(file) {
// 1. 파일 업로드 시작
const uploadResponse = await fetch('/api/upload', {
method: 'POST',
body: file,
});
const { jobId } = await uploadResponse.json();
console.log('작업 시작됨:', jobId);
// 2. 진행 상태 모니터링
await monitorJobProgress(jobId);
}
async function monitorJobProgress(jobId) {
const checkStatus = async () => {
try {
const response = await fetch(`/api/jobs/${jobId}/status`);
const { status, progress, message } = await response.json();
// UI 업데이트
updateProgressBar(progress);
updateStatusMessage(message);
console.log(`진행률: ${progress}% - ${status}`);
if (status === 'completed') {
console.log('작업 완료!');
showSuccessMessage('파일 업로드 완료!');
return; // 폴링 중지
}
if (status === 'failed') {
console.error('작업 실패:', message);
showErrorMessage(message);
return; // 폴링 중지
}
// 작업이 진행 중이면 2초 후 다시 확인
setTimeout(checkStatus, 2000);
} catch (error) {
console.error('상태 확인 실패:', error);
// 에러 시 5초 후 재시도
setTimeout(checkStatus, 5000);
}
};
// 첫 번째 확인 시작
checkStatus();
}
// 사용 예
const fileInput = document.querySelector('#fileInput');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
uploadFileAndMonitor(file);
});
예제 2: 실시간 알림
사용자에게 새 알림을 보여주는 경우입니다.
// ✅ 알림 폴링 시스템
class NotificationPoller {
constructor() {
this.lastCheckTime = Date.now();
this.pollInterval = 10000; // 10초
this.isActive = false;
}
start() {
if (this.isActive) return;
this.isActive = true;
this.poll();
}
stop() {
this.isActive = false;
}
async poll() {
if (!this.isActive) return;
try {
// 마지막 확인 시간 이후의 알림만 가져오기
const response = await fetch(`/api/notifications?since=${this.lastCheckTime}`);
const { notifications } = await response.json();
if (notifications.length > 0) {
// 알림 표시
this.displayNotifications(notifications);
// 알림 개수 배지 업데이트
this.updateBadge(notifications.length);
// 사운드 재생 (선택사항)
this.playNotificationSound();
}
// 마지막 확인 시간 업데이트
this.lastCheckTime = Date.now();
} catch (error) {
console.error('알림 가져오기 실패:', error);
}
// 다음 폴링 예약
setTimeout(() => this.poll(), this.pollInterval);
}
displayNotifications(notifications) {
notifications.forEach(notification => {
// 브라우저 알림 표시
if (Notification.permission === 'granted') {
new Notification(notification.title, {
body: notification.message,
icon: notification.icon,
});
}
// 화면에 알림 추가
addNotificationToUI(notification);
});
}
updateBadge(count) {
const badge = document.querySelector('#notification-badge');
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? 'block' : 'none';
}
}
playNotificationSound() {
const audio = new Audio('/sounds/notification.mp3');
audio.play().catch(err => console.log('사운드 재생 실패:', err));
}
}
// 사용 예
const notificationPoller = new NotificationPoller();
// 페이지 로드 시 시작
document.addEventListener('DOMContentLoaded', () => {
// 알림 권한 요청
if (Notification.permission === 'default') {
Notification.requestPermission();
}
notificationPoller.start();
});
// 페이지를 떠날 때 중지
document.addEventListener('beforeunload', () => {
notificationPoller.stop();
});
예제 3: 적응형 폴링 (Adaptive Polling)
사용자 활동에 따라 폴링 주기를 조정하는 예제입니다.
// ✅ 적응형 폴링 - 사용자 활동에 따라 주기 조정
class AdaptivePoller {
constructor(url) {
this.url = url;
this.minInterval = 5000; // 최소 5초
this.maxInterval = 60000; // 최대 60초
this.currentInterval = this.minInterval;
this.isUserActive = true;
this.timerId = null;
this.setupActivityTracking();
}
setupActivityTracking() {
// 사용자 활동 감지
const activityEvents = ['mousedown', 'keydown', 'scroll', 'touchstart'];
let activityTimeout;
const handleActivity = () => {
this.isUserActive = true;
this.currentInterval = this.minInterval; // 활성 시 빠른 폴링
// 5초간 활동이 없으면 비활성으로 간주
clearTimeout(activityTimeout);
activityTimeout = setTimeout(() => {
this.isUserActive = false;
}, 5000);
};
activityEvents.forEach(event => {
document.addEventListener(event, handleActivity, { passive: true });
});
// 탭 가시성 감지
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.isUserActive = false;
this.currentInterval = this.maxInterval; // 백그라운드에서는 느린 폴링
} else {
this.isUserActive = true;
this.currentInterval = this.minInterval;
}
});
}
start() {
this.poll();
}
stop() {
if (this.timerId) {
clearTimeout(this.timerId);
this.timerId = null;
}
}
async poll() {
try {
const response = await fetch(this.url);
const data = await response.json();
this.onData(data);
// 데이터가 있으면 더 자주 확인
if (data.hasUpdates) {
this.currentInterval = this.minInterval;
} else {
// 데이터가 없으면 점진적으로 간격 증가
this.currentInterval = Math.min(
this.currentInterval * 1.5,
this.maxInterval
);
}
} catch (error) {
this.onError(error);
}
// 다음 폴링 예약
const nextInterval = this.isUserActive
? this.currentInterval
: this.maxInterval;
console.log(`다음 폴링: ${nextInterval}ms 후`);
this.timerId = setTimeout(() => this.poll(), nextInterval);
}
onData(data) {
console.log('받은 데이터:', data);
}
onError(error) {
console.error('폴링 에러:', error);
}
}
// 사용 예
const adaptivePoller = new AdaptivePoller('/api/updates');
adaptivePoller.onData = (data) => {
if (data.updates) {
updateUI(data.updates);
}
};
adaptivePoller.start();
함정과 주의사항
1. 메모리 누수 - 폴링을 중지하지 않는 경우
// ❌ 나쁜 예: 폴링을 중지하지 않음
function startPolling() {
setInterval(() => {
fetch('/api/data');
}, 5000);
}
// 컴포넌트가 언마운트되어도 폴링이 계속 실행됨!
해결책:
// ✅ 좋은 예: 정리(cleanup) 함수 제공
function startPolling() {
const intervalId = setInterval(() => {
fetch('/api/data');
}, 5000);
// 중지 함수 반환
return () => {
clearInterval(intervalId);
};
}
// 사용 예
const stopPolling = startPolling();
// 나중에 중지
stopPolling();
React에서의 예:
// ✅ React에서 폴링 관리
import { useEffect, useState } from 'react';
function usePolling(url, interval = 5000) {
const [data, setData] = useState(null);
useEffect(() => {
const poll = async () => {
const response = await fetch(url);
const result = await response.json();
setData(result);
};
// 즉시 한 번 실행
poll();
// 주기적으로 실행
const intervalId = setInterval(poll, interval);
// 컴포넌트 언마운트 시 정리
return () => {
clearInterval(intervalId);
};
}, [url, interval]);
return data;
}
// 사용 예
function MyComponent() {
const data = usePolling('/api/messages', 5000);
return <div>{JSON.stringify(data)}</div>;
}
2. 네트워크 에러 처리하지 않기
// ❌ 나쁜 예: 에러 처리 없음
setInterval(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => updateUI(data));
// 에러 발생 시 폴링이 계속되지만 아무 일도 안 일어남
}, 5000);
해결책:
// ✅ 좋은 예: 에러 처리 포함
async function poll() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
updateUI(data);
} catch (error) {
console.error('폴링 실패:', error);
// 에러를 사용자에게 알림
showErrorNotification('데이터를 가져올 수 없습니다');
// 또는 에러 횟수를 추적하여 일정 횟수 이상이면 중지
// retryCount++;
// if (retryCount > MAX_RETRIES) {
// stopPolling();
// }
}
}
setInterval(poll, 5000);
3. 중복 요청 - 이전 요청이 완료되기 전에 새 요청
// ❌ 나쁜 예: 요청이 겹칠 수 있음
setInterval(() => {
fetch('/api/slow-endpoint'); // 응답에 6초 걸림
}, 5000); // 5초마다 요청
// 타임라인:
// 0초: 요청1 시작
// 5초: 요청2 시작 (요청1 아직 응답 안 옴)
// 6초: 요청1 응답
// 10초: 요청3 시작 (요청2 아직 응답 안 옴)
// 11초: 요청2 응답
해결책 1: 이전 요청 완료 후 다음 요청
// ✅ 좋은 예: 재귀적 setTimeout 사용
async function poll() {
try {
const response = await fetch('/api/slow-endpoint');
const data = await response.json();
updateUI(data);
} catch (error) {
console.error('폴링 실패:', error);
}
// 이전 요청 완료 후 5초 뒤에 다음 요청
setTimeout(poll, 5000);
}
poll(); // 시작
해결책 2: 진행 중인 요청 추적
// ✅ 좋은 예: 플래그로 중복 방지
let isPolling = false;
setInterval(async () => {
if (isPolling) {
console.log('이전 요청이 진행 중입니다. 건너뜁니다.');
return;
}
isPolling = true;
try {
const response = await fetch('/api/slow-endpoint');
const data = await response.json();
updateUI(data);
} catch (error) {
console.error('폴링 실패:', error);
} finally {
isPolling = false;
}
}, 5000);
4. 불필요한 리렌더링 - 동일한 데이터에도 UI 업데이트
// ❌ 나쁜 예: 항상 상태 업데이트
setInterval(async () => {
const response = await fetch('/api/data');
const newData = await response.json();
setState(newData); // 데이터가 같아도 리렌더링 발생
}, 5000);
해결책:
// ✅ 좋은 예: 데이터 변경 시만 업데이트
let previousData = null;
setInterval(async () => {
const response = await fetch('/api/data');
const newData = await response.json();
// 데이터 비교
if (JSON.stringify(newData) !== JSON.stringify(previousData)) {
setState(newData);
previousData = newData;
console.log('데이터가 변경되었습니다');
} else {
console.log('데이터 변경 없음');
}
}, 5000);
5. 서버 부하 - 너무 짧은 폴링 간격
// ❌ 나쁜 예: 너무 자주 요청
setInterval(() => {
fetch('/api/data');
}, 100); // 100ms마다 (초당 10번!)
권장 폴링 간격:
- 긴급한 알림: 3-5초
- 일반 업데이트: 10-30초
- 백그라운드 작업: 30-60초
// ✅ 좋은 예: 적절한 간격
const POLLING_INTERVALS = {
URGENT: 3000, // 3초
NORMAL: 10000, // 10초
BACKGROUND: 30000, // 30초
};
setInterval(() => {
fetch('/api/notifications');
}, POLLING_INTERVALS.NORMAL);
폴링 최적화하기
1. 조건부 요청 (Conditional Requests)
HTTP ETag나 Last-Modified 헤더를 사용해 변경된 데이터만 가져옵니다.
// ✅ ETag를 사용한 조건부 요청
let etag = null;
async function conditionalPoll() {
const headers = {};
// 이전에 받은 ETag가 있으면 포함
if (etag) {
headers['If-None-Match'] = etag;
}
const response = await fetch('/api/data', { headers });
if (response.status === 304) {
// 304 Not Modified: 데이터 변경 없음
console.log('데이터 변경 없음');
return;
}
// 새 ETag 저장
etag = response.headers.get('ETag');
const data = await response.json();
updateUI(data);
}
setInterval(conditionalPoll, 5000);
서버 측 (Node.js/Express):
// ✅ ETag 지원 서버
app.get('/api/data', (req, res) => {
const data = getData(); // 데이터 가져오기
const currentETag = generateETag(data); // ETag 생성
// 클라이언트가 보낸 ETag와 비교
if (req.headers['if-none-match'] === currentETag) {
// 데이터 변경 없음
return res.status(304).end();
}
// 새 데이터 전송
res.set('ETag', currentETag);
res.json(data);
});
2. 차등 업데이트 (Delta Updates)
전체 데이터 대신 변경된 부분만 전송합니다.
// ✅ 차등 업데이트
let lastUpdateTime = 0;
async function deltaPoll() {
const response = await fetch(`/api/data?since=${lastUpdateTime}`);
const { updates, timestamp } = await response.json();
if (updates.length > 0) {
// 변경된 항목만 업데이트
applyUpdates(updates);
}
lastUpdateTime = timestamp;
}
setInterval(deltaPoll, 5000);
3. 배치 요청 (Batching)
여러 개의 폴링을 하나의 요청으로 묶습니다.
// ❌ 나쁜 예: 개별 요청
setInterval(() => fetch('/api/messages'), 5000);
setInterval(() => fetch('/api/notifications'), 5000);
setInterval(() => fetch('/api/updates'), 5000);
// 3개의 요청 = 3배의 네트워크 비용
// ✅ 좋은 예: 배치 요청
setInterval(async () => {
const response = await fetch('/api/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requests: [
{ endpoint: '/api/messages' },
{ endpoint: '/api/notifications' },
{ endpoint: '/api/updates' },
],
}),
});
const results = await response.json();
results.forEach(({ endpoint, data }) => {
switch (endpoint) {
case '/api/messages':
updateMessages(data);
break;
case '/api/notifications':
updateNotifications(data);
break;
case '/api/updates':
updateUI(data);
break;
}
});
}, 5000);
4. 백오프 전략 (Exponential Backoff)
에러 발생 시 점진적으로 재시도 간격을 늘립니다.
// ✅ Exponential Backoff
class BackoffPoller {
constructor(url, initialInterval = 5000) {
this.url = url;
this.initialInterval = initialInterval;
this.currentInterval = initialInterval;
this.maxInterval = 60000; // 최대 60초
this.errorCount = 0;
}
async poll() {
try {
const response = await fetch(this.url);
const data = await response.json();
// 성공 시 간격 초기화
this.currentInterval = this.initialInterval;
this.errorCount = 0;
this.onSuccess(data);
} catch (error) {
this.errorCount++;
// 지수적으로 간격 증가: 5s → 10s → 20s → 40s → 60s
this.currentInterval = Math.min(
this.currentInterval * 2,
this.maxInterval
);
console.error(`폴링 실패 (${this.errorCount}회). 다음 시도: ${this.currentInterval}ms 후`);
this.onError(error);
}
setTimeout(() => this.poll(), this.currentInterval);
}
start() {
this.poll();
}
onSuccess(data) {
console.log('데이터:', data);
}
onError(error) {
console.error('에러:', error);
}
}
const poller = new BackoffPoller('/api/data');
poller.start();
폴링의 대안들
폴링은 간단하지만, 더 효율적인 대안들이 있습니다.
1. WebSocket
양방향 실시간 통신을 위한 프로토콜입니다.
// ✅ WebSocket 예제
const socket = new WebSocket('ws://localhost:8080');
socket.addEventListener('open', () => {
console.log('연결됨');
});
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('새 메시지:', data);
updateUI(data);
});
socket.addEventListener('close', () => {
console.log('연결 종료');
});
// 서버로 메시지 전송
socket.send(JSON.stringify({ type: 'ping' }));
장점:
- ✅ 진정한 실시간 통신
- ✅ 서버가 먼저 데이터 전송 가능
- ✅ 오버헤드가 매우 낮음
단점:
- ❌ 서버 구현이 복잡
- ❌ 일부 프록시/방화벽에서 차단될 수 있음
2. Server-Sent Events (SSE)
서버에서 클라이언트로 단방향 이벤트 스트림입니다.
// ✅ Server-Sent Events 예제
const eventSource = new EventSource('/api/stream');
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('새 데이터:', data);
updateUI(data);
});
eventSource.addEventListener('error', (error) => {
console.error('SSE 에러:', error);
});
// 연결 종료
// eventSource.close();
서버 측 (Node.js/Express):
app.get('/api/stream', (req, res) => {
// SSE 헤더 설정
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 10초마다 데이터 전송
const intervalId = setInterval(() => {
const data = { time: new Date(), message: 'Hello' };
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 10000);
// 클라이언트 연결 종료 시 정리
req.on('close', () => {
clearInterval(intervalId);
});
});
장점:
- ✅ HTTP 기반 (프록시/방화벽 친화적)
- ✅ 자동 재연결
- ✅ 구현이 간단
단점:
- ❌ 단방향 (서버 → 클라이언트만)
- ❌ IE/Edge(구버전)에서 미지원
3. 언제 무엇을 사용할까?
| 상황 | 권장 기술 | 이유 |
|---|---|---|
| 간단한 상태 확인 (작업 진행률) | Short Polling | 구현 간단, 일시적 |
| 가끔 발생하는 이벤트 (알림) | Long Polling | 불필요한 요청 감소 |
| 채팅, 게임 등 빈번한 양방향 통신 | WebSocket | 진정한 실시간, 낮은 지연 |
| 서버 → 클라이언트 단방향 (주식, 뉴스) | SSE | 간단, HTTP 기반 |
| 구식 브라우저 지원 필요 | Polling | 모든 브라우저 지원 |
의사결정 트리:
실시간 통신이 필요한가?
│
├─ 아니오 → 일반 HTTP 요청 사용
│
└─ 예 → 얼마나 자주 업데이트되는가?
│
├─ 가끔 (1분에 1-2번) → Polling
│ │
│ └─ 예측 가능한 간격? → Short Polling
│ 예측 불가능? → Long Polling
│
└─ 자주 (초당 여러 번) → WebSocket 또는 SSE
│
└─ 양방향 필요? → WebSocket
단방향? → SSE
결론: 폴링을 언제 어떻게 사용할까?
폴링을 이해하면
- ✅ 실시간 기능을 간단하게 구현할 수 있습니다
- ✅ 백그라운드 작업을 모니터링할 수 있습니다
- ✅ 더 나은 대안을 선택할 수 있습니다
핵심 원칙
1. 적절한 폴링 간격 선택 (3~30초)
2. 항상 에러 처리 포함
3. 컴포넌트 언마운트 시 정리(cleanup)
4. 중복 요청 방지
5. 필요하면 적응형 폴링 사용
폴링 vs 대안 선택 가이드
폴링을 사용하세요:
- 구현이 간단해야 할 때
- 업데이트 빈도가 낮을 때 (1분에 몇 번)
- 일시적인 작업 모니터링
- 모든 브라우저 지원이 필요할 때
WebSocket으로 전환하세요:
- 초당 여러 번 업데이트
- 양방향 통신 필요
- 지연 시간이 중요할 때
SSE를 고려하세요:
- 서버 → 클라이언트만 필요
- HTTP 인프라 활용
- 자동 재연결 필요
기억해야 할 것
- 폴링은 간단하지만 비효율적입니다
- 적절한 간격과 에러 처리가 중요합니다
- 정리(cleanup)를 잊지 마세요
- 더 나은 대안이 있는지 고려하세요
폴링은 실시간 통신의 첫 걸음입니다. 이것을 이해하면 더 복잡한 기술로 나아갈 수 있습니다!
참고 자료
공식 문서
심화 학습
관련 문서
- JavaScript 이벤트 루프 - 폴링과 이벤트 루프의 관계
댓글