실시간 기능을 만들 때 가장 먼저 나눌 것은 앱이 열려 있는 동안 데이터를 갱신할 것인지, 앱이 꺼진 사용자에게 다시 방문할 이유를 알릴 것인지다. 전자는 폴링·SSE·WebSocket 같은 연결 방식의 문제이고, 후자는 APNs·FCM을 통한 모바일 푸시 알림의 문제다.
이 둘을 하나의 프로토콜 비교표로 놓으면 요구사항이 섞인다. 푸시 알림은 앱의 최신 상태를 보관하는 데이터 채널이 아니며, WebSocket은 종료된 모바일 앱을 깨우는 알림 채널이 아니다.
먼저 네 가지 질문을 정한다
- 데이터는 서버에서 클라이언트로만 흐르는가, 양방향인가
- 새 이벤트를 몇 초 늦게 받아도 되는가
- 브라우저 탭이나 앱이 닫힌 뒤에도 알려야 하는가
- 연결이 끊겼을 때 어느 지점부터 복구해야 하는가
이 답으로 후보를 상당히 줄일 수 있다.
| 요구사항 | 우선 검토할 방식 |
|---|---|
| 수십 초 늦어도 되는 상태 갱신 | 주기적 폴링 |
| 기존 HTTP 인프라에서 이벤트를 빨리 받고 싶음 | 롱폴링 |
| 서버 → 브라우저 단방향 이벤트 스트림 | SSE |
| 채팅·협업처럼 지속적인 양방향 메시지 | WebSocket |
| 앱이 닫혀도 OS 알림이 필요 | APNs·FCM |
폴링은 단순함이 장점이다
주기적 폴링은 클라이언트가 일정 간격으로 최신 상태를 요청한다.
GET /jobs/42
→ RUNNING
10초 뒤 GET /jobs/42
→ SUCCEEDED
실시간성이 수초에서 수분 단위여도 되고 사용자 수가 크지 않다면 가장 운영하기 쉬운 선택일 수 있다. 일반 HTTP 인증·캐시·관측 도구를 그대로 사용하고, 연결 상태를 별도로 관리하지 않는다.
단점은 변경이 없어도 요청이 발생한다는 점이다. 간격을 짧게 할수록 지연은 줄지만 서버·네트워크 부하는 늘어난다. 화면이 숨겨졌을 때 중지하고, exponential backoff와 ETag·If-None-Match 같은 조건부 요청을 적용할 수 있다.
롱폴링은 응답 뒤 다시 요청한다
롱폴링은 서버가 이벤트나 timeout이 생길 때까지 요청을 열어 둔다. 응답을 받은 클라이언트는 즉시 다음 요청을 보낸다.
client ── request ──> server
client <─ event ─── server
client ── next request ──> server
일반 HTTP 요청·응답 모델을 유지하므로 기존 인프라와 호환해야 할 때 쓸 수 있다. 그러나 이벤트마다 완전한 HTTP 요청과 응답이 반복되고, proxy·load balancer timeout과 동시 연결 제한을 고려해야 한다.
원문의 “클라이언트가 주기적으로 요청한다”는 표현은 짧은 폴링과 혼동될 수 있다. 롱폴링은 보통 응답을 받거나 timeout이 난 직후 다음 요청을 이어 항상 대기 중인 요청에 가깝게 만든다.
SSE는 서버에서 브라우저로 보내는 스트림이다
Server-Sent Events(SSE)는 HTTP 응답을 text/event-stream으로 유지하며 서버가 브라우저에 이벤트를 보낸다. 브라우저의 EventSource는 연결이 끊기면 재연결하고, 이벤트에 id가 있으면 Last-Event-ID를 이용해 이어받을 단서를 제공한다.
id: 1042
event: job-status
data: {"jobId":42,"status":"SUCCEEDED"}
const events = new EventSource('/events/jobs');
events.addEventListener('job-status', (event) => {
const payload = JSON.parse(event.data);
console.log(payload);
});
대시보드, 빌드 로그, 알림 목록처럼 서버에서 클라이언트로만 이벤트가 흐를 때 단순하다. 클라이언트가 서버로 보내는 일반 명령은 별도 HTTP 요청을 사용하면 된다.
운영에서는 다음을 확인한다.
- proxy buffering이 스트림을 모아 두지 않는가
- idle timeout보다 짧은 heartbeat가 필요한가
- 이벤트 ID와 replay 저장소가 있는가
- 브라우저의
EventSource인증 방식이 서비스 정책과 맞는가 - 느린 클라이언트에 대한 backpressure 정책이 있는가
WebSocket은 양쪽이 독립적으로 보낼 때 선택한다
WebSocket은 opening handshake 뒤 지속 연결에서 양방향 메시지를 주고받는다.
const socket = new WebSocket('wss://example.com/ws');
socket.addEventListener('open', () => {
socket.send(JSON.stringify({ type: 'subscribe', roomId: '42' }));
});
socket.addEventListener('message', (event) => {
console.log(JSON.parse(event.data));
});
채팅, 공동 편집과 게임처럼 클라이언트와 서버가 서로 독립적으로 낮은 지연의 이벤트를 계속 보낼 때 적합하다. 하지만 연결만 열었다고 신뢰성 있는 메시징이 완성되는 것은 아니다.
애플리케이션 프로토콜에 다음 항목을 정해야 한다.
- 메시지 type과 schema
- 인증 갱신과 권한 변경 반영
- ping/pong과 연결 유효성 판단
- 재연결 시 마지막 처리 위치
- 중복·순서 뒤바뀜과 idempotency
- 느린 소비자와 송신 buffer 제한
- 서버 배포 시 drain과 reconnect 전략
양방향이라는 이유만으로 WebSocket을 기본값으로 두지 않는다. 서버 → 클라이언트 스트림만 필요하면 SSE가 구현·관측 면에서 더 단순할 수 있다.
모바일 푸시는 최신 데이터의 원본이 아니다
iOS는 APNs, Android와 여러 플랫폼에서는 FCM을 통해 원격 알림을 전달한다. 앱이 foreground 연결을 유지하지 않아도 OS가 알림을 표시하거나 제한된 background 처리를 시작할 수 있다는 점이 SSE·WebSocket과 다르다.
FCM의 notification message와 data message는 처리 주체가 다르다. background에서 notification message는 SDK가 표시하고, data message는 앱 코드가 처리한다. 두 메시지 유형의 payload 최대 크기는 4096바이트지만 콘솔 전송은 별도 제한이 있다.
APNs는 best-effort 서비스다. 알림이 지연·재정렬·throttle되거나 전달되지 않을 수 있다. 특히 iOS background notification은 낮은 우선순위이며 전달이 보장되지 않는다.
따라서 푸시 payload 하나만으로 주문·결제 같은 상태를 확정하지 않는다.
푸시: “주문 상태가 바뀌었습니다.”
↓ 사용자가 앱을 열거나 background refresh 허용
API: 현재 주문 상태를 다시 조회
민감정보도 payload에 그대로 넣지 않는다. FCM 문서는 전송 연결이 암호화되지만 end-to-end encryption은 아니라고 명시한다. 최소 식별자와 이벤트 힌트만 보내고 권한이 적용된 API에서 최신 상태를 읽는 편이 안전하다.
어떤 방식도 재연결 설계를 대신하지 않는다
실시간 기능의 품질은 정상 연결보다 끊어진 뒤 드러난다. 공통으로 다음 필드를 두면 복구가 쉬워진다.
{
"eventId": "1042",
"type": "job.status.changed",
"occurredAt": "2026-08-02T01:20:00Z",
"resourceId": "job-42",
"version": 7
}
eventId: 중복 제거와 이어받기occurredAt: 관측과 지연 분석resourceId: 최신 상태 재조회version: 오래된 이벤트의 덮어쓰기 방지
메시지 전달과 데이터 정합성을 분리하는 것이 핵심이다. 이벤트를 놓칠 수 있다는 전제로 API 재조회나 replay 경로를 마련한다.
최종 선택 순서
- 앱이 닫힌 사용자에게 알려야 하면 APNs·FCM을 별도 채널로 둔다.
- 앱이 열린 동안 지연 허용치가 크면 폴링부터 검토한다.
- 서버 → 브라우저 단방향이면 SSE를 검토한다.
- 진짜 양방향 이벤트가 계속 필요하면 WebSocket을 검토한다.
- 어떤 선택이든 timeout, 재연결, 누락·중복과 관측 지표를 먼저 설계한다.
REST·GraphQL·gRPC 같은 API 계약 방식과의 관계는 API 통신 방식 고르는 기준, TCP 연결 자체의 혼잡과 윈도우는 TCP 혼잡 제어 동작 순서에서 이어서 볼 수 있다.
참고 자료
'배움과 성장 > 네트워크·보안' 카테고리의 다른 글
| Kubernetes 네트워크 트러블슈팅 순서: DNS·TCP·TLS·HTTP를 증거로 좁히기 (0) | 2025.03.25 |
|---|---|
| TCP·UDP 기초: Handshake·Socket State·Routing·Subnet을 한 흐름으로 이해하기 (0) | 2025.02.23 |
| Linux 네트워크 트러블슈팅: DNS부터 TCP·TLS·HTTP까지 (1) | 2024.12.04 |
| CDN POP 선택 원리: DNS·Anycast·HTTP 리디렉션은 어떻게 다른가 (1) | 2024.12.04 |
| HTTP Keep-Alive: HTTP/1.1 지속 연결과 HTTP/2·3 연결 재사용 (0) | 2024.12.03 |
댓글