44.1 폴링이 뭐예요?
실시간을 만드는 가장 쉬운 방법부터 볼게요. 이름은 폴링(polling)이에요. 개념은 유치할 만큼 단순해요. 일정 시간마다 서버에 물어보는 거예요. "새 거 있어? 새 거 있어?" 하고 3초에 한 번씩 계속 찔러보는 거죠. 뒷좌석에 탄 아이가 "다 왔어요? 다 왔어요?" 하고 반복해서 묻는 그 장면을 떠올리면 딱이에요.
이게 왜 좋냐면, 우리가 이미 아는 fetch만으로 만들 수 있기 때문이에요. 새 기술이 하나도 안 필요해요. 서버도 특별히 손볼 게 없어요. 평범한 API를 그냥 반복해서 부르는 것뿐이니까요. 저도 실시간이라는 걸 처음 붙여본 게 이 폴링이었어요. 그날 "어? 실시간 별거 아니네?" 했다가, 사용자가 늘고 나서 서버 요금 고지서를 보고 생각이 좀 바뀌었죠. 그 이야기까지 차근차근 해볼게요.
44.2 setInterval로 어떻게 만들어요?
채팅에서 새 메시지를 폴링으로 가져온다고 해볼게요. 서버에는 "가장 최근 메시지를 주는" 평범한 주소 하나만 있으면 돼요. 클라이언트는 이렇게 짜요.
async function checkNew() {
const res = await fetch('/api/messages/latest');
const data = await res.json();
render(data); // 화면에 그리기
}
// 3초마다 checkNew 실행
const timer = setInterval(checkNew, 3000);
핵심은 마지막 줄 setInterval이에요. 첫 번째 자리에 넣은 함수를, 두 번째 자리의 밀리초(1000분의 1초)마다 계속 실행해줘요. 여기선 3000이니까 3초마다 checkNew가 돌면서 fetch를 날려요. 그만두고 싶으면 clearInterval(timer)로 멈추면 돼요. 사용자가 채팅방을 나갈 때 이걸 꼭 불러줘야, 안 보는 화면이 계속 서버를 두드리는 사고를 막아요.
결과. 사용자가 아무것도 안 눌러도 3초에 한 번씩 화면이 최신으로 바뀌어요. 상대가 메시지를 보내면 최대 3초 안에 내 화면에 떠요. 이게 폴링으로 만든 실시간이에요. 눈으로 보면 그럴듯하게 돌아가죠.
44.3 폴링은 뭐가 아쉬워요?
그런데 저 코드에는 두 가지 딜레마가 숨어 있어요. 바로 간격 3000이라는 숫자예요.
첫째는 지연이에요. 3초 간격이면, 하필 방금 물어본 직후에 새 메시지가 오면 다음 물음까지 3초를 기다려야 보여요. 더 빠르게 하려고 간격을 1초로 줄이면요? 둘째 문제인 낭비가 튀어나와요. 대부분의 물음에는 새 게 없어요. 조용한 채팅방이라면 1초마다 서버에 물어봐도 열 번 중 아홉 번은 "없어"라는 대답만 받아요.
사용자 1000명 x 1초마다 요청
= 초당 1000번의 fetch
= 그 대부분은 "새 거 없음" 이라는 빈 응답
이 헛된 요청이 서버 비용이 되고, 배터리를 갉아먹고, 데이터도 써요. 그래서 폴링은 늘 빠르게 하자니 낭비, 아끼자니 느림 사이에서 줄타기를 해요. 저는 이 줄타기가 싫어서 간격을 5초로 늘렸다가 "왜 이렇게 느려요" 소리를 듣고, 다시 2초로 줄였다가 서버가 헉헉대는 걸 봤어요. 이 딜레마를 정면으로 푼 게 다음에 볼 롱폴링이에요.
44.4 롱폴링은 뭐가 달라요?
폴링의 문제는 새 게 없는데도 자꾸 물어보는 거였죠. 롱폴링(long polling)의 아이디어는 여기서 출발해요. "물어봤는데 새 게 없으면, 서버가 바로 '없어'라고 답하지 말고, 새 게 생길 때까지 대답을 미뤄줘"라는 거예요.
식당에서 진동벨을 받는 장면이 딱이에요. 손님이 1분마다 "음식 나왔어요?" 하고 카운터에 묻는 게 폴링이라면, 진동벨을 쥐고 가만히 기다리다 울리는 순간 받으러 가는 게 롱폴링이에요. 브라우저는 요청 하나를 보내놓고, 서버는 그 요청을 붙잡은 채 새 소식이 생길 때까지 응답을 안 줘요. 새 메시지가 도착하면 그제야 응답을 내려주고, 브라우저는 받자마자 곧바로 다음 요청을 또 보내서 다시 기다려요. 이러면 빈 응답이 확 줄고, 새 소식은 생기자마자 거의 즉시 전달돼요. 지연과 낭비를 한 번에 줄인 셈이죠.
44.5 롱폴링을 코드로 보면요?
클라이언트 코드는 폴링과 모양이 좀 달라요. setInterval 대신, 응답을 받으면 스스로를 다시 부르는 방식이에요.
async function subscribe() {
try {
// 서버가 새 소식 생길 때까지 붙잡음
const res = await fetch('/api/messages/wait');
if (res.status === 200) {
const data = await res.json();
render(data);
}
} catch (e) {
await new Promise(r => setTimeout(r, 1000));
}
subscribe(); // 받자마자 다시 대기
}
subscribe();
핵심은 두 군데예요. 하나는 fetch가 오래 걸린다는 점이에요. 서버가 소식이 생길 때까지 응답을 미루니까, 이 await는 몇 초에서 수십 초까지 걸릴 수 있어요. 둘은 마지막 subscribe()예요. 응답을 처리한 뒤 스스로를 다시 불러서, 끊김 없이 또 대기 상태로 들어가요. 이 꼬리물기 덕분에 브라우저는 사실상 항상 서버와 연결을 걸어둔 상태가 돼요.
결과. 조용할 땐 요청 하나가 조용히 걸려 있을 뿐 트래픽이 없고, 새 메시지가 오면 1초도 안 돼 화면에 떠요. 폴링의 두 딜레마가 동시에 완화됐죠. catch 안에 넣은 1초 대기는, 연결이 실패했을 때 서버를 쉴 틈 없이 두드리지 않게 하는 안전장치예요.
44.6 롱폴링도 완벽하진 않죠?
좋아 보이지만 롱폴링에도 값이 있어요. 첫째, 서버가 요청을 붙잡고 있어야 해요. 사용자 1000명이 동시에 대기하면, 서버는 열려 있는 연결 1000개를 물고 있어야 해요. 아무 일 안 하는 것 같아도 연결 자체가 자원이라, 서버 설계가 이걸 견디게 돼 있어야 해요.
둘째, 메시지가 쉴 새 없이 쏟아지면 롱폴링의 장점이 사라져요. 응답 하나 받고 다시 요청, 또 받고 다시 요청을 반복하면, 결국 잦은 폴링과 비슷해지거든요. 그래서 롱폴링은 가끔, 그러나 오면 빨라야 하는 소식에 잘 맞고, 초당 여러 번 오가는 대화엔 안 맞아요. 이 지점이 바로 SSE나 웹소켓 같은 연결을 아예 열어두는 방식이 등장하는 이유이기도 해요.
44.7 그래서 언제 폴링을 써요?
오래된 기술이라고 폴링을 무시하면 안 돼요. 아직도 자주 쓰여요. 판단 기준을 정리해 드릴게요.
폴링이 잘 맞는 경우
1. 지연이 넉넉하다 (몇 초, 몇 분 늦어도 OK)
2. 데이터가 가끔 바뀐다
3. 서버를 특별히 손대기 어렵다 (평범한 API만 있음)
4. 빨리, 간단히 붙이고 싶다
대시보드의 통계 숫자, 주문 처리 상태(접수 → 조리 → 배달), 몇 분마다 갱신되는 날씨가 딱 폴링감이에요. 반대로 오는 즉시 봐야 하는데 자주는 안 오는 알림 같은 건 롱폴링이 어울려요. 둘 다 추가 서버 기술 없이 HTTP 위에서 굴러간다는 게 공통 장점이고요. 그래서 실시간을 처음 붙일 때, 저는 웹소켓부터 꺼내지 않고 "이거 폴링으로 충분한 거 아냐?"를 먼저 따져요. 절반은 여기서 끝나요.
44.8 정리하면요?
폴링과 롱폴링을 나란히 두고 마무리할게요.
핵심 비교
1. 폴링: setInterval로 주기적으로 물어봄. 쉽지만 지연과 낭비의 줄타기.
2. 롱폴링: 서버가 소식 생길 때까지 응답을 미룸. 빈 요청이 줄고 전달이 빨라짐.
3. 롱폴링의 값: 서버가 연결을 붙잡아야 하고, 소식이 잦으면 이점이 사라짐.
4. 지연이 넉넉하고 가끔 바뀌면 폴링, 즉시성이 필요하되 자주는 아니면 롱폴링.
두 방식의 공통점은 어쨌든 브라우저가 먼저 물어봐야 대화가 시작된다는 거예요. 서버는 스스로 말을 걸지 못하고, 브라우저가 던진 질문에 답하는 형태로만 움직이죠. 이 한계를 이해하고 나면, 폴링과 롱폴링이 왜 "HTTP를 영리하게 쥐어짠 결과물"인지 보여요. "실시간 = 웹소켓"이라는 성급한 등식부터 버리고, 평범한 HTTP만으로도 여기까지 온다는 걸 손에 익히는 게 이번 이야기의 알맹이예요.