๐ ์๋ฐ์คํฌ๋ฆฝํธ๋ก ๊ตฌํํ๋ Server-Sent Events: ์ค์๊ฐ ๋ฐ์ดํฐ ํต์ ์ ์๋ก์ด ํจ๋ฌ๋ค์ ๐

์๋ , ๊ฐ๋ฐ์ ์น๊ตฌ๋ค! ์ค๋์ 2025๋ 3์ 20์ผ, ์ค์๊ฐ ์น ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ ํต์ฌ ๊ธฐ์ ์ธ Server-Sent Events(SSE)์ ๋ํด ํจ๊ป ์์๋ณผ ๊ฑฐ์ผ. ๋ณต์กํ ์๋ฐฉํฅ ํต์ ์์ด๋ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ๋ฐ์ดํฐ๋ฅผ ์ค์๊ฐ์ผ๋ก ๋ณด๋ผ ์ ์๋ ์ด ๊ธฐ์ , ์ด๋ป๊ฒ ํ๋ฉด ์๋ฐ์คํฌ๋ฆฝํธ๋ก ์ฝ๊ณ ํจ์จ์ ์ผ๋ก ๊ตฌํํ ์ ์์์ง ์ฌ๋ฐ๊ฒ ํํค์ณ ๋ณด์! ๐
๐ ๋ชฉ์ฐจ
- Server-Sent Events๋ ๋ฌด์์ธ๊ฐ?
- SSE vs WebSocket: ์ธ์ ๋ฌด์์ ์ ํํด์ผ ํ ๊น?
- SSE ๊ธฐ๋ณธ ๊ตฌํํ๊ธฐ
- ์ค์ ์์ : ์ค์๊ฐ ์๋ฆผ ์์คํ ๋ง๋ค๊ธฐ
- SSE์ ๊ณ ๊ธ ๊ธฐ๋ฅ ํ์ฉํ๊ธฐ
- ์ฑ๋ฅ ์ต์ ํ ๋ฐ ์๋ฌ ํธ๋ค๋ง
- ๋ธ๋ผ์ฐ์ ํธํ์ฑ๊ณผ ๋์ ๋ฐฉ์
- 2025๋ ์ต์ SSE ํ์ฉ ํธ๋ ๋
- ๋ง๋ฌด๋ฆฌ ๋ฐ ์ถ๊ฐ ์๋ฃ
1. Server-Sent Events๋ ๋ฌด์์ธ๊ฐ? ๐ค
Server-Sent Events(SSE)๋ HTTP ์ฐ๊ฒฐ์ ํตํด ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ์๋ ์ ๋ฐ์ดํธ๋ฅผ ํธ์ํ๋ ๊ธฐ์ ์ด์ผ. ์น์์ผ๊ณผ ๋ฌ๋ฆฌ ๋จ๋ฐฉํฅ ํต์ ๋ง ์ง์ํ์ง๋ง, ๊ทธ๋งํผ ๊ตฌํ์ด ๊ฐ๋จํ๊ณ HTTP ํ๋กํ ์ฝ์ ๊ทธ๋๋ก ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๊ธฐ์กด ์ธํ๋ผ์์ ํธํ์ฑ์ด ๋ฐ์ด๋๋ค๋ ์ฅ์ ์ด ์์ด.
SSE์ ํต์ฌ ํน์ง โจ
- ๋จ๋ฐฉํฅ ํต์ - ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก๋ง ๋ฐ์ดํฐ๊ฐ ํ๋ฆ
- ์๋ ์ฌ์ฐ๊ฒฐ - ์ฐ๊ฒฐ์ด ๋์ด์ ธ๋ ์๋์ผ๋ก ๋ค์ ์ฐ๊ฒฐ ์๋
- ์ด๋ฒคํธ ID - ๊ฐ ์ด๋ฒคํธ์ ID๋ฅผ ๋ถ์ฌํด ์ฐ๊ฒฐ์ด ๋๊ธด ํ ๋ง์ง๋ง์ผ๋ก ๋ฐ์ ์ด๋ฒคํธ๋ถํฐ ๋ค์ ์์ ๊ฐ๋ฅ
- ํ์ค HTTP ์ฌ์ฉ - ํน๋ณํ ํ๋กํ ์ฝ์ด ํ์ ์์ด ๋ฐฉํ๋ฒฝ์ด๋ ํ๋ก์ ๋ฌธ์ ๊ฐ ์ ์
- ํ ์คํธ ๊ธฐ๋ฐ ํต์ - ์ฃผ๋ก ํ ์คํธ ๋ฐ์ดํฐ ์ ์ก์ ์ต์ ํ๋จ
SSE๋ ์ฃผ์ ์์ธ, ์์ ๋ฏธ๋์ด ํผ๋, ์๋ฆผ ์์คํ , ์ค์๊ฐ ๋ถ์ ๋์๋ณด๋ ๋ฑ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ์ง์์ ์ธ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ๊ฐ ํ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ ํนํ ์ ์ฉํด. 2025๋ ํ์ฌ, ๋ง์ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ๋ค์ด ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๊ธฐ ์ํด SSE๋ฅผ ํ์ฉํ๊ณ ์์ง!
2. SSE vs WebSocket: ์ธ์ ๋ฌด์์ ์ ํํด์ผ ํ ๊น? ๐คทโโ๏ธ
์ค์๊ฐ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋ฐํ ๋ ํญ์ ๋ง์ฃผ์น๋ ์ง๋ฌธ์ด ์์ด. "WebSocket์ ์จ์ผ ํ ๊น, SSE๋ฅผ ์จ์ผ ํ ๊น?" ๋ ๊ธฐ์ ๋ชจ๋ ์ค์๊ฐ ํต์ ์ ๊ฐ๋ฅํ๊ฒ ํ์ง๋ง, ๊ฐ๊ฐ์ ํน์ฑ๊ณผ ์ฅ๋จ์ ์ด ๋ฌ๋ผ. ์ด ์ฐจ์ด์ ์ ์ดํดํ๋ฉด ํ๋ก์ ํธ์ ๋ง๋ ๊ธฐ์ ์ ์ ํํ๋ ๋ฐ ๋์์ด ๋ ๊ฑฐ์ผ.
์ธ์ SSE๋ฅผ ์ ํํด์ผ ํ ๊น? ๐ฏ
- ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก์ ๋จ๋ฐฉํฅ ํต์ ์ด ์ฃผ์ ์๊ตฌ์ฌํญ์ผ ๋
- ์๋ฆผ, ๋ด์ค ํผ๋, ์ฃผ์ ์์ธ ๋ฑ ์ค์๊ฐ ์ ๋ฐ์ดํธ๊ฐ ํ์ํ ๋
- ๊ธฐ์กด HTTP ์ธํ๋ผ๋ฅผ ํ์ฉํ๊ณ ์ถ์ ๋
- ๊ตฌํ ๋ณต์ก๋๋ฅผ ๋ฎ์ถ๊ณ ์ถ์ ๋
- ์ฐ๊ฒฐ์ด ๋๊ฒผ์ ๋ ์๋ ์ฌ์ฐ๊ฒฐ๊ณผ ์ด๋ฒคํธ ๋ณต๊ตฌ๊ฐ ์ค์ํ ๋
์ค์ ์ฌ๋ก: ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์๋ก์ด ํ๋ก์ ํธ ์ ์์ด๋ ๋ฉ์์ง ์๋ฆผ์ ์ค์๊ฐ์ผ๋ก ๋ฐ๊ณ ์ถ์ ๋, SSE๋ ์๋ฒฝํ ์ ํ์ด์ผ. ์ฌ์ฉ์๊ฐ ์๋ฒ๋ก ์ง์์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ํ์ ์์ด, ์ค์ํ ์ ๋ฐ์ดํธ๋ง ๋ฐ์ผ๋ฉด ๋๋๊น!
๋ฌผ๋ก , ์ฑํ ์ ํ๋ฆฌ์ผ์ด์ ์ฒ๋ผ ์๋ฐฉํฅ ์ค์๊ฐ ํต์ ์ด ํ์ํ ๊ฒฝ์ฐ์๋ WebSocket์ด ๋ ์ ํฉํด. ํ์ง๋ง ๋ง์ ๊ฒฝ์ฐ SSE๋ง์ผ๋ก๋ ์ถฉ๋ถํ๋ฉฐ, ๊ตฌํ๋ ํจ์ฌ ๊ฐ๋จํ๋ค๋ ์ฅ์ ์ด ์์ด.
3. SSE ๊ธฐ๋ณธ ๊ตฌํํ๊ธฐ ๐ป
์ด์ ์๋ฐ์คํฌ๋ฆฝํธ๋ก SSE๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ์๋ฒ ์ธก๊ณผ ํด๋ผ์ด์ธํธ ์ธก ๋ชจ๋ ์ดํด๋ณผ ๊ฑฐ์ผ.
ํด๋ผ์ด์ธํธ ์ธก ๊ตฌํ (๋ธ๋ผ์ฐ์ )
๋ธ๋ผ์ฐ์ ์์ SSE ์ฐ๊ฒฐ์ ์ค์ ํ๋ ๊ฒ์ ์ ๋ง ๊ฐ๋จํด. EventSource API๋ฅผ ์ฌ์ฉํ๋ฉด ๋ผ:
// SSE ์ฐ๊ฒฐ ์์ฑํ๊ธฐ
const eventSource = new EventSource('/events');
// ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ๋กํ๊ธฐ
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('์๋ก์ด ๋ฐ์ดํฐ ์์ :', data);
updateUI(data);
};
// ์ฐ๊ฒฐ ์ด๋ฆผ ์ด๋ฒคํธ
eventSource.onopen = function() {
console.log('SSE ์ฐ๊ฒฐ์ด ์ด๋ ธ์ต๋๋ค!');
};
// ์๋ฌ ์ฒ๋ฆฌ
eventSource.onerror = function(error) {
console.error('SSE ์ฐ๊ฒฐ ์ค๋ฅ:', error);
// ํ์์ ๋ฐ๋ผ ์ฌ์ฐ๊ฒฐ ๋ก์ง ์ถ๊ฐ
};
// ํน์ ์ด๋ฒคํธ ํ์
๋ฆฌ์ค๋
eventSource.addEventListener('update', function(event) {
const updateData = JSON.parse(event.data);
console.log('์
๋ฐ์ดํธ ์ด๋ฒคํธ:', updateData);
});
// ์ฐ๊ฒฐ ์ข
๋ฃ (ํ์ํ ๋)
function closeConnection() {
eventSource.close();
}
โ ๏ธ ์ฃผ์์ฌํญ: EventSource๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฐ๊ฒฐ์ด ๋์ด์ง๋ฉด ์๋์ผ๋ก ์ฌ์ฐ๊ฒฐ์ ์๋ํด. ์ด ๋์์ ์ ์ดํ๊ณ ์ถ๋ค๋ฉด onerror ํธ๋ค๋ฌ์์ ์ ์ ํ ๋ก์ง์ ๊ตฌํํด์ผ ํด.
์๋ฒ ์ธก ๊ตฌํ
์๋ฒ ์ธก์์๋ ์ฌ๋ฐ๋ฅธ ํค๋์ ํ์์ผ๋ก ์๋ต์ ๋ณด๋ด์ผ ํด. ์ฌ๊ธฐ์๋ Node.js์ Express๋ฅผ ์ฌ์ฉํ ์์ ๋ฅผ ์ดํด๋ณด์:
const express = require('express');
const app = express();
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.write('data: {"message": "์ฐ๊ฒฐ ์ฑ๊ณต!"}\n\n');
// 5์ด๋ง๋ค ๋ฐ์ดํฐ ์ ์ก (์์)
const intervalId = setInterval(() => {
const data = {
time: new Date().toISOString(),
value: Math.random() * 100
};
// ์ผ๋ฐ ๋ฉ์์ง ์ ์ก
res.write(`data: ${JSON.stringify(data)}\n\n`);
// ํน์ ์ด๋ฒคํธ ํ์
์ผ๋ก ์ ์ก
res.write(`event: update\ndata: ${JSON.stringify({ updated: true, timestamp: Date.now() })}\n\n`);
}, 5000);
// ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ์ด ๋์ด์ง๋ฉด ์ธํฐ๋ฒ ์ ๋ฆฌ
req.on('close', () => {
clearInterval(intervalId);
console.log('ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ ์ข
๋ฃ');
});
});
app.listen(3000, () => {
console.log('SSE ์๋ฒ๊ฐ ํฌํธ 3000์์ ์คํ ์ค์
๋๋ค');
});
SSE ๋ฉ์์ง ํ์ ์ดํดํ๊ธฐ ๐
SSE ๋ฉ์์ง๋ ํน์ ํ์์ ๋ฐ๋ผ์ผ ํด:
event: ์ด๋ฒคํธ๋ช
id: ์ด๋ฒคํธID
data: ์ค์ ๋ฐ์ดํฐ
retry: ์ฌ์ฐ๊ฒฐ ์๊ฐ(ms)
๊ฐ ํ๋๋ ์ ํ์ ์ด์ง๋ง, ์ ์ด๋ 'data' ํ๋๋ ํฌํจํด์ผ ํด. ๊ทธ๋ฆฌ๊ณ ๊ฐ ๋ฉ์์ง๋ ๋ฐ๋์ ๋น ์ค(\n\n)๋ก ๋๋์ผ ํ๋ค๋ ์ ์ ๊ธฐ์ตํด!
4. ์ค์ ์์ : ์ค์๊ฐ ์๋ฆผ ์์คํ ๋ง๋ค๊ธฐ ๐
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ด์ ์ค์ ๋ก ์ค์๊ฐ ์๋ฆผ ์์คํ ์ ๊ตฌํํด๋ณด์! ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ฌ์ฉ์์๊ฒ ์๋ก์ด ๋ฉ์์ง, ํ๋ก์ ํธ ์ ์, ๋๋ ์ค์ ์ ๋ฐ์ดํธ๋ฅผ ์ค์๊ฐ์ผ๋ก ์๋ ค์ฃผ๋ ๊ธฐ๋ฅ์ ๋ง๋ค์ด๋ณผ ๊ฑฐ์ผ.
์๋ฒ ์ธก ์ฝ๋ (Node.js + Express)
const express = require('express');
const cors = require('cors');
const app = express();
// ํด๋ผ์ด์ธํธ ๋ชฉ๋ก ๊ด๋ฆฌ
const clients = new Map();
// ์๋ฆผ ์ ์ฅ์ (์ค์ ๋ก๋ DB๋ฅผ ์ฌ์ฉํ ๊ฒ)
const notifications = [];
app.use(cors());
app.use(express.json());
// SSE ์๋ํฌ์ธํธ
app.get('/notifications/stream', (req, res) => {
const clientId = req.query.clientId || Date.now();
console.log(`ํด๋ผ์ด์ธํธ ${clientId} ์ฐ๊ฒฐ๋จ`);
// SSE ํค๋ ์ค์
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// ํด๋ผ์ด์ธํธ์ ID ์ ์ก
res.write(`data: ${JSON.stringify({ type: 'connection', clientId })}\n\n`);
// ๋ง์ง๋ง ์๋ฆผ ID ํ์ธ
const lastEventId = req.headers['last-event-id'] || '0';
// ์ฐ๊ฒฐ์ด ๋๊ธด ํ ๋์น ์๋ฆผ ์ ์ก
if (lastEventId !== '0') {
const missedNotifications = notifications.filter(n => n.id > parseInt(lastEventId));
if (missedNotifications.length) {
missedNotifications.forEach(notification => {
res.write(`id: ${notification.id}\n`);
res.write(`data: ${JSON.stringify(notification)}\n\n`);
});
}
}
// ํด๋ผ์ด์ธํธ ๋ฑ๋ก
const newClient = {
id: clientId,
response: res
};
clients.set(clientId, newClient);
// ์ฐ๊ฒฐ ์ข
๋ฃ ์ฒ๋ฆฌ
req.on('close', () => {
console.log(`ํด๋ผ์ด์ธํธ ${clientId} ์ฐ๊ฒฐ ์ข
๋ฃ`);
clients.delete(clientId);
});
});
// ์ ์๋ฆผ ์์ฑ API
app.post('/notifications', (req, res) => {
const notification = {
id: Date.now(),
title: req.body.title,
message: req.body.message,
type: req.body.type || 'info',
timestamp: new Date().toISOString()
};
notifications.push(notification);
// ๋ชจ๋ ์ฐ๊ฒฐ๋ ํด๋ผ์ด์ธํธ์ ์๋ฆผ ์ ์ก
clients.forEach(client => {
client.response.write(`id: ${notification.id}\n`);
client.response.write(`data: ${JSON.stringify(notification)}\n\n`);
});
res.status(201).json({ success: true, notification });
});
// ์๋ฆผ ๋ชฉ๋ก ์กฐํ API
app.get('/notifications', (req, res) => {
res.json(notifications);
});
app.listen(3000, () => {
console.log('์๋ฆผ ์๋ฒ๊ฐ ํฌํธ 3000์์ ์คํ ์ค์
๋๋ค');
});
ํด๋ผ์ด์ธํธ ์ธก ์ฝ๋ (HTML + JavaScript)
<!-- index.html -->
<div class="notification-container">
<h3>์ค์๊ฐ ์๋ฆผ</h3>
<div id="notifications-list"></div>
</div>
<script>
// ํด๋ผ์ด์ธํธ ID (์ค์ ๋ก๋ ์ฌ์ฉ์ ์ธ์ฆ ํ ์ค์ )
const clientId = localStorage.getItem('clientId') || Date.now();
localStorage.setItem('clientId', clientId);
// ๋ง์ง๋ง ์ด๋ฒคํธ ID ์ ์ฅ
let lastEventId = localStorage.getItem('lastEventId') || '0';
// ์๋ฆผ ๋ชฉ๋ก ์ด๊ธฐํ
fetchNotifications();
// SSE ์ฐ๊ฒฐ ์ค์
const eventSource = new EventSource(`/notifications/stream?clientId=${clientId}`);
// ๋ฉ์์ง ์์ ์ฒ๋ฆฌ
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
// ์ฐ๊ฒฐ ํ์ธ ๋ฉ์์ง ๋ฌด์
if (data.type === 'connection') {
console.log('SSE ์ฐ๊ฒฐ ์ฑ๊ณต:', data);
return;
}
// ์ ์๋ฆผ ์ฒ๋ฆฌ
console.log('์ ์๋ฆผ ์์ :', data);
addNotificationToUI(data);
// ๋ง์ง๋ง ์ด๋ฒคํธ ID ์ ์ฅ
if (event.lastEventId) {
lastEventId = event.lastEventId;
localStorage.setItem('lastEventId', lastEventId);
}
};
// ์๋ฌ ์ฒ๋ฆฌ
eventSource.onerror = function(error) {
console.error('SSE ์ฐ๊ฒฐ ์ค๋ฅ:', error);
// ํ์์ ์ฌ์ฐ๊ฒฐ ๋ก์ง ์ถ๊ฐ
};
// ์๋ฆผ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
async function fetchNotifications() {
try {
const response = await fetch('/notifications');
const notifications = await response.json();
// ๊ธฐ์กด ์๋ฆผ ํ์
notifications.forEach(notification => {
addNotificationToUI(notification);
});
} catch (error) {
console.error('์๋ฆผ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ ์คํจ:', error);
}
}
// UI์ ์๋ฆผ ์ถ๊ฐ
function addNotificationToUI(notification) {
const notificationsList = document.getElementById('notifications-list');
const notificationElement = document.createElement('div');
notificationElement.className = `notification ${notification.type}`;
const timestamp = new Date(notification.timestamp).toLocaleTimeString();
notificationElement.innerHTML = `
<div class="notification-header">
<span class="notification-title">${notification.title}</span>
<span class="notification-time">${timestamp}</span>
</div>
<div class="notification-message">${notification.message}</div>
`;
// ์ ์๋ฆผ์ ๋ชฉ๋ก ์๋จ์ ์ถ๊ฐ
notificationsList.prepend(notificationElement);
// ์ ๋๋ฉ์ด์
ํจ๊ณผ
setTimeout(() => {
notificationElement.classList.add('show');
}, 10);
// 5์ด ํ ์๋ฆผ ์คํ์ผ ๋ณ๊ฒฝ
setTimeout(() => {
notificationElement.classList.add('read');
}, 5000);
}
</script>
์๋ฆผ ์คํ์ผ๋ง์ ์ํ CSS:
/* ์๋ฆผ ์ปจํ
์ด๋ ์คํ์ผ */
.notification-container {
max-width: 100%;
margin: 20px auto;
padding: 15px;
border-radius: 8px;
background-color: #f8f9fa;
}
/* ๊ฐ๋ณ ์๋ฆผ ์คํ์ผ */
.notification {
padding: 12px 15px;
margin-bottom: 10px;
border-radius: 6px;
border-left: 4px solid #ccc;
background-color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
opacity: 0;
transform: translateY(10px);
transition: all 0.3s ease;
}
.notification.show {
opacity: 1;
transform: translateY(0);
}
.notification.read {
opacity: 0.7;
}
/* ์๋ฆผ ํ์
๋ณ ์คํ์ผ */
.notification.info {
border-left-color: #3498db;
}
.notification.success {
border-left-color: #2ecc71;
}
.notification.warning {
border-left-color: #f39c12;
}
.notification.error {
border-left-color: #e74c3c;
}
/* ์๋ฆผ ํค๋ ์คํ์ผ */
.notification-header {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
}
.notification-title {
font-weight: bold;
}
.notification-time {
font-size: 0.8em;
color: #777;
}
/* ์๋ฆผ ๋ฉ์์ง ์คํ์ผ */
.notification-message {
color: #333;
}
๐ก ๊ตฌํ ํ
์ ์์ ๋ฅผ ํ์ฅํ๋ฉด ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ค์๊ณผ ๊ฐ์ ์๋ฆผ์ ์ค์๊ฐ์ผ๋ก ์ ๊ณตํ ์ ์์ด:
- ์๋ก์ด ๋ฉ์์ง ๋๋ ๋๊ธ ์๋ฆผ
- ํ๋ก์ ํธ ์ ์ ๋ฐ ์๋ฝ ์๋ฆผ
- ๊ฒฐ์ ๋ฐ ์ ์ฐ ๊ด๋ จ ์๋ฆผ
- ์์คํ ๊ณต์ง์ฌํญ
์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๋ ค๋ฉด ์๋ฆผ์ ์๋ฆฌ๋ ๋ธ๋ผ์ฐ์ ์๋ฆผ(Notification API)์ ์ถ๊ฐํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ!
5. SSE์ ๊ณ ๊ธ ๊ธฐ๋ฅ ํ์ฉํ๊ธฐ ๐
๊ธฐ๋ณธ์ ์ธ SSE ๊ตฌํ์ ๋์ด, ๋ ๊ฐ๋ ฅํ๊ณ ํจ์จ์ ์ธ ์ค์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค๊ธฐ ์ํ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์ดํด๋ณด์!
์ด๋ฒคํธ ID์ ์ฌ์ฐ๊ฒฐ ๊ด๋ฆฌ
์ด๋ฒคํธ ID๋ SSE์ ๊ฐ์ฅ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ ์ค ํ๋์ผ. ์ฐ๊ฒฐ์ด ๋์ด์ก๋ค๊ฐ ๋ค์ ์ฐ๊ฒฐ๋ ๋, ํด๋ผ์ด์ธํธ๊ฐ ๋ง์ง๋ง์ผ๋ก ๋ฐ์ ์ด๋ฒคํธ ์ดํ์ ๋ฐ์ดํฐ๋ง ๋ฐ์ ์ ์๊ฒ ํด์ค.
// ์๋ฒ ์ธก: ์ด๋ฒคํธ 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');
// ๋ง์ง๋ง ์ด๋ฒคํธ ID ํ์ธ
const lastEventId = req.headers['last-event-id'] || '0';
console.log(`ํด๋ผ์ด์ธํธ์ ๋ง์ง๋ง ์ด๋ฒคํธ ID: ${lastEventId}`);
// ๋์น ์ด๋ฒคํธ ์ ์ก (DB์์ ์กฐํ)
sendMissedEvents(res, lastEventId);
// ์ ์ด๋ฒคํธ ์ ์ก
const intervalId = setInterval(() => {
const eventId = Date.now();
const data = { value: Math.random() };
res.write(`id: ${eventId}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);
// ์ฐ๊ฒฐ ์ข
๋ฃ ์ฒ๋ฆฌ
req.on('close', () => {
clearInterval(intervalId);
});
});
์ฌ์ฐ๊ฒฐ ์๊ฐ ์กฐ์
๊ธฐ๋ณธ์ ์ผ๋ก ๋ธ๋ผ์ฐ์ ๋ ์ฐ๊ฒฐ์ด ๋์ด์ง๋ฉด ์ฝ 3์ด ํ์ ์ฌ์ฐ๊ฒฐ์ ์๋ํด. ์ด ์๊ฐ์ ์กฐ์ ํ๊ณ ์ถ๋ค๋ฉด retry ํ๋๋ฅผ ์ฌ์ฉํ ์ ์์ด:
// ์๋ฒ ์ธก: ์ฌ์ฐ๊ฒฐ ์๊ฐ ์ค์ (10์ด)
res.write('retry: 10000\n');
res.write('data: {"message": "10์ด ํ์ ์ฌ์ฐ๊ฒฐ๋ฉ๋๋ค"}\n\n');
์ฌ๋ฌ ์ด๋ฒคํธ ํ์ ํ์ฉํ๊ธฐ
SSE๋ ๊ธฐ๋ณธ 'message' ์ด๋ฒคํธ ์ธ์๋ ์ปค์คํ ์ด๋ฒคํธ ํ์ ์ ์ง์ํด. ์ด๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์์ ์ด๋ฒคํธ ํ์ ๋ณ๋ก ๋ค๋ฅธ ์ฒ๋ฆฌ๋ฅผ ํ ์ ์์ด:
// ์๋ฒ ์ธก: ๋ค์ํ ์ด๋ฒคํธ ํ์
์ ์ก
function sendEvent(res, eventType, data, id) {
if (id) res.write(`id: ${id}\n`);
if (eventType !== 'message') res.write(`event: ${eventType}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
// ์ฌ์ฉ ์์
sendEvent(res, 'update', { status: 'updated', item: 'profile' }, 1001);
sendEvent(res, 'alert', { level: 'warning', message: '์ธ์
์ด ๊ณง ๋ง๋ฃ๋ฉ๋๋ค' }, 1002);
sendEvent(res, 'message', { text: '์ผ๋ฐ ๋ฉ์์ง์
๋๋ค' }, 1003);
// ํด๋ผ์ด์ธํธ ์ธก: ์ด๋ฒคํธ ํ์
๋ณ ์ฒ๋ฆฌ
const eventSource = new EventSource('/events');
// ๊ธฐ๋ณธ ๋ฉ์์ง ์ด๋ฒคํธ
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('๊ธฐ๋ณธ ๋ฉ์์ง:', data);
};
// ์
๋ฐ์ดํธ ์ด๋ฒคํธ
eventSource.addEventListener('update', function(event) {
const data = JSON.parse(event.data);
console.log('์
๋ฐ์ดํธ ์ด๋ฒคํธ:', data);
refreshUI(data);
});
// ์๋ฆผ ์ด๋ฒคํธ
eventSource.addEventListener('alert', function(event) {
const data = JSON.parse(event.data);
console.log('์๋ฆผ ์ด๋ฒคํธ:', data);
showAlert(data);
});
์ธ์ฆ ๋ฐ ๋ณด์
์ค์ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ SSE ์ฐ๊ฒฐ์๋ ์ธ์ฆ์ด ํ์ํด. ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํํ ์ ์์ด:
// ํด๋ผ์ด์ธํธ ์ธก: ์ธ์ฆ ํ ํฐ ํฌํจ
const token = localStorage.getItem('authToken');
const eventSource = new EventSource(`/events?token=${token}`);
// ์๋ฒ ์ธก: ์ธ์ฆ ํ์ธ
app.get('/events', (req, res) => {
const token = req.query.token;
// ํ ํฐ ๊ฒ์ฆ
if (!isValidToken(token)) {
return res.status(401).send('Unauthorized');
}
// ์ฌ์ฉ์ ID ์ถ์ถ
const userId = getUserIdFromToken(token);
// ์ดํ SSE ์ค์ ๋ฐ ์ฌ์ฉ์๋ณ ์ด๋ฒคํธ ์ ์ก
// ...
});
๐ SSE ํ์ฉ ๋ชจ๋ฒ ์ฌ๋ก
- ์ด๋ฒคํธ ๊ทธ๋ฃนํ - ์ฌ๋ฌ ์์ ์ ๋ฐ์ดํธ๋ฅผ ํ๋์ ์ด๋ฒคํธ๋ก ๋ฌถ์ด ์ ์ก
- ์ด๋ฒคํธ ํํฐ๋ง - ํด๋ผ์ด์ธํธ๋ณ๋ก ๊ด๋ จ ์๋ ์ด๋ฒคํธ๋ง ์ ์ก
- ๋ฐฑ์คํ ์ ๋ต - ์ฐ๊ฒฐ ์คํจ ์ ์ ์ง์ ์ผ๋ก ์ฌ์๋ ๊ฐ๊ฒฉ ์ฆ๊ฐ
- ํฌ์ค์ฒดํฌ - ์ฃผ๊ธฐ์ ์ธ ping ์ด๋ฒคํธ๋ก ์ฐ๊ฒฐ ์ํ ํ์ธ
- ์ด๋ฒคํธ ์์ถ - ๋๋์ ๋ฐ์ดํฐ ์ ์ก ์ ์์ถ ๊ณ ๋ ค
6. ์ฑ๋ฅ ์ต์ ํ ๋ฐ ์๋ฌ ํธ๋ค๋ง โก
SSE๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ฐ๋ณ๊ณ ํจ์จ์ ์ด์ง๋ง, ๋๊ท๋ชจ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ์ฑ๋ฅ ์ต์ ํ์ ์๋ฌ ์ฒ๋ฆฌ๊ฐ ์ค์ํด. ์ด ์น์ ์์๋ SSE ๊ตฌํ์ ๋ ๊ฒฌ๊ณ ํ๊ฒ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์์๋ณด์!
์๋ฒ ์ธก ์ฑ๋ฅ ์ต์ ํ
- ์ฐ๊ฒฐ ์ ๊ด๋ฆฌ - ์๋ฒ๋น ๋์ ์ฐ๊ฒฐ ์๋ฅผ ๋ชจ๋ํฐ๋งํ๊ณ ์ ํ
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๊ด๋ฆฌ - ํด๋ผ์ด์ธํธ ๋ชฉ๋ก๊ณผ ์ด๋ฒคํธ ํ์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ ์ต์ ํ
- ์ด๋ฒคํธ ๋ฐฐ์น ์ฒ๋ฆฌ - ์ฌ๋ฌ ์ด๋ฒคํธ๋ฅผ ๋ฌถ์ด์ ์ ์ก
- Redis ํ์ฉ - ๋ค์ค ์๋ฒ ํ๊ฒฝ์์ ์ด๋ฒคํธ ๋ฐํ/๊ตฌ๋ ํจํด ๊ตฌํ
๋ค์ค ์๋ฒ ํ๊ฒฝ์์ Redis๋ฅผ ํ์ฉํ SSE ๊ตฌํ ์์ :
const express = require('express');
const Redis = require('ioredis');
const app = express();
// Redis ํด๋ผ์ด์ธํธ ์ค์
const subscriber = new Redis();
const publisher = new Redis();
// ํด๋ผ์ด์ธํธ ๊ด๋ฆฌ
const clients = new Map();
// SSE ์๋ํฌ์ธํธ
app.get('/events', (req, res) => {
const clientId = req.query.clientId;
// SSE ํค๋ ์ค์
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// ํด๋ผ์ด์ธํธ ๋ฑ๋ก
clients.set(clientId, res);
// Redis ์ฑ๋ ๊ตฌ๋
subscriber.subscribe(`events:${clientId}`, 'events:all');
// ๋ฉ์์ง ์์ ์ฒ๋ฆฌ
const messageHandler = (channel, message) => {
try {
const eventData = JSON.parse(message);
const clientResponse = clients.get(clientId);
if (clientResponse) {
if (eventData.id) clientResponse.write(`id: ${eventData.id}\n`);
if (eventData.event) clientResponse.write(`event: ${eventData.event}\n`);
clientResponse.write(`data: ${JSON.stringify(eventData.data)}\n\n`);
}
} catch (error) {
console.error('๋ฉ์์ง ์ฒ๋ฆฌ ์ค๋ฅ:', error);
}
};
subscriber.on('message', messageHandler);
// ์ฐ๊ฒฐ ์ข
๋ฃ ์ฒ๋ฆฌ
req.on('close', () => {
subscriber.unsubscribe(`events:${clientId}`, 'events:all');
subscriber.removeListener('message', messageHandler);
clients.delete(clientId);
console.log(`ํด๋ผ์ด์ธํธ ${clientId} ์ฐ๊ฒฐ ์ข
๋ฃ`);
});
});
// ์ด๋ฒคํธ ๋ฐํ API
app.post('/publish', express.json(), (req, res) => {
const { channel, event, data } = req.body;
const eventData = {
id: Date.now(),
event,
data,
timestamp: new Date().toISOString()
};
// Redis๋ฅผ ํตํด ์ด๋ฒคํธ ๋ฐํ
publisher.publish(channel, JSON.stringify(eventData));
res.json({ success: true });
});
app.listen(3000, () => {
console.log('SSE ์๋ฒ๊ฐ ํฌํธ 3000์์ ์คํ ์ค์
๋๋ค');
});
ํด๋ผ์ด์ธํธ ์ธก ์๋ฌ ํธ๋ค๋ง
ํด๋ผ์ด์ธํธ์์๋ ์ฐ๊ฒฐ ์ค๋ฅ์ ์ฌ์ฐ๊ฒฐ ๋ก์ง์ ์ ์ฒ๋ฆฌํ๋ ๊ฒ์ด ์ค์ํด:
class EnhancedEventSource {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.eventSource = null;
this.listeners = new Map();
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
this.reconnectInterval = options.reconnectInterval || 1000;
this.maxReconnectInterval = options.maxReconnectInterval || 30000;
this.lastEventId = options.lastEventId || '';
this.connect();
}
connect() {
// ๊ธฐ์กด ์ฐ๊ฒฐ ์ ๋ฆฌ
if (this.eventSource) {
this.eventSource.close();
}
// URL์ ๋ง์ง๋ง ์ด๋ฒคํธ ID ์ถ๊ฐ
const connectUrl = new URL(this.url, window.location.origin);
if (this.lastEventId) {
connectUrl.searchParams.set('lastEventId', this.lastEventId);
}
// ์ EventSource ์์ฑ
this.eventSource = new EventSource(connectUrl.toString());
// ๊ธฐ๋ณธ ์ด๋ฒคํธ ํธ๋ค๋ฌ ์ค์
this.eventSource.onopen = this.handleOpen.bind(this);
this.eventSource.onerror = this.handleError.bind(this);
this.eventSource.onmessage = this.handleMessage.bind(this);
// ์ ์ฅ๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ค์ ๋ฑ๋ก
this.listeners.forEach((listeners, event) => {
listeners.forEach(listener => {
this.eventSource.addEventListener(event, listener);
});
});
}
handleOpen(event) {
console.log('SSE ์ฐ๊ฒฐ ์ฑ๊ณต');
this.reconnectAttempts = 0;
if (this.options.onOpen) {
this.options.onOpen(event);
}
}
handleError(error) {
console.error('SSE ์ฐ๊ฒฐ ์ค๋ฅ:', error);
// EventSource๊ฐ ๋ซํ์ ๋๋ง ์ฌ์ฐ๊ฒฐ ์๋
if (this.eventSource.readyState === EventSource.CLOSED) {
this.reconnect();
}
if (this.options.onError) {
this.options.onError(error);
}
}
handleMessage(event) {
// ๋ง์ง๋ง ์ด๋ฒคํธ ID ์ ์ฅ
if (event.lastEventId) {
this.lastEventId = event.lastEventId;
}
if (this.options.onMessage) {
this.options.onMessage(event);
}
}
reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('์ต๋ ์ฌ์ฐ๊ฒฐ ์๋ ํ์ ์ด๊ณผ');
if (this.options.onMaxReconnectAttempts) {
this.options.onMaxReconnectAttempts();
}
return;
}
// ์ง์ ๋ฐฑ์คํ๋ก ์ฌ์ฐ๊ฒฐ ๊ฐ๊ฒฉ ๊ณ์ฐ
const delay = Math.min(
this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts),
this.maxReconnectInterval
);
console.log(`${delay}ms ํ ์ฌ์ฐ๊ฒฐ ์๋ (${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
addEventListener(event, listener) {
// ๋ฆฌ์ค๋ ๋งต์ ์ถ๊ฐ
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event).add(listener);
// ์ค์ EventSource์ ๋ฆฌ์ค๋ ์ถ๊ฐ
if (this.eventSource) {
this.eventSource.addEventListener(event, listener);
}
return this;
}
removeEventListener(event, listener) {
// ๋ฆฌ์ค๋ ๋งต์์ ์ ๊ฑฐ
if (this.listeners.has(event)) {
this.listeners.get(event).delete(listener);
}
// ์ค์ EventSource์์ ๋ฆฌ์ค๋ ์ ๊ฑฐ
if (this.eventSource) {
this.eventSource.removeEventListener(event, listener);
}
return this;
}
close() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
}
// ์ฌ์ฉ ์์
const sse = new EnhancedEventSource('/events', {
lastEventId: localStorage.getItem('lastEventId'),
maxReconnectAttempts: 15,
reconnectInterval: 2000,
onOpen: () => console.log('์ฐ๊ฒฐ๋จ'),
onError: (error) => console.error('์ค๋ฅ ๋ฐ์:', error),
onMessage: (event) => {
const data = JSON.parse(event.data);
console.log('๋ฉ์์ง ์์ :', data);
// ๋ง์ง๋ง ์ด๋ฒคํธ ID ์ ์ฅ
if (event.lastEventId) {
localStorage.setItem('lastEventId', event.lastEventId);
}
},
onMaxReconnectAttempts: () => {
alert('์๋ฒ ์ฐ๊ฒฐ์ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ํ์ด์ง๋ฅผ ์๋ก๊ณ ์นจํด ์ฃผ์ธ์.');
}
});
// ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ๋ก
sse.addEventListener('update', (event) => {
const data = JSON.parse(event.data);
updateUI(data);
});
โก ์ฑ๋ฅ ์ต์ ํ ํ
- ์ด๋ฒคํธ ํํฐ๋ง - ํด๋ผ์ด์ธํธ์ ํ์ํ ์ด๋ฒคํธ๋ง ์ ์ก
- ์ด๋ฒคํธ ๋ฐฐ์น ์ฒ๋ฆฌ - ์งง์ ์๊ฐ ๋ด ์ฌ๋ฌ ์ด๋ฒคํธ๋ฅผ ๋ฌถ์ด์ ์ ์ก
- ๋ฐ์ดํฐ ์์ถ - ๋์ฉ๋ ๋ฐ์ดํฐ ์ ์ก ์ ์์ถ ๊ณ ๋ ค
- ์ฐ๊ฒฐ ํ๋ง - ์ฌ๋ฌ ์ด๋ฒคํธ ํ์ ์ ํ๋์ SSE ์ฐ๊ฒฐ๋ก ์ฒ๋ฆฌ
- ํฌ์ค์ฒดํฌ - ์ฃผ๊ธฐ์ ์ธ ping/pong์ผ๋ก ์ฐ๊ฒฐ ์ํ ํ์ธ
ํนํ ์ฌ๋ฅ๋ท๊ณผ ๊ฐ์ ํ๋ซํผ์์๋ ์ฌ์ฉ์๋ณ๋ก ๊ด๋ จ ์๋ ์ด๋ฒคํธ๋ง ํํฐ๋งํ์ฌ ์ ์กํ๋ ๊ฒ์ด ์๋ฒ ์์์ ํจ์จ์ ์ผ๋ก ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด์ผ!
๋ชจ๋ํฐ๋ง ๋ฐ ๋๋ฒ๊น
SSE ์ฐ๊ฒฐ์ ๋ชจ๋ํฐ๋งํ๊ณ ๋๋ฒ๊น ํ๋ ๋ฐฉ๋ฒ:
- ์ฐ๊ฒฐ ์ํ ๋ก๊น - ์ฐ๊ฒฐ ์์, ์ข ๋ฃ, ์ฌ์ฐ๊ฒฐ ์ด๋ฒคํธ ๊ธฐ๋ก
- ์ด๋ฒคํธ ์ ์ก๋ ์ธก์ - ์ด๋น ์ด๋ฒคํธ ์, ๋ฐ์ดํฐ ํฌ๊ธฐ ๋ชจ๋ํฐ๋ง
- ํด๋ผ์ด์ธํธ ์ ์ถ์ - ํ์ฑ ์ฐ๊ฒฐ ์ ๋ชจ๋ํฐ๋ง
- ์ค๋ฅ ์ง๊ณ - ๋ฐ์ํ ์ค๋ฅ ์ ํ ๋ฐ ๋น๋ ๋ถ์
// ์๋ฒ ์ธก ๋ชจ๋ํฐ๋ง ์์
const metrics = {
activeConnections: 0,
totalConnections: 0,
eventsSent: 0,
errors: 0,
reconnects: 0
};
app.get('/events', (req, res) => {
// ์ฐ๊ฒฐ ์์ ์ ๋ฉํธ๋ฆญ ์
๋ฐ์ดํธ
metrics.activeConnections++;
metrics.totalConnections++;
// ์ฐ๊ฒฐ ์ข
๋ฃ ์ ๋ฉํธ๋ฆญ ์
๋ฐ์ดํธ
req.on('close', () => {
metrics.activeConnections--;
});
// ์ด๋ฒคํธ ์ ์ก ์ ๋ฉํธ๋ฆญ ์
๋ฐ์ดํธ
const originalWrite = res.write;
res.write = function(data) {
metrics.eventsSent++;
return originalWrite.apply(this, arguments);
};
// ... SSE ์ค์ ...
});
// ๋ฉํธ๋ฆญ API
app.get('/metrics', (req, res) => {
res.json(metrics);
});
7. ๋ธ๋ผ์ฐ์ ํธํ์ฑ๊ณผ ๋์ ๋ฐฉ์ ๐
2025๋ ํ์ฌ, SSE๋ ๋๋ถ๋ถ์ ์ต์ ๋ธ๋ผ์ฐ์ ์์ ์ ์ง์๋์ง๋ง, ์ฌ์ ํ ๋ช ๊ฐ์ง ํธํ์ฑ ๋ฌธ์ ๊ฐ ์์ ์ ์์ด. ์ด ์น์ ์์๋ ๋ธ๋ผ์ฐ์ ํธํ์ฑ ๋ฌธ์ ์ ๊ทธ ๋์ ๋ฐฉ์์ ์์๋ณด์.
์ฃผ์ ํธํ์ฑ ์ด์
- Internet Explorer - ๋ชจ๋ ๋ฒ์ ์์ SSE๋ฅผ ์ง์ํ์ง ์์
- ์ฐ๊ฒฐ ์ ํ - ์ผ๋ถ ๋ธ๋ผ์ฐ์ ๋ ๋๋ฉ์ธ๋น SSE ์ฐ๊ฒฐ ์ ์ ํ (๋ณดํต 6๊ฐ)
- ์ค๋๋ ๋ชจ๋ฐ์ผ ๋ธ๋ผ์ฐ์ - ์ผ๋ถ ๊ตฌํ ๋ชจ๋ฐ์ผ ๋ธ๋ผ์ฐ์ ์์ ์ง์ ์ ํ์
- ํ๋ก์ ์๋ฒ - ์ผ๋ถ ํ๋ก์๋ ์ฅ์๊ฐ ์ฐ๊ฒฐ์ ์ฐจ๋จํ ์ ์์
ํด๋ฆฌํ ๋ฐ ๋์ฒด ๋ฐฉ์
SSE๋ฅผ ์ง์ํ์ง ์๋ ๋ธ๋ผ์ฐ์ ๋ฅผ ์ํ ๋์์ฑ :
// SSE ํด๋ฆฌํ ๋๋ ๋์ฒด ๊ตฌํ
function createEventSource(url, options = {}) {
// ๋ค์ดํฐ๋ธ EventSource ์ง์ ํ์ธ
if (typeof EventSource !== 'undefined') {
return new EventSource(url, options);
}
// ํด๋ฆฌํ ๋๋ ๋์ฒด ๊ตฌํ
console.log('EventSource๊ฐ ์ง์๋์ง ์์ต๋๋ค. ํด๋ฐฑ ๋ฐฉ์์ผ๋ก ์ ํํฉ๋๋ค.');
return createPolyfill(url, options);
}
function createPolyfill(url, options) {
const polyfill = {
listeners: {},
// ๋กฑํด๋ง ๊ตฌํ
connect: function() {
const xhr = new XMLHttpRequest();
let lastEventId = options.lastEventId || '';
const poll = () => {
xhr.open('GET', url + (url.includes('?') ? '&' : '?') + 'lastEventId=' + lastEventId, true);
xhr.setRequestHeader('Accept', 'text/event-stream');
if (lastEventId) {
xhr.setRequestHeader('Last-Event-ID', lastEventId);
}
xhr.onreadystatechange = () => {
if (xhr.readyState === 3 || xhr.readyState === 4) {
// ์๋ต ์ฒ๋ฆฌ
const data = xhr.responseText;
this.processEvents(data);
// ์ฐ๊ฒฐ ์ข
๋ฃ ์ ์ฌ์ฐ๊ฒฐ
if (xhr.readyState === 4) {
setTimeout(poll, 1000);
}
}
};
xhr.onerror = () => {
// ์ค๋ฅ ์ฒ๋ฆฌ
if (this.listeners.error) {
this.listeners.error.forEach(fn => fn({ type: 'error' }));
}
setTimeout(poll, 1000);
};
xhr.send();
};
poll();
},
// ์ด๋ฒคํธ ์ฒ๋ฆฌ
processEvents: function(text) {
const lines = text.split('\n');
let eventType = 'message';
let data = '';
let id = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('event:')) {
eventType = line.substring(6).trim();
} else if (line.startsWith('data:')) {
data += line.substring(5).trim();
} else if (line.startsWith('id:')) {
id = line.substring(3).trim();
} else if (line === '') {
// ์ด๋ฒคํธ ์ข
๋ฃ
if (data) {
const event = {
type: eventType,
data: data,
lastEventId: id
};
// ์ด๋ฒคํธ ๋ฆฌ์ค๋ ํธ์ถ
if (this.listeners[eventType]) {
this.listeners[eventType].forEach(fn => fn(event));
}
// ๊ธฐ๋ณธ ๋ฉ์์ง ๋ฆฌ์ค๋
if (eventType !== 'message' && this.listeners.message) {
this.listeners.message.forEach(fn => fn(event));
}
// ID ์ ์ฅ
if (id) {
options.lastEventId = id;
}
}
// ์ด๊ธฐํ
eventType = 'message';
data = '';
}
}
},
// ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๊ด๋ฆฌ
addEventListener: function(type, callback) {
if (!this.listeners[type]) {
this.listeners[type] = [];
}
this.listeners[type].push(callback);
},
removeEventListener: function(type, callback) {
if (this.listeners[type]) {
this.listeners[type] = this.listeners[type].filter(fn => fn !== callback);
}
},
// ์์ฑ ๋ฐ ๋ฉ์๋
get readyState() { return 1; }, // OPEN
get onmessage() { return this.listeners.message && this.listeners.message[0]; },
set onmessage(fn) {
this.listeners.message = [fn];
},
get onerror() { return this.listeners.error && this.listeners.error[0]; },
set onerror(fn) {
this.listeners.error = [fn];
},
close: function() {
// ์ฐ๊ฒฐ ์ข
๋ฃ ๋ก์ง
}
};
// ์ฐ๊ฒฐ ์์
polyfill.connect();
return polyfill;
}
// ์ฌ์ฉ ์์
const eventSource = createEventSource('/events', {
lastEventId: localStorage.getItem('lastEventId')
});
eventSource.onmessage = function(event) {
console.log('๋ฉ์์ง ์์ :', event.data);
};
๋์ฒด ์ ๊ทผ ๋ฐฉ์
- ๋กฑ ํด๋ง - ์ฃผ๊ธฐ์ ์ผ๋ก ์๋ฒ์ ์์ฒญ์ ๋ณด๋ด ์ ๋ฐ์ดํธ ํ์ธ
- WebSocket - ์๋ฐฉํฅ ํต์ ์ด ํ์ํ๋ค๋ฉด WebSocket ์ฌ์ฉ
- Ajax ํด๋ง - ๋จ์ํ ์ฃผ๊ธฐ์ ํด๋ง์ผ๋ก ๋์ฒด
- ์๋ํํฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ - EventSource ํด๋ฆฌํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
์ฌ๋ฅ๋ท๊ณผ ๊ฐ์ ํ๋ซํผ์์๋ ์ฌ์ฉ์ ๋ธ๋ผ์ฐ์ ๋ฅผ ๊ฐ์งํ์ฌ ์ต์ ์ ์ค์๊ฐ ํต์ ๋ฐฉ์์ ์๋์ผ๋ก ์ ํํ๋ ๊ฒ์ด ์ข์ ์ ๋ต์ด์ผ!
์ฐ๊ฒฐ ์ ํ ๊ทน๋ณตํ๊ธฐ
๋ธ๋ผ์ฐ์ ์ ์ฐ๊ฒฐ ์ ํ(๋ณดํต ๋๋ฉ์ธ๋น 6๊ฐ)์ ๊ทน๋ณตํ๋ ๋ฐฉ๋ฒ:
- ์ด๋ฒคํธ ๋ฉํฐํ๋ ์ฑ - ์ฌ๋ฌ ์ข ๋ฅ์ ์ด๋ฒคํธ๋ฅผ ํ๋์ SSE ์ฐ๊ฒฐ๋ก ์ฒ๋ฆฌ
- ์๋ธ๋๋ฉ์ธ ํ์ฉ - ์ฌ๋ฌ ์๋ธ๋๋ฉ์ธ์ผ๋ก ์ฐ๊ฒฐ ๋ถ์ฐ
- ํ์ํ ๋๋ง ์ฐ๊ฒฐ - ํญ์ ์ฐ๊ฒฐ์ ์ ์งํ์ง ์๊ณ ํ์ํ ๋๋ง ์ฐ๊ฒฐ
- WebSocket ๊ณ ๋ ค - ๋ง์ ์ฐ๊ฒฐ์ด ํ์ํ๋ค๋ฉด WebSocket์ผ๋ก ์ ํ
8. 2025๋ ์ต์ SSE ํ์ฉ ํธ๋ ๋ ๐ฎ
2025๋ ํ์ฌ, SSE๋ ๋ค์ํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ค์๊ฐ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๋ฐ ๋๋ฆฌ ์ฌ์ฉ๋๊ณ ์์ด. ์ต์ ํธ๋ ๋์ ํ์ฉ ์ฌ๋ก๋ฅผ ์ดํด๋ณด์!
๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ์์์ SSE
ํ๋์ ์ธ ๋ง์ดํฌ๋ก์๋น์ค ํ๊ฒฝ์์ SSE๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ์ฉํ๋ ๋ฐฉ๋ฒ:
๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ์์๋ ์ด๋ฒคํธ ๊ธฐ๋ฐ ํต์ ์ด ์ค์ํ๋ฐ, SSE๋ ์ด๋ฐ ์ํคํ ์ฒ์ ์๋ฒฝํ๊ฒ ๋ง์. ๊ฐ ์๋น์ค๊ฐ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๊ณ , ์๋ฆผ ์๋น์ค๊ฐ ์ด๋ฅผ ๊ตฌ๋ ํ์ฌ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌํ๋ ํจํด์ด ๋๋ฆฌ ์ฌ์ฉ๋ผ.
SSE์ GraphQL ๊ตฌ๋ ์ ํตํฉ
2025๋ ์๋ GraphQL๊ณผ SSE๋ฅผ ๊ฒฐํฉํ ์ค์๊ฐ ๋ฐ์ดํฐ ์ฟผ๋ฆฌ ํจํด์ด ์ธ๊ธฐ๋ฅผ ๋๊ณ ์์ด:
// GraphQL ์คํค๋ง (์๋ฒ ์ธก)
const typeDefs = `
type Notification {
id: ID!
message: String!
type: String!
timestamp: String!
}
type Query {
notifications: [Notification!]!
}
type Subscription {
notificationAdded: Notification!
}
`;
// SSE๋ฅผ ํตํ GraphQL ๊ตฌ๋
๊ตฌํ
const resolvers = {
Query: {
notifications: () => db.getNotifications()
},
Subscription: {
notificationAdded: {
subscribe: () => {
// SSE ๊ธฐ๋ฐ ๋น๋๊ธฐ ์ดํฐ๋ ์ดํฐ ๋ฐํ
return {
[Symbol.asyncIterator]: () => ({
next: () => {
return new Promise((resolve) => {
// ์ด๋ฒคํธ ๋ฒ์ค์์ ๋ค์ ์๋ฆผ ๋๊ธฐ
eventBus.once('notification', (notification) => {
resolve({ value: { notificationAdded: notification }, done: false });
});
});
}
})
};
}
}
}
};
// ํด๋ผ์ด์ธํธ ์ธก (React + Apollo)
function NotificationsComponent() {
const { data, loading } = useSubscription(
gql`
subscription NotificationAdded {
notificationAdded {
id
message
type
timestamp
}
}
`
);
useEffect(() => {
if (data?.notificationAdded) {
// ์ ์๋ฆผ ์ฒ๋ฆฌ
showNotification(data.notificationAdded);
}
}, [data]);
return (
<div classname="notifications-panel">
{/* ์๋ฆผ UI */}
</div>
);
}
์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์์์ SSE
AWS Lambda, Azure Functions ๋ฑ ์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์์๋ SSE๋ฅผ ํ์ฉํ ์ ์์ด:
// AWS Lambda + API Gateway ์์
exports.handler = async (event, context) => {
// ์ฐ๊ฒฐ ์ ์ง๋ฅผ ์ํ ์ปจํ
์คํธ ์ค์
context.callbackWaitsForEmptyEventLoop = false;
// SSE ์๋ต ํค๋
const headers = {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' // NGINX ๋ฒํผ๋ง ๋นํ์ฑํ
};
// ํด๋ผ์ด์ธํธ ID
const connectionId = event.requestContext.connectionId;
// DynamoDB์ ์ฐ๊ฒฐ ์ ๋ณด ์ ์ฅ
await saveConnection(connectionId);
// ์ด๊ธฐ ์๋ต
return {
statusCode: 200,
headers,
body: 'data: {"connected":true}\n\n',
isBase64Encoded: false
};
};
// ์ด๋ฒคํธ ๋ฐํ Lambda ํจ์
exports.publishEvent = async (event) => {
const { message, connectionIds } = JSON.parse(event.body);
// ๋ชจ๋ ์ฐ๊ฒฐ๋ ํด๋ผ์ด์ธํธ์ ๋ฉ์์ง ์ ์ก
const promises = connectionIds.map(connectionId => {
return sendMessageToClient(connectionId, message);
});
await Promise.all(promises);
return {
statusCode: 200,
body: JSON.stringify({ sent: connectionIds.length })
};
};
โ ๏ธ ์ฃผ์: ์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์์ SSE๋ฅผ ๊ตฌํํ ๋๋ ํจ์ ์คํ ์๊ฐ ์ ํ๊ณผ ์ฐ๊ฒฐ ์ ์ง ๋ฌธ์ ๋ฅผ ๊ณ ๋ คํด์ผ ํด. AWS API Gateway์ WebSocket ์ง์์ด๋ Azure์ SignalR ์๋น์ค์ ๊ฐ์ ๊ด๋ฆฌํ ์๋น์ค๋ฅผ ํ์ฉํ๋ ๊ฒ์ด ์ข์.
AI ๊ธฐ๋ฐ ์ค์๊ฐ ๋ถ์๊ณผ SSE
2025๋ ์๋ AI ๊ธฐ๋ฐ ์ค์๊ฐ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ SSE๋ก ์ ์กํ๋ ํจํด์ด ์ธ๊ธฐ๋ฅผ ๋๊ณ ์์ด:
- ์ค์๊ฐ ๊ฐ์ ๋ถ์ - ์ฌ์ฉ์ ํผ๋๋ฐฑ์ด๋ ๋ฆฌ๋ทฐ์ ๊ฐ์ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ์ค์๊ฐ์ผ๋ก ๋์๋ณด๋์ ํ์
- ์ด์ ํ์ง - ์์คํ ๋ชจ๋ํฐ๋ง ๋ฐ์ดํฐ์์ ์ด์ ํจํด์ ๊ฐ์งํ์ฌ ์ฆ์ ์๋ฆผ
- ๊ฐ์ธํ๋ ์ถ์ฒ - ์ฌ์ฉ์ ํ๋์ ๊ธฐ๋ฐํ ์ค์๊ฐ ์ถ์ฒ ์ ๊ณต
- ์์ธก ์๋ฆผ - ๋ฏธ๋ ์ด๋ฒคํธ ์์ธก ๊ฒฐ๊ณผ๋ฅผ ์ค์๊ฐ์ผ๋ก ์ ๋ฌ
๐ 2025๋ SSE ํ์ฉ ์ฌ๋ก
์ฌ๋ฅ๋ท๊ณผ ๊ฐ์ ํ๋ซํผ์์๋ SSE๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ํ์ฉํ ์ ์์ด:
- ์ค์๊ฐ ์ ์ฐฐ ์๋ฆผ - ํ๋ฆฌ๋์ ํ๋ก์ ํธ์ ์๋ก์ด ์ ์ฐฐ์ด ๋ค์ด์ฌ ๋ ์๋ฆผ
- ๋ง๊ฐ ์๋ฐ ์๋ฆผ - ํ๋ก์ ํธ ๋ง๊ฐ ์๊ฐ์ด ๋ค๊ฐ์ฌ ๋ ์๋ฆผ
- ๋ง์ถคํ ํ๋ก์ ํธ ์ถ์ฒ - ์ฌ์ฉ์ ํ๋กํ๊ณผ ์ผ์นํ๋ ์ ํ๋ก์ ํธ ์ค์๊ฐ ์๋ฆผ
- ๋ฉ์์ง ๋ฐ ๋ฆฌ๋ทฐ ์๋ฆผ - ์๋ก์ด ๋ฉ์์ง๋ ๋ฆฌ๋ทฐ๊ฐ ์์ฑ๋ ๋ ์๋ฆผ
- ๊ฒฐ์ ์ํ ์ ๋ฐ์ดํธ - ๊ฒฐ์ ์ฒ๋ฆฌ ์ํ ์ค์๊ฐ ์ ๋ฐ์ดํธ
์ด๋ฌํ ์ค์๊ฐ ๊ธฐ๋ฅ์ ์ฌ์ฉ์ ์ฐธ์ฌ๋์ ๋ง์กฑ๋๋ฅผ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ด!
9. ๋ง๋ฌด๋ฆฌ ๋ฐ ์ถ๊ฐ ์๋ฃ ๐
์ง๊ธ๊น์ง Server-Sent Events์ ๊ธฐ๋ณธ ๊ฐ๋ ๋ถํฐ ๊ณ ๊ธ ๊ตฌํ ๋ฐฉ๋ฒ, ๊ทธ๋ฆฌ๊ณ 2025๋ ์ต์ ํธ๋ ๋๊น์ง ์ดํด๋ดค์ด. SSE๋ ๋จ๋ฐฉํฅ ์ค์๊ฐ ํต์ ์ด ํ์ํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋จํ๋ฉด์๋ ๊ฐ๋ ฅํ ์๋ฃจ์ ์ ์ ๊ณตํด.
ํต์ฌ ์์ฝ
- SSE๋ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก์ ๋จ๋ฐฉํฅ ์ค์๊ฐ ํต์ ์ ์ํ ๊ธฐ์
- ํ์ค HTTP ํ๋กํ ์ฝ์ ์ฌ์ฉํ์ฌ ๊ตฌํ์ด ๊ฐ๋จํ๊ณ ๊ธฐ์กด ์ธํ๋ผ์ ํธํ์ฑ์ด ์ข์
- ์๋ ์ฌ์ฐ๊ฒฐ๊ณผ ์ด๋ฒคํธ ID๋ฅผ ํตํ ์ด์ด๋ฐ๊ธฐ ๊ธฐ๋ฅ ์ ๊ณต
- WebSocket๋ณด๋ค ๊ตฌํ์ด ๊ฐ๋จํ๊ณ HTTP ์ธํ๋ผ์ ํธํ์ฑ์ด ์ข์
- ์๋ฆผ, ์ค์๊ฐ ์ ๋ฐ์ดํธ, ๋์๋ณด๋ ๋ฑ ๋ค์ํ ์ค์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉ
์ถ๊ฐ ํ์ต ์๋ฃ
- MDN Web Docs - Server-Sent Events API
- HTML5 Rocks - Stream Updates with Server-Sent Events
- GitHub - EventSource ํด๋ฆฌํ
- WHATWG - Server-Sent Events ๋ช ์ธ
SSE๋ ๋จ์ํจ๊ณผ ๊ฐ๋ ฅํจ์ ๋์์ ๊ฐ์ถ ๊ธฐ์ ์ด์ผ. ๋ณต์กํ ์๋ฐฉํฅ ํต์ ์ด ํ์ํ์ง ์์ ๋ง์ ์ค์๊ฐ ์๋๋ฆฌ์ค์์ WebSocket๋ณด๋ค ๋ ์ ํฉํ ์ ํ์ผ ์ ์์ด. ํนํ ์ฌ๋ฅ๋ท๊ณผ ๊ฐ์ ํ๋ซํผ์์ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๊ธฐ ์ํ ์ค์๊ฐ ์๋ฆผ๊ณผ ์ ๋ฐ์ดํธ๋ฅผ ๊ตฌํํ๋ ๋ฐ ์๋ฒฝํ ์๋ฃจ์ ์ด์ง!
์ด ๊ธ์ด SSE๋ฅผ ์ดํดํ๊ณ ๊ตฌํํ๋ ๋ฐ ๋์์ด ๋์๊ธธ ๋ฐ๋ผ. ์ค์๊ฐ ์น ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ ๋์ ํด๋ณด๊ณ , ์ฌ์ฉ์๋ค์๊ฒ ๋ ๋์ ๊ฒฝํ์ ์ ๊ณตํด๋ณด์! ๐
1. Server-Sent Events๋ ๋ฌด์์ธ๊ฐ? ๐ค
Server-Sent Events(SSE)๋ HTTP ์ฐ๊ฒฐ์ ํตํด ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ์๋ ์ ๋ฐ์ดํธ๋ฅผ ํธ์ํ๋ ๊ธฐ์ ์ด์ผ. ์น์์ผ๊ณผ ๋ฌ๋ฆฌ ๋จ๋ฐฉํฅ ํต์ ๋ง ์ง์ํ์ง๋ง, ๊ทธ๋งํผ ๊ตฌํ์ด ๊ฐ๋จํ๊ณ HTTP ํ๋กํ ์ฝ์ ๊ทธ๋๋ก ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๊ธฐ์กด ์ธํ๋ผ์์ ํธํ์ฑ์ด ๋ฐ์ด๋๋ค๋ ์ฅ์ ์ด ์์ด.
SSE์ ํต์ฌ ํน์ง โจ
- ๋จ๋ฐฉํฅ ํต์ - ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก๋ง ๋ฐ์ดํฐ๊ฐ ํ๋ฆ
- ์๋ ์ฌ์ฐ๊ฒฐ - ์ฐ๊ฒฐ์ด ๋์ด์ ธ๋ ์๋์ผ๋ก ๋ค์ ์ฐ๊ฒฐ ์๋
- ์ด๋ฒคํธ ID - ๊ฐ ์ด๋ฒคํธ์ ID๋ฅผ ๋ถ์ฌํด ์ฐ๊ฒฐ์ด ๋๊ธด ํ ๋ง์ง๋ง์ผ๋ก ๋ฐ์ ์ด๋ฒคํธ๋ถํฐ ๋ค์ ์์ ๊ฐ๋ฅ
- ํ์ค HTTP ์ฌ์ฉ - ํน๋ณํ ํ๋กํ ์ฝ์ด ํ์ ์์ด ๋ฐฉํ๋ฒฝ์ด๋ ํ๋ก์ ๋ฌธ์ ๊ฐ ์ ์
- ํ ์คํธ ๊ธฐ๋ฐ ํต์ - ์ฃผ๋ก ํ ์คํธ ๋ฐ์ดํฐ ์ ์ก์ ์ต์ ํ๋จ
SSE๋ ์ฃผ์ ์์ธ, ์์ ๋ฏธ๋์ด ํผ๋, ์๋ฆผ ์์คํ , ์ค์๊ฐ ๋ถ์ ๋์๋ณด๋ ๋ฑ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ์ง์์ ์ธ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ๊ฐ ํ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ ํนํ ์ ์ฉํด. 2025๋ ํ์ฌ, ๋ง์ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ๋ค์ด ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๊ธฐ ์ํด SSE๋ฅผ ํ์ฉํ๊ณ ์์ง!
2. SSE vs WebSocket: ์ธ์ ๋ฌด์์ ์ ํํด์ผ ํ ๊น? ๐คทโโ๏ธ
์ค์๊ฐ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋ฐํ ๋ ํญ์ ๋ง์ฃผ์น๋ ์ง๋ฌธ์ด ์์ด. "WebSocket์ ์จ์ผ ํ ๊น, SSE๋ฅผ ์จ์ผ ํ ๊น?" ๋ ๊ธฐ์ ๋ชจ๋ ์ค์๊ฐ ํต์ ์ ๊ฐ๋ฅํ๊ฒ ํ์ง๋ง, ๊ฐ๊ฐ์ ํน์ฑ๊ณผ ์ฅ๋จ์ ์ด ๋ฌ๋ผ. ์ด ์ฐจ์ด์ ์ ์ดํดํ๋ฉด ํ๋ก์ ํธ์ ๋ง๋ ๊ธฐ์ ์ ์ ํํ๋ ๋ฐ ๋์์ด ๋ ๊ฑฐ์ผ.
์ธ์ SSE๋ฅผ ์ ํํด์ผ ํ ๊น? ๐ฏ
- ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก์ ๋จ๋ฐฉํฅ ํต์ ์ด ์ฃผ์ ์๊ตฌ์ฌํญ์ผ ๋
- ์๋ฆผ, ๋ด์ค ํผ๋, ์ฃผ์ ์์ธ ๋ฑ ์ค์๊ฐ ์ ๋ฐ์ดํธ๊ฐ ํ์ํ ๋
- ๊ธฐ์กด HTTP ์ธํ๋ผ๋ฅผ ํ์ฉํ๊ณ ์ถ์ ๋
- ๊ตฌํ ๋ณต์ก๋๋ฅผ ๋ฎ์ถ๊ณ ์ถ์ ๋
- ์ฐ๊ฒฐ์ด ๋๊ฒผ์ ๋ ์๋ ์ฌ์ฐ๊ฒฐ๊ณผ ์ด๋ฒคํธ ๋ณต๊ตฌ๊ฐ ์ค์ํ ๋
์ค์ ์ฌ๋ก: ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์๋ก์ด ํ๋ก์ ํธ ์ ์์ด๋ ๋ฉ์์ง ์๋ฆผ์ ์ค์๊ฐ์ผ๋ก ๋ฐ๊ณ ์ถ์ ๋, SSE๋ ์๋ฒฝํ ์ ํ์ด์ผ. ์ฌ์ฉ์๊ฐ ์๋ฒ๋ก ์ง์์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ํ์ ์์ด, ์ค์ํ ์ ๋ฐ์ดํธ๋ง ๋ฐ์ผ๋ฉด ๋๋๊น!
๋ฌผ๋ก , ์ฑํ ์ ํ๋ฆฌ์ผ์ด์ ์ฒ๋ผ ์๋ฐฉํฅ ์ค์๊ฐ ํต์ ์ด ํ์ํ ๊ฒฝ์ฐ์๋ WebSocket์ด ๋ ์ ํฉํด. ํ์ง๋ง ๋ง์ ๊ฒฝ์ฐ SSE๋ง์ผ๋ก๋ ์ถฉ๋ถํ๋ฉฐ, ๊ตฌํ๋ ํจ์ฌ ๊ฐ๋จํ๋ค๋ ์ฅ์ ์ด ์์ด.
3. SSE ๊ธฐ๋ณธ ๊ตฌํํ๊ธฐ ๐ป
์ด์ ์๋ฐ์คํฌ๋ฆฝํธ๋ก SSE๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ์๋ฒ ์ธก๊ณผ ํด๋ผ์ด์ธํธ ์ธก ๋ชจ๋ ์ดํด๋ณผ ๊ฑฐ์ผ.
ํด๋ผ์ด์ธํธ ์ธก ๊ตฌํ (๋ธ๋ผ์ฐ์ )
๋ธ๋ผ์ฐ์ ์์ SSE ์ฐ๊ฒฐ์ ์ค์ ํ๋ ๊ฒ์ ์ ๋ง ๊ฐ๋จํด. EventSource API๋ฅผ ์ฌ์ฉํ๋ฉด ๋ผ:
// SSE ์ฐ๊ฒฐ ์์ฑํ๊ธฐ
const eventSource = new EventSource('/events');
// ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ๋กํ๊ธฐ
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('์๋ก์ด ๋ฐ์ดํฐ ์์ :', data);
updateUI(data);
};
// ์ฐ๊ฒฐ ์ด๋ฆผ ์ด๋ฒคํธ
eventSource.onopen = function() {
console.log('SSE ์ฐ๊ฒฐ์ด ์ด๋ ธ์ต๋๋ค!');
};
// ์๋ฌ ์ฒ๋ฆฌ
eventSource.onerror = function(error) {
console.error('SSE ์ฐ๊ฒฐ ์ค๋ฅ:', error);
// ํ์์ ๋ฐ๋ผ ์ฌ์ฐ๊ฒฐ ๋ก์ง ์ถ๊ฐ
};
// ํน์ ์ด๋ฒคํธ ํ์
๋ฆฌ์ค๋
eventSource.addEventListener('update', function(event) {
const updateData = JSON.parse(event.data);
console.log('์
๋ฐ์ดํธ ์ด๋ฒคํธ:', updateData);
});
// ์ฐ๊ฒฐ ์ข
๋ฃ (ํ์ํ ๋)
function closeConnection() {
eventSource.close();
}
โ ๏ธ ์ฃผ์์ฌํญ: EventSource๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฐ๊ฒฐ์ด ๋์ด์ง๋ฉด ์๋์ผ๋ก ์ฌ์ฐ๊ฒฐ์ ์๋ํด. ์ด ๋์์ ์ ์ดํ๊ณ ์ถ๋ค๋ฉด onerror ํธ๋ค๋ฌ์์ ์ ์ ํ ๋ก์ง์ ๊ตฌํํด์ผ ํด.
์๋ฒ ์ธก ๊ตฌํ
์๋ฒ ์ธก์์๋ ์ฌ๋ฐ๋ฅธ ํค๋์ ํ์์ผ๋ก ์๋ต์ ๋ณด๋ด์ผ ํด. ์ฌ๊ธฐ์๋ Node.js์ Express๋ฅผ ์ฌ์ฉํ ์์ ๋ฅผ ์ดํด๋ณด์:
const express = require('express');
const app = express();
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.write('data: {"message": "์ฐ๊ฒฐ ์ฑ๊ณต!"}\n\n');
// 5์ด๋ง๋ค ๋ฐ์ดํฐ ์ ์ก (์์)
const intervalId = setInterval(() => {
const data = {
time: new Date().toISOString(),
value: Math.random() * 100
};
// ์ผ๋ฐ ๋ฉ์์ง ์ ์ก
res.write(`data: ${JSON.stringify(data)}\n\n`);
// ํน์ ์ด๋ฒคํธ ํ์
์ผ๋ก ์ ์ก
res.write(`event: update\ndata: ${JSON.stringify({ updated: true, timestamp: Date.now() })}\n\n`);
}, 5000);
// ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ์ด ๋์ด์ง๋ฉด ์ธํฐ๋ฒ ์ ๋ฆฌ
req.on('close', () => {
clearInterval(intervalId);
console.log('ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ ์ข
๋ฃ');
});
});
app.listen(3000, () => {
console.log('SSE ์๋ฒ๊ฐ ํฌํธ 3000์์ ์คํ ์ค์
๋๋ค');
});
SSE ๋ฉ์์ง ํ์ ์ดํดํ๊ธฐ ๐
SSE ๋ฉ์์ง๋ ํน์ ํ์์ ๋ฐ๋ผ์ผ ํด:
event: ์ด๋ฒคํธ๋ช
id: ์ด๋ฒคํธID
data: ์ค์ ๋ฐ์ดํฐ
retry: ์ฌ์ฐ๊ฒฐ ์๊ฐ(ms)
๊ฐ ํ๋๋ ์ ํ์ ์ด์ง๋ง, ์ ์ด๋ 'data' ํ๋๋ ํฌํจํด์ผ ํด. ๊ทธ๋ฆฌ๊ณ ๊ฐ ๋ฉ์์ง๋ ๋ฐ๋์ ๋น ์ค(\n\n)๋ก ๋๋์ผ ํ๋ค๋ ์ ์ ๊ธฐ์ตํด!
- ์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์ฌ๋ฅ๋ท์ ์ต์ AI ๊ธฐ์ ๊ณผ ๋ฒ๋ฅ ์ ๊ธฐ๋ฐํ์ฌ ์์ฌ์ ์ง์ ์ฌ์ฐ๊ถ์ ์ ๊ทน์ ์ผ๋ก ๋ณดํธํ๋ฉฐ,
๋ฌด๋จ ์ฌ์ฉ ๋ฐ ์นจํด ํ์์ ๋ํด ๋ฒ์ ๋์์ ํ ๊ถ๋ฆฌ๋ฅผ ๋ณด์ ํฉ๋๋ค.
ยฉ 2025 ์ฌ๋ฅ๋ท | All rights reserved.
๋๊ธ 0๊ฐ