Node.js에서 SSE를 통한 웹소켓을 넘어서는 효율적인 단방향 실시간 통신
Min-jun Kim
Dev Intern · Leapcell

소개
오늘날 상호 연결된 세상에서 실시간 통신은 더 이상 럭셔리가 아닌 많은 웹 애플리케이션의 근본적인 요구 사항입니다. 라이브 스포츠 점수와 주식 시세부터 뉴스 피드와 분석 대시보드에 이르기까지, 지속적인 폴링 없이 서버에서 클라이언트로 업데이트를 푸시하는 능력은 사용자 경험과 애플리케이션 응답성을 크게 향상시킵니다. 웹소켓은 전이중, 양방향 통신을 위한 주요 솔루션으로 부상했지만, 통신 흐름이 주로 단방향(서버-클라이언트)인 시나리오에서는 그 오버헤드가 때때로 과도할 수 있습니다. 이것이 바로 서버 전송 이벤트(SSE)가 Node.js 생태계 내에서 특히 매력적이고 종종 더 효율적인 대안을 제공하는 지점입니다. 이 글에서는 고성능 단방향 실시간 데이터 스트리밍을 위한 Node.js에서 SSE를 사용하는 실제 이점과 구현 세부 사항을 자세히 살펴보겠습니다.
핵심 개념 및 구현
SSE의 구체적인 내용으로 들어가기 전에, 관련된 몇 가지 주요 기술을 간략하게 정의해 보겠습니다.
- HTTP/1.1 및 HTTP/2: 웹 통신의 기본 프로토콜입니다. SSE는 표준 HTTP 연결을 활용합니다.
 - 영구 연결: 클라이언트와 서버 간에 장기간 열려 있는 연결로, 여러 요청/응답 또는 연속 데이터 스트림을 허용합니다.
 - 서버 전송 이벤트(SSE): 웹 페이지가 HTTP 연결을 통해 서버로부터 업데이트를 얻을 수 있도록 하는 W3C 표준입니다. 서버에서 클라이언트로의 일방향 데이터 스트리밍을 위해 설계되었습니다. 클라이언트는 표준 HTTP 요청을 시작하고 서버는 응답을 열어두고 주기적으로 데이터를 보냅니다.
 - 웹소켓: 단일 TCP 연결을 통한 전이중 통신 프로토콜입니다. 클라이언트와 서버 모두 언제든지 서로 데이터를 보낼 수 있는 영구 연결을 제공합니다.
 
서버 전송 이벤트 이해하기
SSE는 기본적으로 서버가 새로운 정보가 있을 때마다 클라이언트로 데이터를 푸시하는 영구 HTTP 연결을 설정함으로써 작동합니다. 웹소켓과 달리 SSE는 표준 HTTP를 기반으로 구축되며 별도의 핸드셰이크나 프로토콜 업그레이드가 필요하지 않습니다. 데이터는 text/event-stream 형식으로 전송되어 브라우저가 쉽게 구문 분석하고 처리할 수 있습니다. 각 "이벤트"는 일반적으로 event 유형, id 및 data 자체로 구성됩니다.
단방향 통신에 웹소켓 대신 SSE를 선택하는 이유
통신이 주로 단방향일 때 SSE는 몇 가지 이점을 제공합니다.
- 간단함: SSE는 웹소켓보다 구현 및 관리가 더 간단합니다. 표준 HTTP를 사용하므로 별도의 웹소켓 서버나 복잡한 프로토콜 처리가 필요 없습니다.
 - 내장된 재연결: 브라우저는 SSE 연결에 대한 자동 재연결을 네이티브로 지원하며, 이는 웹소켓에 대해 수동으로 구현하거나 클라이언트 측 라이브러리로 처리해야 하는 기능입니다.
 - HTTP/2 다중화: HTTP/2를 통해 사용될 때 SSE는 스트림 다중화를 활용하여 단일 TCP 연결을 통해 여러 SSE 연결(또는 기타 HTTP 요청)을 허용하여 오버헤드를 줄일 수 있습니다.
 - 낮은 오버헤드: 간단한 서버-클라이언트 데이터 푸시에 대해 SSE는 웹소켓 핸드셰이크, opcode 처리 또는 프레임 마스킹이 필요하지 않기 때문에 일반적으로 웹소켓보다 오버헤드가 낮습니다.
 - 적은 상태 관리: 클라이언트가 일반적으로 데이터를 다시 보내지 않기 때문에 서버는 전이중 웹소켓 연결에 비해 관리해야 할 연결 상태가 적은 경우가 많습니다.
 
