EventSource๋ Server-Sent Events(SSE)๋ฅผ ๋ธ๋ผ์ฐ์ ์์ ์ฌ์ฉํ๊ธฐ ์ํ ์ธํฐํ์ด์ค์
๋๋ค. HTTP ์๋ฒ์์ **์ง์์ ์ธ ์ฐ๊ฒฐ(persistent connection)**์ ์ด๊ณ , ์๋ฒ๋ก๋ถํฐ text/event-stream ํฌ๋งท์ผ๋ก ์ ์ก๋๋ ์ด๋ฒคํธ๋ฅผ ์์ ํฉ๋๋ค.
EventSource๋ ๋จ๋ฐฉํฅ ํต์ (์๋ฒ โ ํด๋ผ์ด์ธํธ)์ ์ํด ์ค๊ณ๋์์ผ๋ฉฐ, ์ค์๊ฐ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ๊ฐ ํ์ํ ์์ ๋ฏธ๋์ด ํผ๋, ๋ด์ค ์ ๋ฐ์ดํธ, ๋ชจ๋ํฐ๋ง ๋์๋ณด๋ ๋ฑ์ ์ ํฉํฉ๋๋ค.
ํด๋น ๊ฐ๋ ์ด ํ์ํ ์ด์
- ๊ฐ๋จํ ์ค์๊ฐ ํต์ ๊ตฌํ: WebSocket์ฒ๋ผ ๋ณต์กํ ํ๋กํ ์ฝ ํธ๋์์ดํฌ ์์ด, ์ผ๋ฐ HTTP ์ฐ๊ฒฐ๋ง์ผ๋ก ์ค์๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ์ ์์ต๋๋ค
- ๋ธ๋ผ์ฐ์ ๋ค์ดํฐ๋ธ ์ง์: ๋ณ๋์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด ๋ธ๋ผ์ฐ์ ๊ฐ ์ ๊ณตํ๋ API๋ง์ผ๋ก SSE๋ฅผ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค
- ์๋ ์ฌ์ฐ๊ฒฐ ๋ฐ ์ํ ๊ด๋ฆฌ: ์ฐ๊ฒฐ ๋๊น, ์ฌ์ฐ๊ฒฐ, ์ฐ๊ฒฐ ์ํ ์ถ์ ๋ฑ์ ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ฒ๋ฆฌํ๋ฏ๋ก ๊ฐ๋ฐ์๊ฐ ๋ณต์กํ ๋ก์ง์ ์์ฑํ ํ์๊ฐ ์์ต๋๋ค
AS-IS: XMLHttpRequest๋ก ํด๋ง ๊ตฌํ
// ์๋์ผ๋ก ํด๋ง ๋ฐ ์ฌ์ฐ๊ฒฐ ๋ก์ง ๊ตฌํ ํ์
function pollServer() {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/updates');
xhr.onload = function() {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
handleUpdate(data);
}
// 5์ด ํ ๋ค์ ์์ฒญ
setTimeout(pollServer, 5000);
};
xhr.onerror = function() {
console.error('Request failed');
// ์๋ฌ ์ ์ฌ์๋ ๋ก์ง ํ์
setTimeout(pollServer, 10000);
};
xhr.send();
}
pollServer();TO-BE: EventSource ์ฌ์ฉ
// ์๋ ์ฌ์ฐ๊ฒฐ ๋ฐ ์ง์์ ์ฐ๊ฒฐ ์ ๊ณต
const eventSource = new EventSource('/api/updates');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
handleUpdate(data);
};
eventSource.onerror = function(error) {
console.error('Connection error:', error);
// ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ฌ์ฐ๊ฒฐ ์๋
};
// ํ์ ์ ์ฐ๊ฒฐ ์ข
๋ฃ
// eventSource.close();EventSource ์์ฑ์
const eventSource = new EventSource(url, options);๋งค๊ฐ๋ณ์
- url (string, ํ์): SSE ์ด๋ฒคํธ๋ฅผ ์ ์กํ๋ ์๋ฒ ์๋ํฌ์ธํธ URL
- options (object, ์ ํ):
withCredentials(boolean): CORS ์์ฒญ ์ ์ธ์ฆ ์ ๋ณด(์ฟ ํค, Authorization ํค๋ ๋ฑ) ํฌํจ ์ฌ๋ถ. ๊ธฐ๋ณธ๊ฐ์false
์์
// ๊ธฐ๋ณธ ์ฌ์ฉ
const eventSource = new EventSource('/api/events');
// CORS ์ธ์ฆ ์ ๋ณด ํฌํจ
const eventSourceWithCreds = new EventSource('https://api.example.com/events', {
withCredentials: true
});EventSource ์์ฑ (Properties)
| ์์ฑ | ํ์ | ์ค๋ช |
|---|---|---|
| readyState | number (์ฝ๊ธฐ ์ ์ฉ) | ํ์ฌ ์ฐ๊ฒฐ ์ํ: CONNECTING (0), OPEN (1), CLOSED (2) |
| url | string (์ฝ๊ธฐ ์ ์ฉ) | ์ฐ๊ฒฐ๋ ์ด๋ฒคํธ ์์ค์ URL |
| withCredentials | boolean (์ฝ๊ธฐ ์ ์ฉ) | CORS ์ธ์ฆ ์ ๋ณด ํฌํจ ์ฌ๋ถ |
์ฐ๊ฒฐ ์ํ ํ์ธ
const eventSource = new EventSource('/api/events');
console.log(eventSource.readyState); // 0 (CONNECTING)
eventSource.onopen = function() {
console.log(eventSource.readyState); // 1 (OPEN)
};
eventSource.close();
console.log(eventSource.readyState); // 2 (CLOSED)์ฐ๊ฒฐ ์ํ ์์
EventSource.CONNECTING = 0 // ์ฐ๊ฒฐ ์๋ฆฝ ์ค
EventSource.OPEN = 1 // ์ฐ๊ฒฐ ์ด๋ฆผ, ์ด๋ฒคํธ ์์ ์ค
EventSource.CLOSED = 2 // ์ฐ๊ฒฐ ๋ซํEventSource ๋ฉ์๋
close()
์ฐ๊ฒฐ์ ๋ซ๊ณ readyState๋ฅผ CLOSED๋ก ์ค์ ํฉ๋๋ค. ์ด๋ฏธ ๋ซํ ์์ผ๋ฉด ์๋ฌด ๋์๋ ํ์ง ์์ต๋๋ค.
const eventSource = new EventSource('/api/events');
// ํน์ ์กฐ๊ฑด์์ ์ฐ๊ฒฐ ์ข
๋ฃ
if (userLoggedOut) {
eventSource.close();
console.log('Connection closed');
}EventSource ์ด๋ฒคํธ
| ์ด๋ฒคํธ | ์ค๋ช |
|---|---|
| open | ์๋ฒ์์ ์ฐ๊ฒฐ์ด ์ด๋ ธ์ ๋ ๋ฐ์ |
| message | ์๋ฒ๋ก๋ถํฐ ๋ฐ์ดํฐ๋ฅผ ์์ ํ์ ๋ ๋ฐ์ (์ด๋ฆ ์๋ ์ด๋ฒคํธ) |
| error | ์ฐ๊ฒฐ ์คํจ ๋๋ ์๋ฌ ๋ฐ์ ์ ๋ฐ์ |
์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ๋ก ๋ฐฉ๋ฒ
๋ฐฉ๋ฒ 1: on* ์์ฑ ์ฌ์ฉ
const eventSource = new EventSource('/api/events');
eventSource.onopen = function(event) {
console.log('Connection opened', event);
};
eventSource.onmessage = function(event) {
console.log('Message received:', event.data);
};
eventSource.onerror = function(event) {
console.error('Error occurred:', event);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
};๋ฐฉ๋ฒ 2: addEventListener ์ฌ์ฉ
const eventSource = new EventSource('/api/events');
eventSource.addEventListener('open', (event) => {
console.log('Connection opened');
});
eventSource.addEventListener('message', (event) => {
console.log('Message:', event.data);
});
eventSource.addEventListener('error', (event) => {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection lost');
}
});์ด๋ฆ ์๋ ์ด๋ฒคํธ (Named Events)
์๋ฒ์์ ํน์ ์ด๋ฒคํธ ํ์ ์ ์ง์ ํ์ฌ ์ ์กํ๋ฉด, ํด๋ผ์ด์ธํธ๋ ํด๋น ์ด๋ฒคํธ๋ง ์ ๋ณ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
ํด๋ผ์ด์ธํธ ์ฝ๋
const sse = new EventSource('/api/v1/sse');
// "notice" ์ด๋ฒคํธ ์ฒ๋ฆฌ
sse.addEventListener('notice', (event) => {
console.log('Notice:', event.data);
showNotification(event.data);
});
// "update" ์ด๋ฒคํธ ์ฒ๋ฆฌ
sse.addEventListener('update', (event) => {
console.log('Update:', event.data);
refreshData(JSON.parse(event.data));
});
// ์ด๋ฆ ์๋ ๋ฉ์์ง ์ฒ๋ฆฌ
sse.addEventListener('message', (event) => {
console.log('Generic message:', event.data);
});
// ์๋ฌ ์ฒ๋ฆฌ
sse.addEventListener('error', (event) => {
if (event.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
});์๋ฒ ์ฝ๋ (Node.js/Express)
app.get('/api/v1/sse', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// ์ด๋ฆ ์๋ ์ด๋ฒคํธ ์ ์ก
res.write('event: notice\n');
res.write('data: {"message": "System maintenance in 10 minutes"}\n\n');
res.write('event: update\n');
res.write('data: {"stock": "AAPL", "price": 150.23}\n\n');
// ์ด๋ฆ ์๋ ๋ฉ์์ง ์ ์ก (๊ธฐ๋ณธ "message" ์ด๋ฒคํธ)
res.write('data: {"type": "generic", "content": "Hello"}\n\n');
});EventSource์ ์ฃผ์ ํน์ง 3๊ฐ์ง
1. ์๋ ์ฌ์ฐ๊ฒฐ
EventSource๋ ์ฐ๊ฒฐ์ด ๋๊ธฐ๋ฉด ์๋์ผ๋ก ์ฌ์ฐ๊ฒฐ์ ์๋ํฉ๋๋ค. ๊ฐ๋ฐ์๊ฐ ๋ณ๋์ ์ฌ์ฐ๊ฒฐ ๋ก์ง์ ์์ฑํ ํ์๊ฐ ์์ต๋๋ค.
const eventSource = new EventSource('/api/events');
eventSource.onerror = function() {
// ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ฌ์ฐ๊ฒฐ ์๋
console.log('Connection lost, reconnecting...');
};
// ์๋ฒ์์ ์ฌ์ฐ๊ฒฐ ๊ฐ๊ฒฉ์ ์ ์ดํ ์๋ ์์
// res.write('retry: 3000\n'); // 3์ด ํ ์ฌ์ฐ๊ฒฐ2. HTTP/1.1๊ณผ HTTP/2์์์ ์ฐ๊ฒฐ ์ ํ
์ค์ํ ์ ํ ์ฌํญ: HTTP/1.1์์๋ ๋ธ๋ผ์ฐ์ ๊ฐ ๋๋ฉ์ธ๋น ์ฝ 6๊ฐ์ SSE ์ฐ๊ฒฐ๋ง ํ์ฉํฉ๋๋ค. ์ฌ๋ฌ ํญ์ ์ด๋ฉด ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ:
- HTTP/2 ์ฌ์ฉ: HTTP/2์์๋ 100๊ฐ ์ด์์ ๋์ ์คํธ๋ฆผ์ ์ง์ํ๋ฏ๋ก ์ฐ๊ฒฐ ์ ํ์ด ํฌ๊ฒ ์ํ๋ฉ๋๋ค
- ์ฐ๊ฒฐ ํตํฉ: ์ฌ๋ฌ ๋ฐ์ดํฐ ์์ค๋ฅผ ํ๋์ SSE ์ฐ๊ฒฐ๋ก ํตํฉ
- WebSocket ์ฌ์ฉ: ์๋ฐฉํฅ ํต์ ์ด ํ์ํ ๊ฒฝ์ฐ WebSocket์ผ๋ก ์ ํ
graph TD A[HTTP/1.1] -->|์ฐ๊ฒฐ ์ ํ| B[๋๋ฉ์ธ๋น 6๊ฐ] A --> C[๋ค์ค ํญ ์ฌ์ฉ ์ ๋ฌธ์ ๋ฐ์] D[HTTP/2] -->|์ฐ๊ฒฐ ์ ํ| E[100+ ๋์ ์คํธ๋ฆผ] D --> F[๋ค์ค ํญ ์ฌ์ฉ ๊ฐ๋ฅ] style B fill:#ff6b6b style E fill:#51cf66
3. Web Workers ์ง์
EventSource๋ Web Workers์์๋ ์ฌ์ฉํ ์ ์์ด, ๋ฉ์ธ ์ค๋ ๋๋ฅผ ์ฐจ๋จํ์ง ์๊ณ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ค์๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
worker.js
const eventSource = new EventSource('/api/events');
eventSource.onmessage = function(event) {
// ๋ฉ์ธ ์ค๋ ๋๋ก ๋ฐ์ดํฐ ์ ๋ฌ
self.postMessage({
type: 'sse-data',
data: event.data
});
};
eventSource.onerror = function() {
self.postMessage({
type: 'sse-error',
message: 'Connection failed'
});
};main.js
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
if (event.data.type === 'sse-data') {
console.log('Received from worker:', event.data.data);
updateUI(event.data.data);
}
};์ค์ ์ฌ์ฉ ์์
์ค์๊ฐ ์๋ฆผ ์์คํ
const notifications = new EventSource('/api/notifications');
notifications.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
// ๋ธ๋ผ์ฐ์ ์๋ฆผ ํ์
if (Notification.permission === 'granted') {
new Notification(data.title, {
body: data.message,
icon: data.icon
});
}
});๋ผ์ด๋ธ ๋ชจ๋ํฐ๋ง ๋์๋ณด๋
const metrics = new EventSource('/api/metrics');
metrics.onmessage = (event) => {
const data = JSON.parse(event.data);
// ์ฐจํธ ์
๋ฐ์ดํธ
updateChart({
cpu: data.cpu,
memory: data.memory,
requests: data.requests
});
};์ฑํ ๋ฉ์์ง ์์ (๋จ๋ฐฉํฅ)
const chat = new EventSource('/api/chat/messages');
chat.addEventListener('new-message', (event) => {
const message = JSON.parse(event.data);
appendMessageToChat({
user: message.user,
text: message.text,
timestamp: message.timestamp
});
});