본문 바로가기

실시간 웹 통신

폴링·SSE·WebSocket의 통신 방향과 연결 비용을 비교하고 알림과 채팅에 맞는 방식을 선택합니다.

일반적인 HTTP API 조회는 브라우저가 먼저 요청하고 서버의 응답 본문을 받아 완료합니다. HTTP에서도 응답을 오래 유지해 조금씩 보내는 스트리밍이 가능하며, SSE가 그 예입니다.

채팅, 알림, 진행률처럼 서버의 새 상태를 바로 받아야 하는 화면에서는 다른 통신 전략이 필요합니다.

이 절에서는 폴링, Server-Sent Events(SSE), WebSocket의 차이를 이해하고 상황에 맞는 방식을 선택합니다.

아래 다이어그램은 허용 지연 시간과 통신 방향에 따라 실시간 방식을 고르는 기준을 보여줍니다.

폴링·SSE·WebSocket은 허용 지연과 통신 방향으로 비교합니다

REALTIME TRANSPORT

폴링·SSE·WebSocket은 허용 지연과 통신 방향으로 비교합니다

새 상태를 언제 알아야 하는지와 누가 먼저 메시지를 보내야 하는지 정합니다. 이 절의 알림 예제는 폴링과 SSE, 양방향 채팅 예제는 WebSocket으로 비교할 수 있습니다.

Polling · 반복 요청

브라우저 → 서버: 새 알림?
브라우저 ← 서버: 응답

예제는10초마다 HTTP 요청합니다. 변화가 없어도 요청하고, 주기를 줄이면 요청량이 늘어납니다.

탭이 숨겨지면 중단하고 다시 보이면 즉시 갱신합니다. 일반 HTTP 요청의 오류 처리가 필요합니다.

SSE · 서버 → 브라우저

EventSource 연결 요청
서버 → 브라우저: 이벤트

하나의 HTTP 스트림에서 텍스트 이벤트를 받습니다. 알림·진행률처럼 서버 전달이 중심인 기능에 적합합니다.

EventSource는 기본 재접속을 처리하지만 누락 이벤트 재전송은 서버가 구현해야 합니다.

WebSocket · 양방향

브라우저 ↔ 서버
연결 뒤 메시지 교환

채팅·협업처럼 양쪽에서 메시지를 자주 보내는 기능에 적합합니다. 운영 연결은 wss://로 보호합니다.

자동 재접속은 직접 구현합니다. 메시지마다 입력·권한을 검사하고 누락 복구도 설계합니다.

세 방식 모두: 연결과 데이터 복구를 검증

인증, 프록시 타임아웃, 동시 연결 수, 서버 간 메시지 전달과 배포 중 종료를 확인합니다. 마지막 이벤트·메시지 ID와 중복 처리 정책으로 끊김 이후 상태를 맞춥니다.

즉시 갱신이 필요하지 않다면

탐색·포커스·사용자 새로고침 때 일반 HTTP로 재검증하는 방법도 가능합니다. 필요한 지연 범위를 충족하는지 먼저 판단합니다.

Long polling도 가능한 선택

서버가 이벤트나 타임아웃까지 HTTP 응답을 보류하고, 응답 뒤 클라이언트가 다시 요청합니다. 대기 연결과 프록시 제한을 함께 확인합니다.


실시간의 의미

웹에서 실시간은 변화가 생긴 직후 사용자가 알아야 한다는 요구사항을 뜻합니다.

모든 기능이 밀리초 단위의 양방향 연결을 필요로 하는 것은 아닙니다.

게시글 목록을 1분마다 갱신해도 충분하다면 일반 재검증이 가장 단순합니다.

알림을 몇 초 안에 받아야 한다면 폴링이나 SSE를 고려할 수 있습니다.

사용자끼리 메시지를 주고받는 채팅은 WebSocket이 자연스럽습니다.

기술을 고르기 전에 허용 지연 시간과 통신 방향을 먼저 정합니다. HTTP 요청·응답, JSON, async/await, 이벤트 리스너와 타이머를 알고 있으면 예제를 따라가기 쉽습니다. 아래 클라이언트 코드는 각각 다른 방식을 보여주는 조각이며, 화면을 그리는 renderNotifications, renderNotification, renderMessage, showConnectionState는 애플리케이션이 구현할 함수입니다.


폴링

폴링은 브라우저가 일정한 간격으로 서버에 새 데이터가 있는지 묻는 방식입니다.

let lastId = 42; // 이미 화면에 반영한 마지막 알림 ID
let requestInFlight = false;

