**Server-Sent Events(SSE)**๋ ์๋ฒ๊ฐ ์น ํด๋ผ์ด์ธํธ์๊ฒ ์ค์๊ฐ ๋ฐ์ดํฐ๋ฅผ ํธ์ํ ์ ์๋๋ก ํ๋ Web API์ ๋๋ค. ์ ํต์ ์ธ HTTP ์์ฒญ-์๋ต ํจํด๊ณผ ๋ฌ๋ฆฌ, ์๋ฒ๊ฐ ์ธ์ ๋ ์ง ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง๋ฅผ ๋ณด๋ผ ์ ์๋ ๋จ๋ฐฉํฅ ํต์ ์ฑ๋์ ์ ๊ณตํฉ๋๋ค.
SSE๋ HTTP ํ๋กํ ์ฝ ๊ธฐ๋ฐ์ผ๋ก ์๋ํ๋ฉฐ, EventSource ์ธํฐํ์ด์ค๋ฅผ ํตํด ๋ธ๋ผ์ฐ์ ์์ ๋ค์ดํฐ๋ธ๋ก ์ง์๋ฉ๋๋ค. ์๋ฒ์์ ๋ณด๋ด๋ ๋ฉ์์ง๋ ํด๋ผ์ด์ธํธ ์ธก์์ ์ด๋ฒคํธ + ๋ฐ์ดํฐ ํํ๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค.
ํด๋น ๊ฐ๋ ์ด ํ์ํ ์ด์
- ์ค์๊ฐ ์ ๋ฐ์ดํธ๊ฐ ํ์ํ์ง๋ง ์๋ฐฉํฅ ํต์ ์ ๋ถํ์ํ ๊ฒฝ์ฐ: ์ฃผ์ ์์ธ, ์๋ฆผ, ๋์๋ณด๋ ์ ๋ฐ์ดํธ ๋ฑ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก๋ง ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ฉด ๋๋ ์ํฉ์์ WebSocket๋ณด๋ค ๊ฐ๋จํ๊ณ ํจ์จ์ ์ ๋๋ค
- ํด๋ง(Polling)์ ๋นํจ์จ์ฑ ๊ฐ์ : ํด๋ผ์ด์ธํธ๊ฐ ์ฃผ๊ธฐ์ ์ผ๋ก ์๋ฒ์ ์์ฒญ์ ๋ณด๋ด๋ ํด๋ง ๋ฐฉ์์ ๋ถํ์ํ ๋คํธ์ํฌ ํธ๋ํฝ๊ณผ ์๋ฒ ๋ถํ๋ฅผ ๋ฐ์์ํต๋๋ค. SSE๋ ์๋ฒ๊ฐ ๋ฐ์ดํฐ๊ฐ ์์ ๋๋ง ์ ์กํ๋ฏ๋ก ๋ฆฌ์์ค๋ฅผ ์ ์ฝํฉ๋๋ค
- ์๋ ์ฌ์ฐ๊ฒฐ ๋ฐ ๊ฐ๋จํ ๊ตฌํ: ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ฐ๊ฒฐ์ ๊ด๋ฆฌํ๊ณ ์ฌ์ฐ๊ฒฐ์ ์ฒ๋ฆฌํ๋ฏ๋ก, ๊ฐ๋ฐ์๊ฐ ๋ณ๋์ ๋ณต์กํ ๋ก์ง์ ๊ตฌํํ ํ์๊ฐ ์์ต๋๋ค
AS-IS: Polling ๋ฐฉ์
sequenceDiagram autonumber participant Client participant Server loop ์ฃผ๊ธฐ์ ํด๋ง (์: 5์ด๋ง๋ค) Client->>Server: HTTP GET /api/updates Server->>Client: 200 OK (๋ฐ์ดํฐ ์์) Note over Client: 5์ด ๋๊ธฐ Client->>Server: HTTP GET /api/updates Server->>Client: 200 OK (๋ฐ์ดํฐ ์์) Note over Client: 5์ด ๋๊ธฐ Client->>Server: HTTP GET /api/updates Server->>Client: 200 OK { data: "new update" } end Note over Client,Server: ๋ถํ์ํ ์์ฒญ์ด ๋ง๊ณ ์ค์๊ฐ์ฑ ๋ฎ์
TO-BE: SSE ๋ฐฉ์
sequenceDiagram autonumber participant Client participant Server Client->>Server: HTTP GET /api/events (EventSource ์ฐ๊ฒฐ) Server->>Client: 200 OK (์ฐ๊ฒฐ ์ ์ง) Note over Client,Server: ์ฐ๊ฒฐ์ด ์ด๋ฆฐ ์ํ๋ก ์ ์ง๋จ Note over Server: ์ด๋ฒคํธ ๋ฐ์ (๋ฐ์ดํฐ ์ค๋น๋จ) Server->>Client: data: {"message": "update 1"} Note over Server: ๋ ๋ค๋ฅธ ์ด๋ฒคํธ ๋ฐ์ Server->>Client: data: {"message": "update 2"} Note over Server: ์ถ๊ฐ ์ด๋ฒคํธ Server->>Client: data: {"message": "update 3"} Note over Client,Server: ํ๋์ ์ฐ๊ฒฐ๋ก ์ค์๊ฐ ์ ๋ฐ์ดํธ ์์
SSE vs WebSocket ๋น๊ต
| ํน์ง | SSE | WebSocket |
|---|---|---|
| ํต์ ๋ฐฉํฅ | ๋จ๋ฐฉํฅ (์๋ฒ โ ํด๋ผ์ด์ธํธ) | ์๋ฐฉํฅ (์๋ฒ โ ํด๋ผ์ด์ธํธ) |
| ํ๋กํ ์ฝ | HTTP/HTTPS | WebSocket ํ๋กํ ์ฝ (ws://, wss://) |
| ์ฌ์ฐ๊ฒฐ | ๋ธ๋ผ์ฐ์ ๊ฐ ์๋ ์ฒ๋ฆฌ | ์๋์ผ๋ก ๊ตฌํ ํ์ |
| ๊ตฌํ ๋ณต์ก๋ | ๊ฐ๋จ (EventSource API) | ์๋์ ์ผ๋ก ๋ณต์ก |
| ์ค๋ฒํค๋ | ๋ฎ์ (๋จ๋ฐฉํฅ ํต์ ์ ์ต์ ํ) | ๋์ (์๋ฐฉํฅ ํต์ ์ง์) |
| ๋ฐฉํ๋ฒฝ ํต๊ณผ | ์ฉ์ด (HTTP ๊ธฐ๋ฐ) | ์ ํ์ (์ผ๋ถ ํ๋ก์์์ ์ฐจ๋จ) |
| ์ฌ์ฉ ์ฌ๋ก | ์ค์๊ฐ ์๋ฆผ, ํผ๋, ๋์๋ณด๋, ์ฃผ์ ์์ธ | ์ฑํ , ๊ฒ์, ํ์ ๋๊ตฌ ๋ฑ ์๋ฐฉํฅ ํต์ |
์ธ์ SSE๋ฅผ ์ฌ์ฉํ ๊น?
- โ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก๋ง ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๋ฉด ๋๋ ๊ฒฝ์ฐ
- โ ์ค์๊ฐ ์ ๋ฐ์ดํธ๊ฐ ํ์ํ์ง๋ง ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ๋ก ๋ฉ์์ง๋ฅผ ์์ฃผ ๋ณด๋ผ ํ์๊ฐ ์๋ ๊ฒฝ์ฐ
- โ ๋ธ๋ผ์ฐ์ ํธํ์ฑ๊ณผ ๋ฐฉํ๋ฒฝ ํต๊ณผ๊ฐ ์ค์ํ ๊ฒฝ์ฐ
- โ ๊ตฌํ์ ๊ฐ๋จํ๊ฒ ์ ์งํ๊ณ ์ถ์ ๊ฒฝ์ฐ
์ธ์ WebSocket์ ์ฌ์ฉํ ๊น?
- โ ํด๋ผ์ด์ธํธ์ ์๋ฒ ๊ฐ ์๋ฐฉํฅ ์ค์๊ฐ ํต์ ์ด ํ์ํ ๊ฒฝ์ฐ (์ฑํ , ๊ฒ์)
- โ ๋ฎ์ ์ง์ฐ ์๊ฐ(latency)์ด ์ค์ํ ๊ฒฝ์ฐ
- โ ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ๋ก ๋น๋ฒํ๊ฒ ๋ฉ์์ง๋ฅผ ๋ณด๋ด์ผ ํ๋ ๊ฒฝ์ฐ
EventSource API ์ฌ์ฉ๋ฒ
๊ธฐ๋ณธ ์ฐ๊ฒฐ ๋ฐ ๋ฉ์์ง ์์
// SSE ์ฐ๊ฒฐ ์์ฑ
const eventSource = new EventSource('/api/events');
// ๋ฉ์์ง ์์ ์ด๋ฒคํธ ๋ฆฌ์ค๋
eventSource.onmessage = function(event) {
console.log('์ ๋ฉ์์ง ์์ :', event.data);
// JSON ๋ฐ์ดํฐ ํ์ฑ
const data = JSON.parse(event.data);
updateUI(data);
};
// ์ฐ๊ฒฐ ์ฑ๊ณต
eventSource.onopen = function() {
console.log('SSE ์ฐ๊ฒฐ ์ฑ๊ณต');
};
// ์๋ฌ ์ฒ๋ฆฌ
eventSource.onerror = function(error) {
console.error('SSE ์ฐ๊ฒฐ ์๋ฌ:', error);
// ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ฌ์ฐ๊ฒฐ ์๋
};
// ์ฐ๊ฒฐ ์ข
๋ฃ (ํ์ํ ๊ฒฝ์ฐ)
// eventSource.close();์ปค์คํ ์ด๋ฒคํธ ์ฒ๋ฆฌ
const eventSource = new EventSource('/api/events');
// ํน์ ์ด๋ฒคํธ ํ์
๋ฆฌ์ค๋
eventSource.addEventListener('notification', function(event) {
const notification = JSON.parse(event.data);
showNotification(notification);
});
eventSource.addEventListener('stock-update', function(event) {
const stockData = JSON.parse(event.data);
updateStockPrice(stockData);
});์๋ฒ ์ธก ๊ตฌํ ์์ (Node.js/Express)
app.get('/api/events', (req, res) => {
// SSE ํค๋ ์ค์
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง ์ ์ก
const sendEvent = (data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
// ์ด๊ธฐ ๋ฉ์์ง
sendEvent({ message: 'Connected to SSE' });
// ์ฃผ๊ธฐ์ ์ผ๋ก ์
๋ฐ์ดํธ ์ ์ก
const intervalId = setInterval(() => {
sendEvent({
timestamp: Date.now(),
message: 'Server update'
});
}, 5000);
// ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ ์ข
๋ฃ ์ ์ ๋ฆฌ
req.on('close', () => {
clearInterval(intervalId);
console.log('Client disconnected');
});
});์ปค์คํ ์ด๋ฒคํธ ํ์ ์ ์ก (์๋ฒ)
// ์ด๋ฒคํธ ํ์
์ง์
res.write(`event: notification\n`);
res.write(`data: ${JSON.stringify({ title: 'New alert', body: 'Something happened' })}\n\n`);
res.write(`event: stock-update\n`);
res.write(`data: ${JSON.stringify({ symbol: 'AAPL', price: 150.23 })}\n\n`);SSE์ ์ฃผ์ ํน์ง 4๊ฐ์ง
1. HTTP ๊ธฐ๋ฐ ํ๋กํ ์ฝ
SSE๋ ์ผ๋ฐ์ ์ธ HTTP ์ฐ๊ฒฐ ์์์ ์๋ํฉ๋๋ค. ๋ณ๋์ ํ๋กํ ์ฝ ์ ๊ทธ๋ ์ด๋๊ฐ ํ์ํ์ง ์์ผ๋ฉฐ, ๊ธฐ์กด HTTP ์ธํ๋ผ(ํ๋ก์, ๋ก๋ ๋ฐธ๋ฐ์, ๋ฐฉํ๋ฒฝ)์ ํธํ๋ฉ๋๋ค.
GET /api/events HTTP/1.1
Host: example.com
Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"message": "Hello"}
data: {"message": "World"}2. ์๋ ์ฌ์ฐ๊ฒฐ ๋ฉ์ปค๋์ฆ
๋ธ๋ผ์ฐ์ ์ EventSource ๊ตฌํ์ฒด๋ ์ฐ๊ฒฐ์ด ๋์ด์ง๋ฉด ์๋์ผ๋ก ์ฌ์ฐ๊ฒฐ์ ์๋ํฉ๋๋ค. ๊ฐ๋ฐ์๊ฐ ๋ณ๋์ ์ฌ์ฐ๊ฒฐ ๋ก์ง์ ๊ตฌํํ ํ์๊ฐ ์์ต๋๋ค.
// ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ฌ์ฐ๊ฒฐ ์ฒ๋ฆฌ
eventSource.onerror = function(error) {
// ์๋ฌ ๋ก๊น
๋ง ํ๋ฉด ๋จ
console.log('์ฐ๊ฒฐ ๋๊น, ์๋ ์ฌ์ฐ๊ฒฐ ์ค...');
// ๋ธ๋ผ์ฐ์ ๊ฐ ์์์ ์ฌ์ฐ๊ฒฐ ์๋
};์๋ฒ ์ธก์์ ์ฌ์ฐ๊ฒฐ ์๊ฐ์ ์ ์ดํ ์๋ ์์ต๋๋ค:
// ์๋ฒ: ํด๋ผ์ด์ธํธ์๊ฒ 3์ด ํ ์ฌ์ฐ๊ฒฐํ๋ผ๊ณ ์ง์
res.write(`retry: 3000\n`);
res.write(`data: ${JSON.stringify({ message: 'retry after 3s' })}\n\n`);3. ์ด๋ฒคํธ ID ๋ฐ ๋ง์ง๋ง ์ด๋ฒคํธ ์ถ์
SSE๋ ๊ฐ ๋ฉ์์ง์ ๊ณ ์ ID๋ฅผ ๋ถ์ฌํ์ฌ, ์ฐ๊ฒฐ์ด ๋๊ฒผ๋ค๊ฐ ์ฌ์ฐ๊ฒฐ๋ ๋ ๋ง์ง๋ง์ผ๋ก ๋ฐ์ ์ด๋ฒคํธ ์ดํ๋ถํฐ ๋ค์ ๋ฐ์ ์ ์์ต๋๋ค.
์๋ฒ ์ธก:
let messageId = 0;
res.write(`id: ${messageId}\n`);
res.write(`data: ${JSON.stringify({ content: 'message' })}\n\n`);
messageId++;ํด๋ผ์ด์ธํธ ์ฌ์ฐ๊ฒฐ ์ ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก Last-Event-ID ํค๋๋ฅผ ์ ์ก:
GET /api/events HTTP/1.1
Last-Event-ID: 42์๋ฒ๋ ์ด๋ฅผ ํ์ธํ๊ณ 42๋ฒ ์ดํ์ ์ด๋ฒคํธ๋ง ์ ์กํ ์ ์์ต๋๋ค.
4. Web Workers ์ง์
SSE๋ Web Workers์์๋ ์ฌ์ฉ ๊ฐ๋ฅํ์ฌ, ๋ฉ์ธ ์ค๋ ๋๋ฅผ ์ฐจ๋จํ์ง ์๊ณ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ค์๊ฐ ๋ฐ์ดํฐ๋ฅผ ์์ ํ ์ ์์ต๋๋ค.
// worker.js
const eventSource = new EventSource('/api/events');
eventSource.onmessage = function(event) {
// ๋ฉ์ธ ์ค๋ ๋๋ก ๋ฉ์์ง ์ ๋ฌ
self.postMessage({
type: 'sse-update',
data: event.data
});
};์ค์ ์ฌ์ฉ ์ฌ๋ก
1. ์ค์๊ฐ ์๋ฆผ ์์คํ
// ์ฌ์ฉ์์๊ฒ ์ค์๊ฐ ์๋ฆผ ํธ์
const eventSource = new EventSource('/api/notifications');
eventSource.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
showToast(notification.title, notification.message);
});2. ๋ผ์ด๋ธ ๋์๋ณด๋
// ์ค์๊ฐ ๋ชจ๋ํฐ๋ง ๋์๋ณด๋
const eventSource = new EventSource('/api/metrics');
eventSource.onmessage = (event) => {
const metrics = JSON.parse(event.data);
updateChart(metrics.cpu, metrics.memory, metrics.requests);
};3. ์์ ๋ฏธ๋์ด ํผ๋
// ์ ํฌ์คํธ ์ค์๊ฐ ์
๋ฐ์ดํธ
const eventSource = new EventSource('/api/feed');
eventSource.addEventListener('new-post', (event) => {
const post = JSON.parse(event.data);
prependPostToFeed(post);
});4. ์ฃผ์ ์์ธ ์ ๋ฐ์ดํธ
// ์ค์๊ฐ ์ฃผ์ ๊ฐ๊ฒฉ
const eventSource = new EventSource('/api/stocks?symbols=AAPL,GOOGL');
eventSource.addEventListener('price-update', (event) => {
const { symbol, price, change } = JSON.parse(event.data);
updateStockTicker(symbol, price, change);
});