Node.js에서 SSE 구현하기
Node.js에서 SSE 서버를 설정하고 클라이언트 측 JavaScript 애플리케이션으로 이를 사용하는 실제 예제를 살펴보겠습니다.
Node.js 서버
Node.js 웹 애플리케이션에서 널리 사용되는 Express.js를 서버에 사용하겠습니다.
// server.js const express = require('express'); const cors = require('cors'); // 교차 출처 요청용 const app = express(); const PORT = process.js.env.PORT || 3000; app.use(cors()); // 클라이언트 측 액세스를 위한 CORS 활성화 /** * 활성 SSE 연결에 대한 응답 객체를 저장하기 위한 인메모리 클라이언트 목록입니다. * 프로덕션 환경에서는 Redis Pub/Sub와 같은 보다 강력한 메시징 시스템을 사용하여 * 여러 서버 인스턴스에 걸쳐 이벤트를 브로드캐스트할 수 있습니다. */ let clients = []; let eventId = 0; // 간단한 이벤트 ID 카운터 app.get('/events', (req, res) => { // SSE에 필요한 헤더 설정 res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); // Nginx 버퍼링이 적용되는 경우 비활성화 // 클라이언트의 응답 객체 저장 clients.push(res); // 연결 확인을 위한 초기 이벤트 전송 res.write('event: connected\n'); res.write(`data: ${JSON.stringify({ message: 'Connected to SSE stream' })}\n\n`); console.log('New SSE client connected.'); // 연결이 닫힐 때 클라이언트 제거 req.on('close', () => { console.log('SSE client disconnected.'); clients = clients.filter(client => client !== res); }); }); // 연결된 클라이언트에 데이터를 시뮬레이션하여 전송하는 엔드포인트 app.post('/send-update', express.json(), (req, res) => { const { message } = req.body; if (!message) { return res.status(400).send('Message is required'); } eventId++; const eventData = { id: eventId, timestamp: new Date().toISOString(), message: message }; clients.forEach(client => { // SSE 사양에 따라 이벤트 데이터 형식 지정 client.write(`id: ${eventData.id}\n`); client.write('event: new_update\n'); client.write(`data: ${JSON.stringify(eventData)}\n\n`); }); res.status(200).send(`Update sent to ${clients.length} clients.`); }); // 기본적인 테스트를 위한 간단한 루트 엔드포인트 app.get('/', (req, res) => { res.send('SSE server is running. Connect to /events to receive updates.'); }); app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });
이 서버 구현에서:
Content-Type을text/event-stream으로 설정하고 중요한 캐시 제어 헤더를 설정합니다.- 나중에 메시지를 브로드캐스트할 수 있도록 연결된 클라이언트의 
res객체를 저장합니다. - 클라이언트가 연결되면 초기 
connected이벤트를 보냅니다. clients배열을 정리하기 위해 클라이언트 연결 해제를 처리합니다./send-updatePOST 엔드포인트는 외부 트리거 또는 내부 로직이 새 데이터를 푸시하는 것을 시뮬레이션합니다. 활성 클라이언트 연결을 모두 반복하고new_update이벤트를 보냅니다.
클라이언트 측 JavaScript
이제 이러한 이벤트를 사용하는 간단한 HTML 페이지와 JavaScript를 만듭니다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SSE Client</title> <style> body { font-family: sans-serif; margin: 20px; } #messages { border: 1px solid #ccc; padding: 10px; min-height: 200px; max-height: 400px; overflow-y: auto; background-color: #f9f9f9; } .message { margin-bottom: 5px; padding: 5px; border-bottom: 1px dashed #eee; } label { display: block; margin-top: 10px; } input[type="text"] { width: 300px; padding: 8px; } button { padding: 8px 15px; margin-top: 5px; cursor: pointer; } </style> </head> <body> <h1>Server-Sent Events Client</h1> <p>This page connects to a Node.js SSE server and displays real-time updates.</p> <h2>Live Updates</h2> <div id="messages"> <p>Waiting for updates...</p> </div> <h2>Send Test Update (via POST to server)</h2> <div> <label for="messageInput">Message:</label> <input type="text" id="messageInput" placeholder="Enter message to send via POST"> <button id="sendButton">Send Update</button> <p id="sendStatus"></p> </div> <script> const messageContainer = document.getElementById('messages'); const messageInput = document.getElementById('messageInput'); const sendButton = document.getElementById('sendButton'); const sendStatus = document.getElementById('sendStatus'); // EventSource 인스턴스 생성 const eventSource = new EventSource('http://localhost:3000/events'); eventSource.onopen = function() { console.log('SSE connection opened.'); addMessage('System', 'Connected to SSE stream.'); }; // 사용자 정의 'connected' 이벤트 수신 eventSource.addEventListener('connected', function(event) { const data = JSON.parse(event.data); console.log('Received initial connection event:', data); addMessage('System', data.message); }); // 사용자 정의 'new_update' 이벤트 수신 eventSource.addEventListener('new_update', function(event) { const data = JSON.parse(event.data); console.log('Received new update:', data); addMessage(`Event ID ${data.id} (${new Date(data.timestamp).toLocaleTimeString()})`, data.message); }); eventSource.onerror = function(error) { console.error('SSE Error:', error); addMessage('System', 'SSE connection error. Trying to reconnect...'); // EventSource는 자동으로 재연결을 시도합니다. }; function addMessage(sender, text) { const div = document.createElement('div'); div.className = 'message'; div.innerHTML = ``; messageContainer.appendChild(div); messageContainer.scrollTop = messageContainer.scrollHeight; // 아래로 자동 스크롤 } // --- 서버 업데이트를 트리거하기 위한 클라이언트 측 POST 기능 --- sendButton.addEventListener('click', async () => { const message = messageInput.value.trim(); if (!message) { sendStatus.textContent = 'Please enter a message.'; sendStatus.style.color = 'red'; return; } try { const response = await fetch('http://localhost:3000/send-update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }) }); if (response.ok) { const result = await response.text(); sendStatus.textContent = `Success: ${result}`; sendStatus.style.color = 'green'; messageInput.value = ''; // 입력 필드 지우기 } else { const errorText = await response.text(); throw new Error(`Server error: ${response.status} - ${errorText}`); } } catch (error) { console.error('Failed to send update:', error); sendStatus.textContent = `Error sending update: ${error.message}`; sendStatus.style.color = 'red'; } }); </script> </body> </html>
클라이언트 측에서는:
- SSE를 소비하기 위한 기본 브라우저 인터페이스인 
EventSourceAPI를 사용합니다. onopen,new_update(사용자 정의 이벤트 유형) 및onerror이벤트를 수신합니다.EventSource클라이언트는 연결이 끊어져도 자동으로 재연결을 처리합니다.sendButton기능은 서버의 POST 엔드포인트를 트리거하여 메시지를 SSE를 통해 브로드캐스트합니다.
예제 실행
- 서버:
npm init -y npm install express cors node server.js - 클라이언트: 웹 브라우저에서 
index.html파일을 엽니다. - 브라우저에 "Connected to SSE stream." 메시지가 표시되어야 합니다.
 - 클라이언트 페이지의 입력 필드와 버튼을 사용하여 메시지를 보내면 새로고침 없이 "Live Updates" 섹션에 실시간으로 표시되는 것을 볼 수 있습니다.
 
