[Nexacro N] SSE(Server-Sent Events) 실시간 수신

Nexacro N에서 EventSource API를 이용해 서버 푸시 알림, 실시간 로그, 진행률을 단방향으로 수신하는 SSE 구현 방법과 Dataset 연동 패턴을 설명합니다.

· 4 min read · PALDYN Team

지난 글에서 양방향 WebSocket 연동을 다뤘다. 서버에서 클라이언트로만 데이터를 밀어주면 되는 시나리오—알림, 실시간 로그, 배치 진행률—에는 구현이 훨씬 단순한 SSE(Server-Sent Events)가 더 적합하다. HTTP 인프라를 그대로 사용하고 브라우저가 자동 재연결을 처리해주기 때문이다.

SSE vs WebSocket

두 기술은 목적이 다르다. WebSocket이 양방향 실시간 채널이라면, SSE는 서버가 클라이언트에게 지속적으로 이벤트를 스트리밍하는 단방향 파이프다.

SSE vs WebSocket 비교표

서버 쪽에서도 SSE는 단순 HTTP 엔드포인트로 구현할 수 있어 Nginx, 기존 Spring MVC 필터, 프록시 등 기존 인프라를 변경할 필요가 없다는 장점이 있다.

기본 연결 구현

Nexacro N은 HTML5 런타임이므로 EventSource API를 바로 사용할 수 있다.

var evtSrc = null;

function fn_sseConnect() {
  evtSrc = new EventSource("/sse/alerts");

  // 기본 메시지 수신 (event: 생략된 경우)
  evtSrc.onmessage = function(event) {
    var row = this.ds_notify.addRow();
    this.ds_notify.setColumn(row, "MSG",  event.data);
    this.ds_notify.setColumn(row, "TIME",
      new Date().toLocaleTimeString());
  }.bind(this);

  // 커스텀 이벤트 구독 (event: PRICE)
  evtSrc.addEventListener("PRICE",
    fn_onPrice.bind(this));

  evtSrc.onerror = function() {
    trace("SSE 오류 — 브라우저 자동 재연결 대기");
  };
}

function fn_sseClose() {
  if (evtSrc) { evtSrc.close(); evtSrc = null; }
}

EventSource는 연결이 끊기면 브라우저가 자동으로 재연결을 시도한다(Retry 헤더로 간격 조정 가능). 이 점이 WebSocket과 가장 큰 차이다.

Dataset 연동 패턴

EventSource Dataset 연동 코드

수신 데이터는 JSON 문자열로 전달되는 경우가 많다. event.data를 파싱해 Dataset에 넣는다.

function fn_onPrice(event) {
  var data = JSON.parse(event.data);
  var row = this.ds_price.addRow();
  this.ds_price.setColumn(row, "STOCK", data.stock);
  this.ds_price.setColumn(row, "PRICE", data.price);
  this.ds_price.setColumn(row, "CHANGE", data.change);
}

서버 구현 (Spring Boot)

Spring의 SseEmitter를 사용하면 SSE 스트림을 쉽게 구현할 수 있다.

// Spring Boot SSE 엔드포인트 예시
@GetMapping(value = "/sse/alerts",
    produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter getAlerts() {
  SseEmitter emitter = new SseEmitter(0L); // 무한 타임아웃
  sseService.addEmitter(emitter);
  return emitter;
}

서버에서 이벤트를 보낼 때는 emitter.send(SseEmitter.event().name("PRICE").data(json))처럼 이벤트 이름과 데이터를 함께 전송한다. 클라이언트는 addEventListener("PRICE", handler)로 해당 이벤트만 구독한다.

폼 생명주기 연동

function Form_onload(obj, e) {
  this.fn_sseConnect();
}

function Form_onunload(obj, e) {
  this.fn_sseClose();
}

폼이 닫힐 때 evtSrc.close()를 호출하지 않으면 SSE 연결이 브라우저 세션 내에서 계속 유지된다. 서버의 Emitter 목록에도 dead connection이 쌓이므로 반드시 정리해야 한다.

CORS 설정

SSE 엔드포인트를 다른 도메인에서 요청할 경우 서버에서 Access-Control-Allow-Origin 헤더를 설정해야 한다. 자격증명(쿠키/헤더)이 필요하면 EventSource 생성 시 두 번째 인자로 { withCredentials: true }를 전달한다.

evtSrc = new EventSource("/sse/alerts",
  { withCredentials: true });

진행률 표시 응용

장시간 배치 작업의 진행률을 ProgressBar 컴포넌트로 표시하는 전형적인 사용 사례다.

evtSrc.addEventListener("PROGRESS", function(e) {
  var pct = parseInt(e.data);
  this.ProgressBar00.value = pct;
  if (pct >= 100) this.fn_sseClose();
}.bind(this));

배치 완료 이벤트를 수신하면 evtSrc.close()로 연결을 닫아 서버 자원을 해제한다.


지난 글: WebSocket으로 실시간 서버 연동

다음 글: CRUD 패턴 설계와 표준 구조


읽어주셔서 감사합니다. 😊