async function refreshNotifications() {
  if (document.hidden || requestInFlight) return;
  requestInFlight = true;

  try {
    const response = await fetch(`/api/notifications?after=${lastId}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const notifications = await response.json();
    if (!Array.isArray(notifications)) throw new Error('알림 배열이 필요합니다');

    renderNotifications(notifications);
    if (notifications.length > 0) {
      lastId = notifications[notifications.length - 1].id;
    }
    showConnectionState('갱신 완료');
  } catch (error) {
    showConnectionState('알림을 갱신하지 못했습니다');
  } finally {
    requestInFlight = false;
  }
}

let timerId;

function startPolling() {
  if (document.hidden || timerId !== undefined) return;
  void refreshNotifications();
  timerId = setInterval(refreshNotifications, 10_000);
}

function stopPolling() {
  if (timerId === undefined) return;
  clearInterval(timerId);
  timerId = undefined;
}

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    stopPolling();
  } else {
    startPolling();
  }
});

startPolling();

일반 HTTP API만 있으면 구현할 수 있고 연결 복구도 단순합니다.

변화가 없어도 요청을 반복하므로 주기가 짧을수록 서버와 네트워크 비용이 늘어납니다.

탭이 보이지 않을 때는 요청을 줄이거나 중지합니다.

탭이 다시 보일 때 startPolling()은 즉시 갱신을 시도하고 타이머를 재개합니다. 이전 요청이 아직 진행 중이면 그 결과를 기다리고 다음 주기에 다시 조회합니다. requestInFlight는 느린 요청과 새 요청의 겹침을 막고, finally는 실패해도 다음 시도를 허용합니다. clearInterval()은 새 타이머 호출을 멈추며 이미 시작한 HTTP 요청을 취소하지는 않습니다.

이 API의 계약은 after보다 큰 숫자 ID의 알림을 ID 오름차순 배열로 반환하는 것입니다. 42는 예제의 초기 완료 ID이며, 화면에 반영한 뒤 마지막 ID를 갱신해 같은 알림을 계속 받지 않습니다. 운영에서는 응답의 각 필드도 검증하고, 권한 오류·네트워크 실패에 맞춰 재시도 정책을 정합니다. Fetch 문서가 설명하듯 HTTP 404·500 응답 자체는 fetch()를 거부하지 않으므로 response.ok를 확인해야 합니다.

폴링은 주기가 짧을수록 요청 비용은 늘고 데이터 지연은 줄어드는 상충 관계를 갖습니다. 10초는 타이머의 목표 간격이며 네트워크 지연, 진행 중인 요청, 브라우저의 타이머 지연에 따라 실제 갱신은 늦어질 수 있습니다.


SSE로 서버 알림 받기

SSE는 하나의 HTTP 연결을 유지하면서 서버가 브라우저로 텍스트 이벤트를 계속 보내는 방식입니다.

통신 방향은 서버에서 브라우저로 한쪽입니다.

브라우저의 EventSource가 연결과 기본 재접속을 관리합니다.

client.js
const events = new EventSource('/api/notifications/stream');

events.addEventListener('notification', (event) => {
  const notification = JSON.parse(event.data);
  renderNotification(notification);
});

events.onopen = () => {
  showConnectionState('연결됨');
};

events.onerror = () => {
  showConnectionState(events.readyState === EventSource.CLOSED
    ? '연결 종료'
    : '재연결 중');
};

function stopNotifications() {
  events.close();
}

화면이 이 구독을 더 이상 사용하지 않거나 로그아웃하면 stopNotifications()를 호출합니다. EventSource 표준에서 CLOSED는 재접속을 시도하지 않는 상태입니다. 기본 재접속이 모든 종료에 적용되는 것은 아니며 서버가 HTTP 204로 재접속을 중단할 수도 있습니다.

서버는 text/event-stream 형식으로 응답합니다. 아래 서버는 Express가 설치된 별도 학습 환경에서 실행하는 교육용 생산자이며, /api를 이 서버로 보내는 프록시나 같은 출처의 클라이언트가 필요합니다. 실제 개인 알림을 공개하는 운영 서버로 그대로 배포하지 않습니다.

server.mjs
import express from 'express';

const app = express();

app.get('/api/notifications/stream', (request, response) => {
  response.setHeader('Content-Type', 'text/event-stream');
  response.setHeader('Cache-Control', 'no-cache');
  response.setHeader('Connection', 'keep-alive');
  response.flushHeaders();

  const send = (notification) => {
    response.write(`id: ${notification.id}\n`);
    response.write('event: notification\n');
    response.write(`data: ${JSON.stringify(notification)}\n\n`);
  };

  const heartbeat = setInterval(() => {
    response.write(': keep-alive\n\n');
  }, 15_000);

  const requestedLastId = Number(request.get('last-event-id') ?? 0);
  let notificationId = Number.isSafeInteger(requestedLastId) && requestedLastId >= 0
    ? requestedLastId
    : 0;
  const notificationTimer = setInterval(() => {
    notificationId += 1;
    send({
      id: notificationId,
      message: `새 알림 ${notificationId}`,
      createdAt: new Date().toISOString(),
    });
  }, 10_000);

  response.on('close', () => {
    clearInterval(heartbeat);
    clearInterval(notificationTimer);
  });
});

app.listen(4100);

SSE의 id: 필드는 브라우저가 재접속할 때 Last-Event-ID 헤더로 전달합니다.

위 예제는 연결마다 다음 숫자로 가상의 알림을 만드는 최소 흐름입니다. 여러 사용자에게 공유되는 실제 이벤트 로그를 저장하거나 끊긴 동안의 알림을 재생하지 않습니다. 운영 서버는 이벤트를 저장하고 해당 ID 이후의 누락 이벤트를 조회해야 실제 복구가 됩니다. Last-Event-ID는 전송 계층의 수신 기준이며 화면 반영 성공을 증명하는 응답은 아닙니다. 처리 실패까지 복구해야 한다면 애플리케이션의 완료 ID·확인 정책도 설계합니다.

각 이벤트는 빈 줄로 끝납니다.

콜론으로 시작하는 줄은 연결을 유지하기 위한 주석 이벤트입니다.

예제의 notificationTimer는 send()가 실제 이벤트를 전송하는 흐름을 보여주기 위한 교육용 알림 생산자입니다.

운영 코드에서는 이 타이머 대신 메시지 브로커나 데이터베이스 변경 구독에서 알림을 받아 send()를 호출합니다.

사용자가 연결을 닫으면 하트비트, 알림 생산자와 외부 구독을 모두 정리합니다.

아래 다이어그램은 SSE 연결에서 이벤트·하트비트·재접속이 이어지는 흐름을 정리합니다.

실시간 연결·인증·메시지 시퀀스 — 상태 추적

실제 SSE 예제의 연결·notification id/data·주석 heartbeat·종료 타이머 정리와 WebSocket 메시지 계약을 구분한다. 현재 연결/마지막 ID/자원 상태로 같은 수신 사례를 추적할 가치가 있다.

SSE의 이벤트·하트비트와 WebSocket 메시지를 구별합니다

MESSAGE FLOW

SSE의 이벤트·하트비트와 WebSocket 메시지를 구별합니다

SSE 예제는 EventSource 요청에 text/event-stream으로 응답하고, 이름이 notification인 이벤트를 전달합니다. WebSocket은 별도 연결에서 join·chat 메시지를 양방향으로 교환합니다.

SSE: 연결 요청과 서버 사건

SSE: 연결 요청과 서버 사건notification은 10초 생산자의 사용자 사건이고 15초 주석 heartbeat와 다릅니다. 종료 시 알림·하트비트 타이머를 모두 정리합니다.브라우저서버EventSource GETtext/event-streamnotification주석 heartbeat연결 종료
SSE: 연결 요청과 서버 사건notification은 10초 생산자의 사용자 사건이고 15초 주석 heartbeat와 다릅니다. 종료 시 알림·하트비트 타이머를 모두 정리합니다.브라우저서버EventSource GETtext/event-streamnotification주석 heartbeat연결 종료

notification은 10초 생산자의 사용자 사건이고 15초 주석 heartbeat와 다릅니다. 종료 시 알림·하트비트 타이머를 모두 정리합니다.

WebSocket: 두 끝이 보내는 메시지

WebSocket: 두 끝이 보내는 메시지SSE와 별도 연결입니다. 실제 SSE 예제에는 운영용 인증·인가 전체가 없고, 운영에서는 연결 권한과 각 입력 메시지의 종류·길이·권한을 확인합니다.브라우저서버별도 WS 연결join / chat메시지 응답
WebSocket: 두 끝이 보내는 메시지SSE와 별도 연결입니다. 실제 SSE 예제에는 운영용 인증·인가 전체가 없고, 운영에서는 연결 권한과 각 입력 메시지의 종류·길이·권한을 확인합니다.브라우저서버별도 WS 연결join / chat메시지 응답

SSE와 별도 연결입니다. 실제 SSE 예제에는 운영용 인증·인가 전체가 없고, 운영에서는 연결 권한과 각 입력 메시지의 종류·길이·권한을 확인합니다.

원래 설명과 상세 기준

SSE · 연결과 알림

GET /api/notifications/stream
← Content-Type: text/event-stream
← id: 43
← event: notification
← data: {"id":43,...}
← 빈 줄로 이벤트 끝

서버의 교육용 생산자는10초마다 send()합니다. 브라우저는 notification 리스너에서 event.data를 JSON으로 읽습니다.

SSE · 생존과 종료

: keep-alive

서버의15초 주석 하트비트는 notification 이벤트가 아닙니다. 연결 종료 시 하트비트·알림 생산자 타이머를 모두 정리합니다.

재접속 시 Last-Event-ID를 전달할 수 있습니다. 예제는 다음 ID를 이어가며, 실제 누락 이벤트를 재전송하려면 서버 저장과 조회가 필요합니다.

WebSocket · 보내기와 받기

open → send({type:"join",...})
OPEN일 때 send({type:"chat",...})
message → JSON.parse(event.data)

브라우저와 서버 모두 연결 뒤 메시지를 보낼 수 있습니다. 이 연결의 메시지와 SSE의 data/event/id 행 형식은 서로 다릅니다.

공통 · 인증·메시지 검증

서버는 연결 시 세션·구독 권한을 검사하고, 사용자 입력의 종류·길이·권한을 다시 확인합니다. 작성자 신원은 클라이언트가 보낸 사용자 ID 대신 인증된 연결에서 정합니다.

SSE 예제에는 이 운영용 인증·인가 전체가 구현되어 있지 않습니다. 연결 성공과 검증 완료를 구분합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 뒤의 연결·알림 id 43·주석 heartbeat·timer 정리는 본문 SSE 예제의 수명 추적입니다. WebSocket은 첫 장면에서 전달 계약만 독립 비교하며 SSE 처리의 다음 단계가 아닙니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 설명서버의 교육용 생산자는 10초마다 send()합니다. 브라우저는 notification 리스너에서 event.data를 JSON으로 읽습니다.
원도표 설명서버의 15초 주석 하트비트는 notification 이벤트가 아닙니다. 연결 종료 시 하트비트·알림 생산자 타이머를 모두 정리합니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
SSE 연결 · WS 계약 비교SSE: 서버→client 사건 / WebSocket: 양방향 메시지추적할 SSE 연결을 열고 별도 WebSocket의 양방향 계약과 비교한다두 API를 같은 양방향 스트림으로 읽지 않는다
SSE · 현재 알림알림: id 43 / heartbeat: 알림 아님
  • 알림: id43
알림 id 43을 업무 사건으로 처리한다연결 유지 신호와 사용자 알림을 구분한다
SSE · 소유자 종료연결: 종료 / timer: 해제닫기와 timer 해제를 수행한다연결 소유자가 연결과 반복 자원을 함께 정리한다

실시간 연결·인증·메시지 시퀀스 — 상태 추적

실제 SSE 예제의 연결·notification id/data·주석 heartbeat·종료 타이머 정리와 WebSocket 메시지 계약을 구분한다. 현재 연결/마지막 ID/자원 상태로 같은 수신 사례를 추적할 가치가 있다.

SSE의 이벤트·하트비트와 WebSocket 메시지를 구별합니다

MESSAGE FLOW

SSE의 이벤트·하트비트와 WebSocket 메시지를 구별합니다

SSE 예제는 EventSource 요청에 text/event-stream으로 응답하고, 이름이 notification인 이벤트를 전달합니다. WebSocket은 별도 연결에서 join·chat 메시지를 양방향으로 교환합니다.

SSE: 연결 요청과 서버 사건

SSE: 연결 요청과 서버 사건notification은 10초 생산자의 사용자 사건이고 15초 주석 heartbeat와 다릅니다. 종료 시 알림·하트비트 타이머를 모두 정리합니다.브라우저서버EventSource GETtext/event-streamnotification주석 heartbeat연결 종료
SSE: 연결 요청과 서버 사건notification은 10초 생산자의 사용자 사건이고 15초 주석 heartbeat와 다릅니다. 종료 시 알림·하트비트 타이머를 모두 정리합니다.브라우저서버EventSource GETtext/event-streamnotification주석 heartbeat연결 종료

notification은 10초 생산자의 사용자 사건이고 15초 주석 heartbeat와 다릅니다. 종료 시 알림·하트비트 타이머를 모두 정리합니다.

WebSocket: 두 끝이 보내는 메시지

WebSocket: 두 끝이 보내는 메시지SSE와 별도 연결입니다. 실제 SSE 예제에는 운영용 인증·인가 전체가 없고, 운영에서는 연결 권한과 각 입력 메시지의 종류·길이·권한을 확인합니다.브라우저서버별도 WS 연결join / chat메시지 응답
WebSocket: 두 끝이 보내는 메시지SSE와 별도 연결입니다. 실제 SSE 예제에는 운영용 인증·인가 전체가 없고, 운영에서는 연결 권한과 각 입력 메시지의 종류·길이·권한을 확인합니다.브라우저서버별도 WS 연결join / chat메시지 응답

SSE와 별도 연결입니다. 실제 SSE 예제에는 운영용 인증·인가 전체가 없고, 운영에서는 연결 권한과 각 입력 메시지의 종류·길이·권한을 확인합니다.

원래 설명과 상세 기준

SSE · 연결과 알림

GET /api/notifications/stream
← Content-Type: text/event-stream
← id: 43
← event: notification
← data: {"id":43,...}
← 빈 줄로 이벤트 끝

서버의 교육용 생산자는10초마다 send()합니다. 브라우저는 notification 리스너에서 event.data를 JSON으로 읽습니다.

SSE · 생존과 종료

: keep-alive

서버의15초 주석 하트비트는 notification 이벤트가 아닙니다. 연결 종료 시 하트비트·알림 생산자 타이머를 모두 정리합니다.

재접속 시 Last-Event-ID를 전달할 수 있습니다. 예제는 다음 ID를 이어가며, 실제 누락 이벤트를 재전송하려면 서버 저장과 조회가 필요합니다.

WebSocket · 보내기와 받기

open → send({type:"join",...})
OPEN일 때 send({type:"chat",...})
message → JSON.parse(event.data)

브라우저와 서버 모두 연결 뒤 메시지를 보낼 수 있습니다. 이 연결의 메시지와 SSE의 data/event/id 행 형식은 서로 다릅니다.

공통 · 인증·메시지 검증

서버는 연결 시 세션·구독 권한을 검사하고, 사용자 입력의 종류·길이·권한을 다시 확인합니다. 작성자 신원은 클라이언트가 보낸 사용자 ID 대신 인증된 연결에서 정합니다.

SSE 예제에는 이 운영용 인증·인가 전체가 구현되어 있지 않습니다. 연결 성공과 검증 완료를 구분합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 뒤의 연결·알림 id 43·주석 heartbeat·timer 정리는 본문 SSE 예제의 수명 추적입니다. WebSocket은 첫 장면에서 전달 계약만 독립 비교하며 SSE 처리의 다음 단계가 아닙니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 설명서버의 교육용 생산자는 10초마다 send()합니다. 브라우저는 notification 리스너에서 event.data를 JSON으로 읽습니다.
원도표 설명서버의 15초 주석 하트비트는 notification 이벤트가 아닙니다. 연결 종료 시 하트비트·알림 생산자 타이머를 모두 정리합니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
SSE 연결 · WS 계약 비교SSE: 서버→client 사건 / WebSocket: 양방향 메시지추적할 SSE 연결을 열고 별도 WebSocket의 양방향 계약과 비교한다두 API를 같은 양방향 스트림으로 읽지 않는다
SSE · 현재 알림알림: id 43 / heartbeat: 알림 아님
  • 알림: id43
알림 id 43을 업무 사건으로 처리한다연결 유지 신호와 사용자 알림을 구분한다
SSE · 소유자 종료연결: 종료 / timer: 해제닫기와 timer 해제를 수행한다연결 소유자가 연결과 반복 자원을 함께 정리한다

WebSocket으로 양방향 통신하기

WebSocket은 연결을 만든 뒤 브라우저와 서버가 양쪽에서 메시지를 보낼 수 있습니다.

채팅, 협업 편집, 실시간 게임처럼 양방향 메시지가 잦은 기능에 적합합니다.

client.js
const socket = new WebSocket('wss://example.com/chat');

socket.addEventListener('open', () => {
  socket.send(JSON.stringify({ type: 'join', roomId: 'general' }));
});

socket.addEventListener('message', (event) => {
  const message = JSON.parse(event.data);
  renderMessage(message);
});

function sendChat(content) {
  if (socket.readyState !== WebSocket.OPEN) {
    return;
  }

  socket.send(JSON.stringify({ type: 'chat', content }));
}

ws://는 암호화하지 않은 연결이고 wss://는 TLS로 보호한 연결입니다.

운영 환경에서는 HTTPS와 함께 wss://를 사용합니다.

브라우저가 보낸 JSON은 신뢰할 수 없는 입력이므로 서버에서 타입, 길이와 권한을 검증합니다.

앞의 실시간 연결·메시지 흐름은 WebSocket 연결 수립과 양방향 메시지 교환도 함께 보여줍니다.


메시지 계약 설계

실시간 연결에서도 메시지의 종류와 데이터 모양을 명시해야 합니다.

{
  "type": "chat",
  "messageId": "m-1024",
  "roomId": "general",
  "content": "안녕하세요",
  "sentAt": "2026-07-18T10:30:00.000Z"
}

type은 수신 측이 메시지 처리기를 선택하는 기준입니다.

messageId는 재전송이나 중복 수신을 구분하는 데 사용합니다.

sentAt은 문자열 형식과 시간대 기준을 고정합니다.

서버는 클라이언트가 보낸 사용자 ID를 그대로 믿지 않고 인증된 연결 정보에서 작성자를 결정합니다.

메시지 봉투에서는 식별자·종류·본문·시각의 역할을 명확히 구분해야 합니다.


연결 끊김과 재접속

모바일 네트워크와 절전 상태에서는 연결이 자주 끊길 수 있습니다.

실시간 기능은 정상 연결뿐 아니라 재접속 흐름을 함께 설계해야 합니다.

WebSocket에는 자동 재접속이 없으므로 아래 예제는 상한이 있는 단계별 대기 시간을 적용합니다. 운영 시에는 지수 백오프와 jitter를 함께 설계합니다.

const delays = [1_000, 2_000, 5_000, 10_000, 30_000];
let attempt = 0;

function scheduleReconnect(connect) {
  const delay = delays[Math.min(attempt, delays.length - 1)];
  attempt += 1;
  setTimeout(connect, delay);
}

연결에 성공하면 attempt를 0으로 되돌립니다.

재접속 뒤 놓친 메시지를 가져올 수 있도록 마지막으로 처리한 이벤트 ID나 서버가 정한 순서를 전달합니다. 시각만 사용하면 같은 시각의 여러 메시지가 생길 수 있으므로 동률을 구별할 식별자와 조회 규칙이 필요합니다. 중복 ID는 다시 적용하지 않고, 복구 조회와 새 실시간 메시지 사이에 누락 구간이 생기지 않게 합니다. 로그 보관 기간을 넘긴 경우에는 권한을 확인한 최신 전체 상태를 다시 읽는 정책도 필요합니다.

재접속이 곧 데이터 복구를 의미하는 것은 아닙니다.

아래 다이어그램은 연결 끊김부터 백오프·재접속·누락 메시지 복구까지의 상태 변화를 보여줍니다.

실시간 재연결·재생·복구 상태 — 상태 추적

마지막 처리 ID·끊김·대기·새 연결·누락 조회·중복 제거·수신 재개가 실제로 다른 상태다. 본문 미구현 replay를 개념 모델로 명시하고 재연결 성공과 복구 완료를 구별한다.

재접속 뒤 누락 메시지를 복구해야 같은 상태로 돌아갑니다

RECONNECT & RECOVERY

재접속 뒤 누락 메시지를 복구해야 같은 상태로 돌아갑니다

연결을 다시 여는 일과 놓친 데이터를 메우는 일은 다릅니다. 마지막으로 반영한 ID를 기준으로 재전송·중복 제거를 수행하고, 다시 실시간 수신으로 전환합니다.

재연결과 누락 복구는 별도의 계약

재연결과 누락 복구는 별도의 계약그림의 로그 조회·재생·중복 제거는 운영 복구 계약입니다. 교재 SSE 코드는 ID 다음 숫자의 교육용 사건을 생성하며 실제 누락 로그 저장·재전송을 구현하지 않습니다. 재연결만으로 exactly-once를 보장하지 않습니다.끊김재시도복구 계약정상정상 수신처리 완료 ID 저장끊김 / 재시도 대기SSE와 WS 방식 구별새 연결인증 / 구독 권한저장 로그 조회마지막 완료 ID 이후ID 중복 제거순서대로 반영실시간 수신 재개완료 ID 전진
재연결과 누락 복구는 별도의 계약그림의 로그 조회·재생·중복 제거는 운영 복구 계약입니다. 교재 SSE 코드는 ID 다음 숫자의 교육용 사건을 생성하며 실제 누락 로그 저장·재전송을 구현하지 않습니다. 재연결만으로 exactly-once를 보장하지 않습니다.끊김재시도복구 계약정상정상 수신처리 완료 ID저장끊김 / 재시도대기SSE와 WS 방식구별새 연결인증 / 구독권한저장 로그 조회마지막 완료 ID이후ID 중복 제거순서대로 반영실시간 수신재개완료 ID 전진

그림의 로그 조회·재생·중복 제거는 운영 복구 계약입니다. 교재 SSE 코드는 ID 다음 숫자의 교육용 사건을 생성하며 실제 누락 로그 저장·재전송을 구현하지 않습니다. 재연결만으로 exactly-once를 보장하지 않습니다.

원래 설명과 상세 기준
  1. 정상 수신 · 마지막 완료 ID 보관

    메시지를 검증해 화면 상태에 반영한 뒤 마지막으로 처리한 ID나 순서를 저장합니다.

  2. 끊김 감지 · 재시도 대기

    WebSocket 예제는1·2·5·10·30초의 상한 있는 단계별 대기를 사용합니다. 운영에서는 지수 백오프·jitter를 검토합니다. SSE의 기본 재접속은 EventSource가 처리합니다.

  3. 새 연결 · 인증과 구독 권한 확인

    새 연결에 유효한 세션과 채널 권한을 확인합니다. 인증·권한 문제라면 필요한 사용자 조치를 안내하고 무한 재시도를 피합니다.

  4. 누락 복구 · 마지막 ID 이후 조회

    서버에 저장된 이벤트에서 마지막 완료 ID 이후를 가져옵니다. SSE Last-Event-ID는 기준을 전달하는 수단이며, 그 값만으로 서버에 누락 데이터가 생기지는 않습니다.

  5. 중복 제거 · 순서대로 반영

    messageId로 중복을 구분하고 상태에 반영한 뒤 완료 ID를 전진시킵니다. 로그 보관 범위를 벗어났다면 전체 상태를 다시 읽는 재동기화 정책도 필요합니다.

  6. 실시간 수신 재개

    복구 조회와 새 메시지 수신 사이에 빈 구간이 생기지 않게 설계합니다. WebSocket 연결 성공 시 예제의 attempt를0으로 되돌립니다.

교재 코드가 보여주는 범위

SSE 서버는 전달받은 ID 다음 숫자로 교육용 이벤트를 생성합니다. 끊긴 동안의 실제 이벤트 저장·재전송은 구현되어 있지 않습니다. WebSocket 대기 함수도 연결·복구 구현 전체가 아닙니다.

운영에서 확인할 범위

ID의 의미·로그 보관 기간·순서·중복·재동기화 경로를 정합니다. 서버 재시작, 여러 인스턴스, 재배포 중에도 이 복구 계약이 유지되는지 확인합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 원래 본문의 구현과 도표가 설명한 설계 보강을 분리한다. 서사에 포함한 확장 동작은 assumptions에 명시하고 이미 구현된 안전장치로 주장하지 않는다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석정상 수신 · 마지막 완료 ID 보관: 메시지를 검증해 화면 상태에 반영한 뒤 마지막으로 처리한 ID나 순서를 저장합니다.
원도표 설명WebSocket 예제는 1·2·5·10·30초의 상한 있는 단계별 대기를 사용합니다. 운영에서는 지수 백오프·jitter를 검토합니다. SSE의 기본 재접속은 EventSource가 처리합니다.
원도표 조건·주석끊김 감지 · 재시도 대기: WebSocket 예제는 1·2·5·10·30초의 상한 있는 단계별 대기를 사용합니다. 운영에서는 지수 백오프·jitter를 검토합니다. SSE의 기본 재접속은 EventSource가 처리합니다.
원도표 조건·주석새 연결 · 인증과 구독 권한 확인: 새 연결에 유효한 세션과 채널 권한을 확인합니다. 인증·권한 문제라면 필요한 사용자 조치를 안내하고 무한 재시도를 피합니다.
원도표 조건·주석누락 복구 · 마지막 ID 이후 조회: 서버에 저장된 이벤트에서 마지막 완료 ID 이후를 가져옵니다. SSE Last-Event-ID는 기준을 전달하는 수단이며, 그 값만으로 서버에 누락 데이터가 생기지는 않습니다.
원도표 조건·주석중복 제거 · 순서대로 반영: messageId로 중복을 구분하고 상태에 반영한 뒤 완료 ID를 전진시킵니다. 로그 보관 범위를 벗어났다면 전체 상태를 다시 읽는 재동기화 정책도 필요합니다.
원도표 설명복구 조회와 새 메시지 수신 사이에 빈 구간이 생기지 않게 설계합니다. WebSocket 연결 성공 시 예제의 attempt를 0으로 되돌립니다.
원도표 조건·주석실시간 수신 재개: 복구 조회와 새 메시지 수신 사이에 빈 구간이 생기지 않게 설계합니다. WebSocket 연결 성공 시 예제의 attempt를 0으로 되돌립니다.
원도표 설명교재 코드가 보여 주는 범위
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
끊기기 전마지막 처리: 기록된 ID / 연결: 열림처리한 마지막 알림 ID를 기록한다수신 ID와 처리 완료 위치를 구분한다
재연결 요청연결: 새 연결 / replay: 별도 서버 계약마지막 위치를 보내고 server replay를 요청한다본문 client 연결만으로 server replay가 구현되지는 않는다
재전달 판정새 ID: 처리 후 위치 갱신 / 중복 ID: 중복 처리 안 함누락 사건은 처리하고 중복 ID는 건너뛴다재연결만으로 exactly-once가 보장되지는 않는다

실시간 재연결·재생·복구 상태 — 상태 추적

마지막 처리 ID·끊김·대기·새 연결·누락 조회·중복 제거·수신 재개가 실제로 다른 상태다. 본문 미구현 replay를 개념 모델로 명시하고 재연결 성공과 복구 완료를 구별한다.

재접속 뒤 누락 메시지를 복구해야 같은 상태로 돌아갑니다

RECONNECT & RECOVERY

재접속 뒤 누락 메시지를 복구해야 같은 상태로 돌아갑니다

연결을 다시 여는 일과 놓친 데이터를 메우는 일은 다릅니다. 마지막으로 반영한 ID를 기준으로 재전송·중복 제거를 수행하고, 다시 실시간 수신으로 전환합니다.

재연결과 누락 복구는 별도의 계약

재연결과 누락 복구는 별도의 계약그림의 로그 조회·재생·중복 제거는 운영 복구 계약입니다. 교재 SSE 코드는 ID 다음 숫자의 교육용 사건을 생성하며 실제 누락 로그 저장·재전송을 구현하지 않습니다. 재연결만으로 exactly-once를 보장하지 않습니다.끊김재시도복구 계약정상정상 수신처리 완료 ID 저장끊김 / 재시도 대기SSE와 WS 방식 구별새 연결인증 / 구독 권한저장 로그 조회마지막 완료 ID 이후ID 중복 제거순서대로 반영실시간 수신 재개완료 ID 전진
재연결과 누락 복구는 별도의 계약그림의 로그 조회·재생·중복 제거는 운영 복구 계약입니다. 교재 SSE 코드는 ID 다음 숫자의 교육용 사건을 생성하며 실제 누락 로그 저장·재전송을 구현하지 않습니다. 재연결만으로 exactly-once를 보장하지 않습니다.끊김재시도복구 계약정상정상 수신처리 완료 ID저장끊김 / 재시도대기SSE와 WS 방식구별새 연결인증 / 구독권한저장 로그 조회마지막 완료 ID이후ID 중복 제거순서대로 반영실시간 수신재개완료 ID 전진

그림의 로그 조회·재생·중복 제거는 운영 복구 계약입니다. 교재 SSE 코드는 ID 다음 숫자의 교육용 사건을 생성하며 실제 누락 로그 저장·재전송을 구현하지 않습니다. 재연결만으로 exactly-once를 보장하지 않습니다.

원래 설명과 상세 기준
  1. 정상 수신 · 마지막 완료 ID 보관

    메시지를 검증해 화면 상태에 반영한 뒤 마지막으로 처리한 ID나 순서를 저장합니다.

  2. 끊김 감지 · 재시도 대기

    WebSocket 예제는1·2·5·10·30초의 상한 있는 단계별 대기를 사용합니다. 운영에서는 지수 백오프·jitter를 검토합니다. SSE의 기본 재접속은 EventSource가 처리합니다.

  3. 새 연결 · 인증과 구독 권한 확인

    새 연결에 유효한 세션과 채널 권한을 확인합니다. 인증·권한 문제라면 필요한 사용자 조치를 안내하고 무한 재시도를 피합니다.

  4. 누락 복구 · 마지막 ID 이후 조회

    서버에 저장된 이벤트에서 마지막 완료 ID 이후를 가져옵니다. SSE Last-Event-ID는 기준을 전달하는 수단이며, 그 값만으로 서버에 누락 데이터가 생기지는 않습니다.

  5. 중복 제거 · 순서대로 반영

    messageId로 중복을 구분하고 상태에 반영한 뒤 완료 ID를 전진시킵니다. 로그 보관 범위를 벗어났다면 전체 상태를 다시 읽는 재동기화 정책도 필요합니다.

  6. 실시간 수신 재개

    복구 조회와 새 메시지 수신 사이에 빈 구간이 생기지 않게 설계합니다. WebSocket 연결 성공 시 예제의 attempt를0으로 되돌립니다.

교재 코드가 보여주는 범위

SSE 서버는 전달받은 ID 다음 숫자로 교육용 이벤트를 생성합니다. 끊긴 동안의 실제 이벤트 저장·재전송은 구현되어 있지 않습니다. WebSocket 대기 함수도 연결·복구 구현 전체가 아닙니다.

운영에서 확인할 범위

ID의 의미·로그 보관 기간·순서·중복·재동기화 경로를 정합니다. 서버 재시작, 여러 인스턴스, 재배포 중에도 이 복구 계약이 유지되는지 확인합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 원래 본문의 구현과 도표가 설명한 설계 보강을 분리한다. 서사에 포함한 확장 동작은 assumptions에 명시하고 이미 구현된 안전장치로 주장하지 않는다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석정상 수신 · 마지막 완료 ID 보관: 메시지를 검증해 화면 상태에 반영한 뒤 마지막으로 처리한 ID나 순서를 저장합니다.
원도표 설명WebSocket 예제는 1·2·5·10·30초의 상한 있는 단계별 대기를 사용합니다. 운영에서는 지수 백오프·jitter를 검토합니다. SSE의 기본 재접속은 EventSource가 처리합니다.
원도표 조건·주석끊김 감지 · 재시도 대기: WebSocket 예제는 1·2·5·10·30초의 상한 있는 단계별 대기를 사용합니다. 운영에서는 지수 백오프·jitter를 검토합니다. SSE의 기본 재접속은 EventSource가 처리합니다.
원도표 조건·주석새 연결 · 인증과 구독 권한 확인: 새 연결에 유효한 세션과 채널 권한을 확인합니다. 인증·권한 문제라면 필요한 사용자 조치를 안내하고 무한 재시도를 피합니다.
원도표 조건·주석누락 복구 · 마지막 ID 이후 조회: 서버에 저장된 이벤트에서 마지막 완료 ID 이후를 가져옵니다. SSE Last-Event-ID는 기준을 전달하는 수단이며, 그 값만으로 서버에 누락 데이터가 생기지는 않습니다.
원도표 조건·주석중복 제거 · 순서대로 반영: messageId로 중복을 구분하고 상태에 반영한 뒤 완료 ID를 전진시킵니다. 로그 보관 범위를 벗어났다면 전체 상태를 다시 읽는 재동기화 정책도 필요합니다.
원도표 설명복구 조회와 새 메시지 수신 사이에 빈 구간이 생기지 않게 설계합니다. WebSocket 연결 성공 시 예제의 attempt를 0으로 되돌립니다.
원도표 조건·주석실시간 수신 재개: 복구 조회와 새 메시지 수신 사이에 빈 구간이 생기지 않게 설계합니다. WebSocket 연결 성공 시 예제의 attempt를 0으로 되돌립니다.
원도표 설명교재 코드가 보여 주는 범위
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
끊기기 전마지막 처리: 기록된 ID / 연결: 열림처리한 마지막 알림 ID를 기록한다수신 ID와 처리 완료 위치를 구분한다
재연결 요청연결: 새 연결 / replay: 별도 서버 계약마지막 위치를 보내고 server replay를 요청한다본문 client 연결만으로 server replay가 구현되지는 않는다
재전달 판정새 ID: 처리 후 위치 갱신 / 중복 ID: 중복 처리 안 함누락 사건은 처리하고 중복 ID는 건너뛴다재연결만으로 exactly-once가 보장되지는 않는다

인증과 보안

실시간 연결도 일반 API와 같은 인증과 권한 검사가 필요합니다.

연결이 열릴 때 세션을 검증하고 구독할 수 있는 채널인지 확인합니다.

메시지를 받을 때마다 현재 사용자가 해당 작업을 할 수 있는지도 검사합니다.

브라우저 URL의 쿼리 문자열에 장기 액세스 토큰을 넣으면 로그와 방문 기록에 남을 수 있습니다.

쿠키 기반 세션이나 짧게 만료되는 연결용 토큰을 사용할 수 있지만, 만료 시간을 짧게 해도 URL 로그 유출 자체가 없어지지는 않습니다. WebSocket 핸드셰이크에서는 허용된 Origin을 확인하고, 세션 만료·로그아웃 후 연결과 권한을 해제합니다. Origin 검사는 인증을 대신하지 않습니다. OWASP 지침처럼 연결 인증 후에도 메시지별 권한을 확인해야 합니다. 브라우저의 EventSource 생성자에는 임의의 Authorization 헤더를 지정하는 옵션이 없으므로 인증 전달 방식도 선택한 API에 맞춰 설계합니다.

메시지 크기와 전송 빈도를 제한하여 메모리 고갈과 스팸을 막습니다.


방식 선택 기준

변경 빈도가 낮고 지연을 허용할 수 있으면 폴링을 사용합니다.

서버에서 브라우저로 알림과 진행률을 보낼 때는 SSE가 단순합니다.

양방향 메시지가 자주 오가는 채팅과 협업 기능에는 WebSocket을 사용합니다.

연결 수가 많은 서비스에서는 프록시의 타임아웃, 서버 인스턴스 간 메시지 전달과 배포 중 연결 종료를 함께 고려합니다.

실시간 기술은 빠르게 보이는 화면보다 끊김 후에도 상태를 복구하는 설계가 더 중요합니다.

이 장의 실시간 전송 방식 선택 다이어그램은 폴링·SSE·WebSocket의 방향·복구·운영 비용을 한 번에 비교합니다.

직접 연습: 방향·요청 수·누락 복구를 판단한다

  1. 사용자는 보고서 생성을 HTTP POST로 요청합니다. 그 뒤 진행률은 서버가 보내며, 페이지에서 서버로 연속 입력을 보낼 필요가 없습니다. 기존 HTTP 스트림을 지원한다는 조건에서 어떤 방식이 적합한가요? 빠른 양방향 채팅도 같은 방식 하나로 반드시 처리해야 하나요?
풀이

진행률 수신에는 SSE가 적합합니다. 보고서 시작은 일반 POST, 진행률은 서버에서 브라우저로 보내는 SSE로 나눌 수 있습니다. SSE가 단방향이라고 브라우저의 다른 HTTP 요청까지 금지되는 것은 아닙니다. 양방향 메시지가 자주 오가는 채팅은 WebSocket을 별도로 검토합니다. 실제 선택에는 서버·프록시의 스트리밍 지원과 허용 지연도 확인합니다.

  1. 보이는 탭 1,000개가 시각 0초에 즉시 조회하고 이후 10초 간격으로 폴링합니다. 모든 요청은 다음 주기 전에 성공하며 탭 전환·타이머 지연은 없습니다. 0초 이상, 60초 미만에 총 몇 번의 요청이 생기나요? 데이터가 10.1초에 바뀌면 다음 20초 조회까지 대략 얼마나 기다리나요?
풀이

한 탭은 0·10·20·30·40·50초에 6번 요청하므로 총 6,000번입니다. 60초 요청은 지정 범위에 포함되지 않습니다. 변화는 다음 조회까지 약 9.9초를 기다리고 여기에 응답 지연이 더해집니다. 이는 문제의 이상적인 조건에서 계산한 값이며 실제 타이머가 10초 이내 갱신을 보장하는 것은 아닙니다.

  1. 화면은 메시지 42까지 반영했습니다. 연결이 끊긴 동안 서버 로그에 43·44·45가 쌓였고, 재접속 후 43·44·44·45가 전달됩니다. 무엇을 반영하고 완료 ID는 언제 전진시키나요? 서버가 이미 50 이전 로그를 지웠다면 45로 숫자만 바꾸어도 복구된 것인가요?
풀이

43·44·45를 순서대로 한 번씩 반영하고 중복 44는 건너뜁니다. 각 메시지를 검증해 실제 상태에 적용한 뒤 완료 ID를 전진시킵니다. 42 이후 로그가 남아 있지 않으면 빠진 내용을 숫자만으로 재구성할 수 없습니다. 서버가 허용하는 최신 전체 상태를 다시 읽고 그 스냅샷 기준의 커서부터 수신을 이어가야 합니다. 연결 복구, 이벤트 재전송, 중복 제거와 상태 복구는 각각 확인해야 합니다.