애플리케이션 시나리오
SSE는 서버가 실시간 이벤트의 주요 출처이고 클라이언트가 단순히 이를 소비하는 시나리오에서 빛을 발합니다. 일반적인 사용 사례는 다음과 같습니다.
- 뉴스 피드 및 라이브 블로그: 기사가 발생하는 대로 새 기사 또는 업데이트 푸시.
 - 주식 시세 및 암호화폐 가격: 실시간 시장 데이터 표시.
 - 스포츠 점수 및 이벤트 업데이트: 점수, 게임 통계 또는 라이브 논평 보내기.
 - 분석 대시보드: 실시간 메트릭 및 데이터 시각화 스트리밍.
 - 알림 시스템: 사용자에게 즉시 알림 제공(예: 새 이메일, 친구 요청).
 - 진행률 표시기: 오래 실행되는 서버 측 작업의 상태 표시.
 
이 모든 경우에 웹소켓을 사용할 수 있지만 SSE는 단방향 데이터 흐름에 대해 더 가볍고 간단한 솔루션을 제공합니다.
결론
서버 전송 이벤트(SSE)는 Node.js 애플리케이션에서 단방향 실시간 통신 기능을 구축하기 위한 매우 효율적이고 우아하게 단순한 솔루션을 제공합니다. 표준 HTTP와 자동 재연결을 위한 네이티브 브라우저 지원을 활용함으로써 SSE는 복잡성과 오버헤드를 줄여 서버-클라이언트 데이터 스트리밍에 탁월한 선택이 됩니다. 서버가 클라이언트 측에서 빈번한 통신을 예상하지 않고 업데이트를 푸시하는 것이 주요 요구 사항일 때, SSE는 웹소켓보다 우수하고 더 적합한 대안으로 두드러집니다. 개발 노력과 강력한 성능을 최소화하여 응답성이 뛰어나고 매력적인 사용자 경험을 가능하게 합니다.