이 글에서는 웹 애플리케이션에서 실시간 데이터 통신을 구현하는 기술 중 하나인 Server-Sent Events(SSE)에 대해 정리합니다. SSE의 기본 개념과 동작 방식, 다른 기술과의 비교, 그리고 운영 환경에서 반드시 고려해야 할 보안 강화 방안까지 알아봅니다.
What is Server-Sent Events (SSE)?
Server-Sent Events는 단어 의미 그대로 서버가 클라이언트로 이벤트를 보내는 기술입니다. 브라우저가 서버에 한번 연결을 요청하면, 그 연결을 계속 유지한 채로 서버가 클라이언트에게 필요한 데이터를 비동기적으로 푸시(Push)하는 단방향 통신 방식입니다.
SSE는 HTTP 프로토콜을 기반으로 동작하므로 추가적인 프로토콜 구현 없이 기존의 웹 인프라를 그대로 활용할 수 있다는 장점이 있습니다. 주된 목적은 서버에서 클라이언트로의 실시간 데이터 스트리밍이며, 알림, 실시간 피드, 상태 업데이트 등에 널리 사용됩니다.
How it Works
SSE의 동작 원리는 비교적 간단합니다. 클라이언트와 서버 간의 상호작용은 다음과 같은 흐름으로 이루어집니다.
- Client Connection: 클라이언트는 Javascript의
EventSource
객체를 사용하여 서버의 특정 엔드포인트에 연결을 요청합니다. - Server Response: 서버는 이 요청에 대해
Content-Type
헤더를text/event-stream
으로 설정하여 응답합니다. 이 헤더는 클라이언트에게 앞으로 전송될 데이터가 이벤트 스트림임을 알리는 역할을 합니다. - Persistent Connection: 서버는 연결을 종료하지 않고 계속 열어둡니다.
- Data Push: 서버는 새로운 데이터나 이벤트가 발생할 때마다 정해진 형식에 맞춰 클라이언트로 메시지를 전송합니다. 이 연결은 서버나 클라이언트가 명시적으로 닫기 전까지 유지됩니다.
sequenceDiagram participant Client participant Server Client->>Server: new EventSource('/events') Note over Client,Server: Initial HTTP Request Server-->>Client: HTTP 200 OK<br>Content-Type: text/event-stream<br>Connection: keep-alive Note over Client,Server: Connection Established loop Real-time Events Server-->>Client: data: Some new data\n\n Server-->>Client: event: notification<br>data: {"user":"guest","message":"Welcome!"}\n\n Server-->>Client: data: Another update\n\n end Client->>Server: eventSource.close() Note over Client,Server: Connection Closed by Client
Event Stream Format
서버가 전송하는 데이터는 단순한 텍스트 스트림이며, 각 메시지는 하나 이상의 key: value
필드와 두 개의 개행 문자(\n\n
)로 구분됩니다. 주요 필드는 다음과 같습니다.
data
: 전송할 실제 데이터. 한 메시지에 여러 개의data
필드가 포함될 수 있습니다.event
: 이벤트의 종류를 지정하는 필드. 클라이언트는 이 값을 사용하여 특정 타입의 이벤트를 리스닝할 수 있습니다. 지정하지 않으면message
이벤트로 처리됩니다.id
: 각 이벤트의 고유 ID. 클라이언트가 서버와 재연결될 경우, 마지막으로 수신한 이벤트의id
를Last-Event-ID
헤더에 담아 전송하여 유실된 데이터를 복구할 수 있습니다.retry
: 클라이언트가 재연결을 시도하기 전에 대기해야 할 시간을 밀리초 단위로 지정합니다.
// Simple message
data: This is a message.
// JSON data with a custom event type
event: user_update
data: {"username": "hahwul", "status": "online"}
// Message with an ID for synchronization
id: msg1
data: Some data stream
SSE vs. WebSockets vs. Polling
실시간 웹 통신을 구현하는 기술은 SSE 외에도 Polling, Long-Polling, WebSockets 등이 있습니다. 각 기술은 뚜렷한 특징과 장단점을 가지므로, 해결하려는 문제에 따라 적합한 기술을 선택해야 합니다.
Feature | Polling | Long-Polling | SSE (Server-Sent Events) | WebSockets |
---|---|---|---|---|
Direction | Client -> Server | Client -> Server | Server -> Client (Unidirectional) | Bidirectional |
Protocol | HTTP | HTTP | HTTP | WebSocket (ws:// , wss:// ) |
Connection | New connection per request | Long-lived, then new | Single persistent connection | Single persistent connection |
Overhead | High | Medium | Low | Low (after handshake) |
Use Case | Infrequent updates | Delayed updates | Notifications, Live Feeds | Chat, Gaming, Collaboration |
Reconnection | Manual | Manual | Automatic (built-in) | Manual |
- Polling: 클라이언트가 일정한 주기로 서버에 데이터를 요청하는 가장 간단한 방식이지만, 불필요한 요청이 많아 비효율적입니다.
- WebSockets: 양방향 통신을 지원하며, 매우 낮은 지연 시간으로 데이터를 주고받을 수 있습니다. 채팅 애플리케이션이나 실시간 온라인 게임처럼 클라이언트와 서버 간의 상호작용이 빈번할 때 가장 이상적입니다. 하지만 HTTP와는 다른 프로토콜을 사용합니다.
- SSE: 서버에서 클라이언트로의 단방향 데이터 푸시가 필요할 때 가장 적합합니다. HTTP를 사용하므로 구현이 간단하고,
EventSource
API에 자동 재연결 기능이 내장되어 있어 안정적입니다.
Client-Side Implementation
클라이언트 측 구현은 EventSource
API를 통해 매우 간단하게 작성할 수 있습니다.
const eventSource = new EventSource('/stream');
// General message listener
eventSource.onmessage = (event) => {
console.log('New message:', event.data);
};
// Listener for custom events
eventSource.addEventListener('notification', (event) => {
const notificationData = JSON.parse(event.data);
console.log('Notification:', notificationData.message);
});
// Error handling
eventSource.onerror = (err) => {
console.error("EventSource failed:", err);
// EventSource will automatically try to reconnect.
// To close it permanently:
// eventSource.close();
};
How to Securing SSE
SSE는 HTTP 위에서 동작하기 때문에 일반적인 웹 보안 위협에 노출되어 있습니다. 따라서 SSE를 프로덕션 환경에서 사용할 때는 반드시 보안을 고려해야 합니다.
Authentication and Authorization
EventSource
API는 Authorization
같은 커스텀 HTTP 헤더를 설정하는 기능을 표준적으로 지원하지 않습니다. 따라서 일반적인 토큰 기반 인증에 제약이 있습니다.
- Cookie-based Authentication: 브라우저가
EventSource
요청 시 자동으로 쿠키를 전송하므로, 세션 쿠키 기반의 인증은 자연스럽게 동작합니다. 이는 가장 간단하고 효과적인 방법 중 하나입니다. - Query Parameter Token: URL의 쿼리 파라미터를 통해 인증 토큰을 전달하는 방법도 가능합니다. (
/stream?token=...
) 하지만 이 방식은 토큰이 서버 로그, 브라우저 히스토리 등에 노출될 위험이 있어 신중하게 사용해야 합니다.
CSRF (Cross-Site Request Forgery)
SSE 연결은 GET 요청으로 시작되므로 CSRF 공격에 취약할 수 있습니다. 공격자는 사용자가 로그인된 상태에서 악의적인 페이지를 방문하도록 유도하여, 사용자의 권한으로 SSE 연결을 맺고 민감한 실시간 데이터를 탈취할 수 있습니다.
- Origin Header Check: 서버 측에서 요청의
Origin
헤더를 검증하여 허가된 도메인에서의 요청만 허용해야 합니다. - SameSite Cookies: 인증 쿠키에
SameSite=Lax
또는SameSite=Strict
속성을 설정하여 다른 출처에서 요청 시 쿠키가 전송되지 않도록 방지합니다.
XSS (Cross-Site Scripting)
서버가 보내주는 데이터를 클라이언트에서 그대로 HTML에 렌더링할 경우 XSS 취약점이 발생할 수 있습니다. 서버에서 받은 데이터는 절대 신뢰해서는 안 되며, 항상 이스케이프(Escape) 또는 인코딩(Encode) 후 사용해야 합니다.
const outputDiv = document.getElementById('output');
eventSource.onmessage = (event) => {
// Vulnerable to XSS: Never do this!
// outputDiv.innerHTML += event.data + '<br>';
// Safe: Use textContent to render data as plain text.
const p = document.createElement('p');
p.textContent = event.data;
outputDiv.appendChild(p);
};
Transport Security
중간자(MITM) 공격을 통해 이벤트 스트림이 노출되거나 변조되는 것을 막기 위해, SSE 통신은 반드시 HTTPS를 통해 암호화되어야 합니다. 이는 선택이 아닌 필수 사항입니다.
Denial of Service (DoS)
SSE는 장시간 연결을 유지하므로 서비스 거부 공격의 대상이 될 수 있습니다. 공격자가 수많은 연결을 생성하여 서버의 리소스를 고갈시킬 수 있습니다.
- Rate Limiting: 특정 IP 주소나 사용자가 생성할 수 있는 동시 연결 수를 제한해야 합니다.
- Resource Management: 웹 서버나 리버스 프록시(Nginx 등)의 최대 동시 연결 수 설정을 최적화하여 비정상적인 트래픽을 제어해야 합니다.
Conclusion
Server-Sent Events(SSE)는 서버에서 클라이언트로의 실시간 데이터 스트리밍을 위한 강력하고 표준화된 기술입니다. HTTP를 기반으로 하여 구현이 용이하고, 자동 재연결과 같은 편의 기능을 내장하고 있어 개발 및 유지보수가 편리합니다.
하지만 그 편리함 이면에는 반드시 고려해야 할 보안적 측면이 존재합니다. 안전한 SSE 구현을 위해서는 인증/인가, CSRF, XSS, DoS 등 다양한 위협을 인지하고 적절한 방어 메커니즘을 적용하는 것이 매우 중요합니다.
양방향 통신이 필수적인 경우가 아니라면, SSE는 WebSockets의 복잡성 없이도 실시간 업데이트 기능을 구현할 수 있는 매우 효율적인 대안입니다.