๐Ÿš€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ๊ตฌํ˜„ํ•˜๋Š” Server-Sent Events: ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ํ†ต์‹ ์˜ ์ƒˆ๋กœ์šด ํŒจ๋Ÿฌ๋‹ค์ž„ ๐Ÿš€

์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ๊ตฌํ˜„ํ•˜๋Š” Server-Sent Events: ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ํ†ต์‹ ์˜ ์ƒˆ๋กœ์šด ํŒจ๋Ÿฌ๋‹ค์ž„ ๐Ÿš€

 

 

์•ˆ๋…•, ๊ฐœ๋ฐœ์ž ์นœ๊ตฌ๋“ค! ์˜ค๋Š˜์€ 2025๋…„ 3์›” 20์ผ, ์‹ค์‹œ๊ฐ„ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์˜ ํ•ต์‹ฌ ๊ธฐ์ˆ ์ธ Server-Sent Events(SSE)์— ๋Œ€ํ•ด ํ•จ๊ป˜ ์•Œ์•„๋ณผ ๊ฑฐ์•ผ. ๋ณต์žกํ•œ ์–‘๋ฐฉํ–ฅ ํ†ต์‹  ์—†์ด๋„ ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” ์ด ๊ธฐ์ˆ , ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ์‰ฝ๊ณ  ํšจ์œจ์ ์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์„์ง€ ์žฌ๋ฐŒ๊ฒŒ ํŒŒํ—ค์ณ ๋ณด์ž! ๐Ÿ”

๐Ÿ“š ๋ชฉ์ฐจ

  1. Server-Sent Events๋ž€ ๋ฌด์—‡์ธ๊ฐ€?
  2. SSE vs WebSocket: ์–ธ์ œ ๋ฌด์—‡์„ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ?
  3. SSE ๊ธฐ๋ณธ ๊ตฌํ˜„ํ•˜๊ธฐ
  4. ์‹ค์ „ ์˜ˆ์ œ: ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๋งŒ๋“ค๊ธฐ
  5. SSE์˜ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ํ™œ์šฉํ•˜๊ธฐ
  6. ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ์—๋Ÿฌ ํ•ธ๋“ค๋ง
  7. ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ๊ณผ ๋Œ€์‘ ๋ฐฉ์•ˆ
  8. 2025๋…„ ์ตœ์‹  SSE ํ™œ์šฉ ํŠธ๋ Œ๋“œ
  9. ๋งˆ๋ฌด๋ฆฌ ๋ฐ ์ถ”๊ฐ€ ์ž๋ฃŒ

1. Server-Sent Events๋ž€ ๋ฌด์—‡์ธ๊ฐ€? ๐Ÿค”

Server-Sent Events(SSE)๋Š” HTTP ์—ฐ๊ฒฐ์„ ํ†ตํ•ด ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ์ž๋™ ์—…๋ฐ์ดํŠธ๋ฅผ ํ‘ธ์‹œํ•˜๋Š” ๊ธฐ์ˆ ์ด์•ผ. ์›น์†Œ์ผ“๊ณผ ๋‹ฌ๋ฆฌ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ๋งŒ ์ง€์›ํ•˜์ง€๋งŒ, ๊ทธ๋งŒํผ ๊ตฌํ˜„์ด ๊ฐ„๋‹จํ•˜๊ณ  HTTP ํ”„๋กœํ† ์ฝœ์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ์กด ์ธํ”„๋ผ์™€์˜ ํ˜ธํ™˜์„ฑ์ด ๋›ฐ์–ด๋‚˜๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์–ด.

์„œ๋ฒ„ ํด๋ผ์ด์–ธํŠธ HTTP ์—ฐ๊ฒฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ

SSE์˜ ํ•ต์‹ฌ ํŠน์ง• โœจ

  1. ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹  - ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ๋งŒ ๋ฐ์ดํ„ฐ๊ฐ€ ํ๋ฆ„

  2. ์ž๋™ ์žฌ์—ฐ๊ฒฐ - ์—ฐ๊ฒฐ์ด ๋Š์–ด์ ธ๋„ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ์—ฐ๊ฒฐ ์‹œ๋„

  3. ์ด๋ฒคํŠธ ID - ๊ฐ ์ด๋ฒคํŠธ์— ID๋ฅผ ๋ถ€์—ฌํ•ด ์—ฐ๊ฒฐ์ด ๋Š๊ธด ํ›„ ๋งˆ์ง€๋ง‰์œผ๋กœ ๋ฐ›์€ ์ด๋ฒคํŠธ๋ถ€ํ„ฐ ๋‹ค์‹œ ์ˆ˜์‹  ๊ฐ€๋Šฅ

  4. ํ‘œ์ค€ HTTP ์‚ฌ์šฉ - ํŠน๋ณ„ํ•œ ํ”„๋กœํ† ์ฝœ์ด ํ•„์š” ์—†์–ด ๋ฐฉํ™”๋ฒฝ์ด๋‚˜ ํ”„๋ก์‹œ ๋ฌธ์ œ๊ฐ€ ์ ์Œ

  5. ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ํ†ต์‹  - ์ฃผ๋กœ ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ „์†ก์— ์ตœ์ ํ™”๋จ

SSE๋Š” ์ฃผ์‹ ์‹œ์„ธ, ์†Œ์…œ ๋ฏธ๋””์–ด ํ”ผ๋“œ, ์•Œ๋ฆผ ์‹œ์Šคํ…œ, ์‹ค์‹œ๊ฐ„ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ๋“ฑ ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ์ง€์†์ ์ธ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ํŠนํžˆ ์œ ์šฉํ•ด. 2025๋…„ ํ˜„์žฌ, ๋งŽ์€ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ๋“ค์ด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•ด SSE๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์žˆ์ง€!

2. SSE vs WebSocket: ์–ธ์ œ ๋ฌด์—‡์„ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ? ๐Ÿคทโ€โ™‚๏ธ

์‹ค์‹œ๊ฐ„ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•  ๋•Œ ํ•ญ์ƒ ๋งˆ์ฃผ์น˜๋Š” ์งˆ๋ฌธ์ด ์žˆ์–ด. "WebSocket์„ ์จ์•ผ ํ• ๊นŒ, SSE๋ฅผ ์จ์•ผ ํ• ๊นŒ?" ๋‘ ๊ธฐ์ˆ  ๋ชจ๋‘ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜์ง€๋งŒ, ๊ฐ๊ฐ์˜ ํŠน์„ฑ๊ณผ ์žฅ๋‹จ์ ์ด ๋‹ฌ๋ผ. ์ด ์ฐจ์ด์ ์„ ์ดํ•ดํ•˜๋ฉด ํ”„๋กœ์ ํŠธ์— ๋งž๋Š” ๊ธฐ์ˆ ์„ ์„ ํƒํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋  ๊ฑฐ์•ผ.

SSE vs WebSocket Server-Sent Events WebSocket โœ… HTTP ๊ธฐ๋ฐ˜ (๊ธฐ์กด ์ธํ”„๋ผ ํ™œ์šฉ) โœ… ๊ตฌํ˜„ ๊ฐ„๋‹จ โœ… ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ โœ… ์ด๋ฒคํŠธ ID๋กœ ์ด์–ด๋ฐ›๊ธฐ ๊ฐ€๋Šฅ โŒ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ๋งŒ ๊ฐ€๋Šฅ โŒ ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ์— ์ตœ์ ํ™” โŒ ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ (๋ธŒ๋ผ์šฐ์ €๋‹น) โœ… ์–‘๋ฐฉํ–ฅ ํ†ต์‹  โœ… ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ ์ „์†ก ๊ฐ€๋Šฅ โœ… ๋‚ฎ์€ ์ง€์—ฐ ์‹œ๊ฐ„ โœ… ํ”„๋กœํ† ์ฝœ ํ™•์žฅ ๊ฐ€๋Šฅ โŒ ๊ตฌํ˜„ ๋ณต์žก๋„ ๋†’์Œ โŒ ๋ฐฉํ™”๋ฒฝ/ํ”„๋ก์‹œ ๋ฌธ์ œ ๊ฐ€๋Šฅ์„ฑ โŒ ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ ์—†์Œ

์–ธ์ œ SSE๋ฅผ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ? ๐ŸŽฏ

  1. ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ์˜ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ์ด ์ฃผ์š” ์š”๊ตฌ์‚ฌํ•ญ์ผ ๋•Œ

  2. ์•Œ๋ฆผ, ๋‰ด์Šค ํ”ผ๋“œ, ์ฃผ์‹ ์‹œ์„ธ ๋“ฑ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•  ๋•Œ

  3. ๊ธฐ์กด HTTP ์ธํ”„๋ผ๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์‹ถ์„ ๋•Œ

  4. ๊ตฌํ˜„ ๋ณต์žก๋„๋ฅผ ๋‚ฎ์ถ”๊ณ  ์‹ถ์„ ๋•Œ

  5. ์—ฐ๊ฒฐ์ด ๋Š๊ฒผ์„ ๋•Œ ์ž๋™ ์žฌ์—ฐ๊ฒฐ๊ณผ ์ด๋ฒคํŠธ ๋ณต๊ตฌ๊ฐ€ ์ค‘์š”ํ•  ๋•Œ

์‹ค์ œ ์‚ฌ๋ก€: ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ ์ œ์•ˆ์ด๋‚˜ ๋ฉ”์‹œ์ง€ ์•Œ๋ฆผ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ›๊ณ  ์‹ถ์„ ๋•Œ, 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)๋กœ ๋๋‚˜์•ผ ํ•œ๋‹ค๋Š” ์ ์„ ๊ธฐ์–ตํ•ด!

์„œ๋ฒ„ ํด๋ผ์ด์–ธํŠธ data: {"value": 42} event: update data: {"updated": true} HTTP ์—ฐ๊ฒฐ ์œ ์ง€ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ (๋‹จ๋ฐฉํ–ฅ)

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;
}

๐Ÿ’ก ๊ตฌํ˜„ ํŒ

์œ„ ์˜ˆ์ œ๋ฅผ ํ™•์žฅํ•˜๋ฉด ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์•Œ๋ฆผ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด:

  1. ์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€ ๋˜๋Š” ๋Œ“๊ธ€ ์•Œ๋ฆผ

  2. ํ”„๋กœ์ ํŠธ ์ œ์•ˆ ๋ฐ ์ˆ˜๋ฝ ์•Œ๋ฆผ

  3. ๊ฒฐ์ œ ๋ฐ ์ •์‚ฐ ๊ด€๋ จ ์•Œ๋ฆผ

  4. ์‹œ์Šคํ…œ ๊ณต์ง€์‚ฌํ•ญ

์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๋ ค๋ฉด ์•Œ๋ฆผ์— ์†Œ๋ฆฌ๋‚˜ ๋ธŒ๋ผ์šฐ์ € ์•Œ๋ฆผ(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 ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ํ™œ์šฉ ์„œ๋ฒ„ ํด๋ผ์ด์–ธํŠธ Content-Type: text/event-stream Cache-Control: no-cache id: 1001 event: update data: {"status":"updated"} retry: 10000

์ธ์ฆ ๋ฐ ๋ณด์•ˆ

์‹ค์ œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” 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 ํ™œ์šฉ ๋ชจ๋ฒ” ์‚ฌ๋ก€

  1. ์ด๋ฒคํŠธ ๊ทธ๋ฃนํ™” - ์—ฌ๋Ÿฌ ์ž‘์€ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•˜๋‚˜์˜ ์ด๋ฒคํŠธ๋กœ ๋ฌถ์–ด ์ „์†ก

  2. ์ด๋ฒคํŠธ ํ•„ํ„ฐ๋ง - ํด๋ผ์ด์–ธํŠธ๋ณ„๋กœ ๊ด€๋ จ ์žˆ๋Š” ์ด๋ฒคํŠธ๋งŒ ์ „์†ก

  3. ๋ฐฑ์˜คํ”„ ์ „๋žต - ์—ฐ๊ฒฐ ์‹คํŒจ ์‹œ ์ ์ง„์ ์œผ๋กœ ์žฌ์‹œ๋„ ๊ฐ„๊ฒฉ ์ฆ๊ฐ€

  4. ํ—ฌ์Šค์ฒดํฌ - ์ฃผ๊ธฐ์ ์ธ ping ์ด๋ฒคํŠธ๋กœ ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ

  5. ์ด๋ฒคํŠธ ์••์ถ• - ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ ์ „์†ก ์‹œ ์••์ถ• ๊ณ ๋ ค

6. ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ์—๋Ÿฌ ํ•ธ๋“ค๋ง โšก

SSE๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ฐ€๋ณ๊ณ  ํšจ์œจ์ ์ด์ง€๋งŒ, ๋Œ€๊ทœ๋ชจ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” ์„ฑ๋Šฅ ์ตœ์ ํ™”์™€ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ์ค‘์š”ํ•ด. ์ด ์„น์…˜์—์„œ๋Š” SSE ๊ตฌํ˜„์„ ๋” ๊ฒฌ๊ณ ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž!

์„œ๋ฒ„ ์ธก ์„ฑ๋Šฅ ์ตœ์ ํ™”

  1. ์—ฐ๊ฒฐ ์ˆ˜ ๊ด€๋ฆฌ - ์„œ๋ฒ„๋‹น ๋™์‹œ ์—ฐ๊ฒฐ ์ˆ˜๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ์ œํ•œ

  2. ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๊ด€๋ฆฌ - ํด๋ผ์ด์–ธํŠธ ๋ชฉ๋ก๊ณผ ์ด๋ฒคํŠธ ํ์˜ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ ์ตœ์ ํ™”

  3. ์ด๋ฒคํŠธ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ - ์—ฌ๋Ÿฌ ์ด๋ฒคํŠธ๋ฅผ ๋ฌถ์–ด์„œ ์ „์†ก

  4. 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);
});

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

  1. ์ด๋ฒคํŠธ ํ•„ํ„ฐ๋ง - ํด๋ผ์ด์–ธํŠธ์— ํ•„์š”ํ•œ ์ด๋ฒคํŠธ๋งŒ ์ „์†ก

  2. ์ด๋ฒคํŠธ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ - ์งง์€ ์‹œ๊ฐ„ ๋‚ด ์—ฌ๋Ÿฌ ์ด๋ฒคํŠธ๋ฅผ ๋ฌถ์–ด์„œ ์ „์†ก

  3. ๋ฐ์ดํ„ฐ ์••์ถ• - ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ „์†ก ์‹œ ์••์ถ• ๊ณ ๋ ค

  4. ์—ฐ๊ฒฐ ํ’€๋ง - ์—ฌ๋Ÿฌ ์ด๋ฒคํŠธ ํƒ€์ž…์„ ํ•˜๋‚˜์˜ SSE ์—ฐ๊ฒฐ๋กœ ์ฒ˜๋ฆฌ

  5. ํ—ฌ์Šค์ฒดํฌ - ์ฃผ๊ธฐ์ ์ธ ping/pong์œผ๋กœ ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ

ํŠนํžˆ ์žฌ๋Šฅ๋„ท๊ณผ ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋Š” ์‚ฌ์šฉ์ž๋ณ„๋กœ ๊ด€๋ จ ์žˆ๋Š” ์ด๋ฒคํŠธ๋งŒ ํ•„ํ„ฐ๋งํ•˜์—ฌ ์ „์†กํ•˜๋Š” ๊ฒƒ์ด ์„œ๋ฒ„ ์ž์›์„ ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ!

๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋””๋ฒ„๊น…

SSE ์—ฐ๊ฒฐ์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ๋””๋ฒ„๊น…ํ•˜๋Š” ๋ฐฉ๋ฒ•:

  1. ์—ฐ๊ฒฐ ์ƒํƒœ ๋กœ๊น… - ์—ฐ๊ฒฐ ์‹œ์ž‘, ์ข…๋ฃŒ, ์žฌ์—ฐ๊ฒฐ ์ด๋ฒคํŠธ ๊ธฐ๋ก

  2. ์ด๋ฒคํŠธ ์ „์†ก๋Ÿ‰ ์ธก์ • - ์ดˆ๋‹น ์ด๋ฒคํŠธ ์ˆ˜, ๋ฐ์ดํ„ฐ ํฌ๊ธฐ ๋ชจ๋‹ˆํ„ฐ๋ง

  3. ํด๋ผ์ด์–ธํŠธ ์ˆ˜ ์ถ”์  - ํ™œ์„ฑ ์—ฐ๊ฒฐ ์ˆ˜ ๋ชจ๋‹ˆํ„ฐ๋ง

  4. ์˜ค๋ฅ˜ ์ง‘๊ณ„ - ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜ ์œ ํ˜• ๋ฐ ๋นˆ๋„ ๋ถ„์„
// ์„œ๋ฒ„ ์ธก ๋ชจ๋‹ˆํ„ฐ๋ง ์˜ˆ์‹œ
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๋Š” ๋Œ€๋ถ€๋ถ„์˜ ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ž˜ ์ง€์›๋˜์ง€๋งŒ, ์—ฌ์ „ํžˆ ๋ช‡ ๊ฐ€์ง€ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์–ด. ์ด ์„น์…˜์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ์™€ ๊ทธ ๋Œ€์‘ ๋ฐฉ์•ˆ์„ ์•Œ์•„๋ณด์ž.

๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ (2025๋…„ ๊ธฐ์ค€) Chrome Firefox Safari Edge Opera IE ์™„๋ฒฝ ์ง€์› (๋ฒ„์ „ 6+) ์™„๋ฒฝ ์ง€์› (๋ฒ„์ „ 6+) ์™„๋ฒฝ ์ง€์› (๋ฒ„์ „ 5+) ์™„๋ฒฝ ์ง€์› (Chromium ๊ธฐ๋ฐ˜) ์™„๋ฒฝ ์ง€์› (๋ฒ„์ „ 11+) ์ง€์› ์•ˆ ํ•จ (๋ชจ๋“  ๋ฒ„์ „) ์™„๋ฒฝ ์ง€์› ์ง€์› ์•ˆ ํ•จ

์ฃผ์š” ํ˜ธํ™˜์„ฑ ์ด์Šˆ

  1. Internet Explorer - ๋ชจ๋“  ๋ฒ„์ „์—์„œ SSE๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์Œ

  2. ์—ฐ๊ฒฐ ์ œํ•œ - ์ผ๋ถ€ ๋ธŒ๋ผ์šฐ์ €๋Š” ๋„๋ฉ”์ธ๋‹น SSE ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ (๋ณดํ†ต 6๊ฐœ)

  3. ์˜ค๋ž˜๋œ ๋ชจ๋ฐ”์ผ ๋ธŒ๋ผ์šฐ์ € - ์ผ๋ถ€ ๊ตฌํ˜• ๋ชจ๋ฐ”์ผ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง€์› ์ œํ•œ์ 

  4. ํ”„๋ก์‹œ ์„œ๋ฒ„ - ์ผ๋ถ€ ํ”„๋ก์‹œ๋Š” ์žฅ์‹œ๊ฐ„ ์—ฐ๊ฒฐ์„ ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ์Œ

ํด๋ฆฌํ•„ ๋ฐ ๋Œ€์ฒด ๋ฐฉ์•ˆ

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);
};

๋Œ€์ฒด ์ ‘๊ทผ ๋ฐฉ์‹

  1. ๋กฑ ํด๋ง - ์ฃผ๊ธฐ์ ์œผ๋กœ ์„œ๋ฒ„์— ์š”์ฒญ์„ ๋ณด๋‚ด ์—…๋ฐ์ดํŠธ ํ™•์ธ

  2. WebSocket - ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด WebSocket ์‚ฌ์šฉ

  3. Ajax ํด๋ง - ๋‹จ์ˆœํ•œ ์ฃผ๊ธฐ์  ํด๋ง์œผ๋กœ ๋Œ€์ฒด

  4. ์„œ๋“œํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ - EventSource ํด๋ฆฌํ•„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ

์žฌ๋Šฅ๋„ท๊ณผ ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋Š” ์‚ฌ์šฉ์ž ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ์ตœ์ ์˜ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  ๋ฐฉ์‹์„ ์ž๋™์œผ๋กœ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ข‹์€ ์ „๋žต์ด์•ผ!

์—ฐ๊ฒฐ ์ œํ•œ ๊ทน๋ณตํ•˜๊ธฐ

๋ธŒ๋ผ์šฐ์ €์˜ ์—ฐ๊ฒฐ ์ œํ•œ(๋ณดํ†ต ๋„๋ฉ”์ธ๋‹น 6๊ฐœ)์„ ๊ทน๋ณตํ•˜๋Š” ๋ฐฉ๋ฒ•:

  1. ์ด๋ฒคํŠธ ๋ฉ€ํ‹ฐํ”Œ๋ ‰์‹ฑ - ์—ฌ๋Ÿฌ ์ข…๋ฅ˜์˜ ์ด๋ฒคํŠธ๋ฅผ ํ•˜๋‚˜์˜ SSE ์—ฐ๊ฒฐ๋กœ ์ฒ˜๋ฆฌ

  2. ์„œ๋ธŒ๋„๋ฉ”์ธ ํ™œ์šฉ - ์—ฌ๋Ÿฌ ์„œ๋ธŒ๋„๋ฉ”์ธ์œผ๋กœ ์—ฐ๊ฒฐ ๋ถ„์‚ฐ

  3. ํ•„์š”ํ•  ๋•Œ๋งŒ ์—ฐ๊ฒฐ - ํ•ญ์ƒ ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•˜์ง€ ์•Š๊ณ  ํ•„์š”ํ•  ๋•Œ๋งŒ ์—ฐ๊ฒฐ

  4. WebSocket ๊ณ ๋ ค - ๋งŽ์€ ์—ฐ๊ฒฐ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด WebSocket์œผ๋กœ ์ „ํ™˜

9. ๋งˆ๋ฌด๋ฆฌ ๋ฐ ์ถ”๊ฐ€ ์ž๋ฃŒ ๐Ÿ“š

์ง€๊ธˆ๊นŒ์ง€ Server-Sent Events์˜ ๊ธฐ๋ณธ ๊ฐœ๋…๋ถ€ํ„ฐ ๊ณ ๊ธ‰ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•, ๊ทธ๋ฆฌ๊ณ  2025๋…„ ์ตœ์‹  ํŠธ๋ Œ๋“œ๊นŒ์ง€ ์‚ดํŽด๋ดค์–ด. SSE๋Š” ๋‹จ๋ฐฉํ–ฅ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์ด ํ•„์š”ํ•œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๊ฐ„๋‹จํ•˜๋ฉด์„œ๋„ ๊ฐ•๋ ฅํ•œ ์†”๋ฃจ์…˜์„ ์ œ๊ณตํ•ด.

ํ•ต์‹ฌ ์š”์•ฝ

  1. SSE๋Š” ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ์˜ ๋‹จ๋ฐฉํ–ฅ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์„ ์œ„ํ•œ ๊ธฐ์ˆ 

  2. ํ‘œ์ค€ HTTP ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„์ด ๊ฐ„๋‹จํ•˜๊ณ  ๊ธฐ์กด ์ธํ”„๋ผ์™€ ํ˜ธํ™˜์„ฑ์ด ์ข‹์Œ

  3. ์ž๋™ ์žฌ์—ฐ๊ฒฐ๊ณผ ์ด๋ฒคํŠธ ID๋ฅผ ํ†ตํ•œ ์ด์–ด๋ฐ›๊ธฐ ๊ธฐ๋Šฅ ์ œ๊ณต

  4. WebSocket๋ณด๋‹ค ๊ตฌํ˜„์ด ๊ฐ„๋‹จํ•˜๊ณ  HTTP ์ธํ”„๋ผ์™€ ํ˜ธํ™˜์„ฑ์ด ์ข‹์Œ

  5. ์•Œ๋ฆผ, ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ, ๋Œ€์‹œ๋ณด๋“œ ๋“ฑ ๋‹ค์–‘ํ•œ ์‹ค์‹œ๊ฐ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ ํ•ฉ

์ถ”๊ฐ€ ํ•™์Šต ์ž๋ฃŒ

  1. MDN Web Docs - Server-Sent Events API

  2. HTML5 Rocks - Stream Updates with Server-Sent Events

  3. GitHub - EventSource ํด๋ฆฌํ•„

  4. WHATWG - Server-Sent Events ๋ช…์„ธ

SSE๋Š” ๋‹จ์ˆœํ•จ๊ณผ ๊ฐ•๋ ฅํ•จ์„ ๋™์‹œ์— ๊ฐ–์ถ˜ ๊ธฐ์ˆ ์ด์•ผ. ๋ณต์žกํ•œ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ ๋งŽ์€ ์‹ค์‹œ๊ฐ„ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ WebSocket๋ณด๋‹ค ๋” ์ ํ•ฉํ•œ ์„ ํƒ์ผ ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ์žฌ๋Šฅ๋„ท๊ณผ ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•œ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ๊ณผ ์—…๋ฐ์ดํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ์™„๋ฒฝํ•œ ์†”๋ฃจ์…˜์ด์ง€!

์ด ๊ธ€์ด SSE๋ฅผ ์ดํ•ดํ•˜๊ณ  ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜์—ˆ๊ธธ ๋ฐ”๋ผ. ์‹ค์‹œ๊ฐ„ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์— ๋„์ „ํ•ด๋ณด๊ณ , ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ๋” ๋‚˜์€ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ด๋ณด์ž! ๐Ÿš€

1. Server-Sent Events๋ž€ ๋ฌด์—‡์ธ๊ฐ€? ๐Ÿค”

Server-Sent Events(SSE)๋Š” HTTP ์—ฐ๊ฒฐ์„ ํ†ตํ•ด ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ์ž๋™ ์—…๋ฐ์ดํŠธ๋ฅผ ํ‘ธ์‹œํ•˜๋Š” ๊ธฐ์ˆ ์ด์•ผ. ์›น์†Œ์ผ“๊ณผ ๋‹ฌ๋ฆฌ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ๋งŒ ์ง€์›ํ•˜์ง€๋งŒ, ๊ทธ๋งŒํผ ๊ตฌํ˜„์ด ๊ฐ„๋‹จํ•˜๊ณ  HTTP ํ”„๋กœํ† ์ฝœ์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ์กด ์ธํ”„๋ผ์™€์˜ ํ˜ธํ™˜์„ฑ์ด ๋›ฐ์–ด๋‚˜๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์–ด.

์„œ๋ฒ„ ํด๋ผ์ด์–ธํŠธ HTTP ์—ฐ๊ฒฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ

SSE์˜ ํ•ต์‹ฌ ํŠน์ง• โœจ

  1. ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹  - ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ๋งŒ ๋ฐ์ดํ„ฐ๊ฐ€ ํ๋ฆ„

  2. ์ž๋™ ์žฌ์—ฐ๊ฒฐ - ์—ฐ๊ฒฐ์ด ๋Š์–ด์ ธ๋„ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ์—ฐ๊ฒฐ ์‹œ๋„

  3. ์ด๋ฒคํŠธ ID - ๊ฐ ์ด๋ฒคํŠธ์— ID๋ฅผ ๋ถ€์—ฌํ•ด ์—ฐ๊ฒฐ์ด ๋Š๊ธด ํ›„ ๋งˆ์ง€๋ง‰์œผ๋กœ ๋ฐ›์€ ์ด๋ฒคํŠธ๋ถ€ํ„ฐ ๋‹ค์‹œ ์ˆ˜์‹  ๊ฐ€๋Šฅ

  4. ํ‘œ์ค€ HTTP ์‚ฌ์šฉ - ํŠน๋ณ„ํ•œ ํ”„๋กœํ† ์ฝœ์ด ํ•„์š” ์—†์–ด ๋ฐฉํ™”๋ฒฝ์ด๋‚˜ ํ”„๋ก์‹œ ๋ฌธ์ œ๊ฐ€ ์ ์Œ

  5. ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ํ†ต์‹  - ์ฃผ๋กœ ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ „์†ก์— ์ตœ์ ํ™”๋จ

SSE๋Š” ์ฃผ์‹ ์‹œ์„ธ, ์†Œ์…œ ๋ฏธ๋””์–ด ํ”ผ๋“œ, ์•Œ๋ฆผ ์‹œ์Šคํ…œ, ์‹ค์‹œ๊ฐ„ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ๋“ฑ ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ์ง€์†์ ์ธ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ํŠนํžˆ ์œ ์šฉํ•ด. 2025๋…„ ํ˜„์žฌ, ๋งŽ์€ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ๋“ค์ด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•ด SSE๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์žˆ์ง€!

2. SSE vs WebSocket: ์–ธ์ œ ๋ฌด์—‡์„ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ? ๐Ÿคทโ€โ™‚๏ธ

์‹ค์‹œ๊ฐ„ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•  ๋•Œ ํ•ญ์ƒ ๋งˆ์ฃผ์น˜๋Š” ์งˆ๋ฌธ์ด ์žˆ์–ด. "WebSocket์„ ์จ์•ผ ํ• ๊นŒ, SSE๋ฅผ ์จ์•ผ ํ• ๊นŒ?" ๋‘ ๊ธฐ์ˆ  ๋ชจ๋‘ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜์ง€๋งŒ, ๊ฐ๊ฐ์˜ ํŠน์„ฑ๊ณผ ์žฅ๋‹จ์ ์ด ๋‹ฌ๋ผ. ์ด ์ฐจ์ด์ ์„ ์ดํ•ดํ•˜๋ฉด ํ”„๋กœ์ ํŠธ์— ๋งž๋Š” ๊ธฐ์ˆ ์„ ์„ ํƒํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋  ๊ฑฐ์•ผ.

SSE vs WebSocket Server-Sent Events WebSocket โœ… HTTP ๊ธฐ๋ฐ˜ (๊ธฐ์กด ์ธํ”„๋ผ ํ™œ์šฉ) โœ… ๊ตฌํ˜„ ๊ฐ„๋‹จ โœ… ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ โœ… ์ด๋ฒคํŠธ ID๋กœ ์ด์–ด๋ฐ›๊ธฐ ๊ฐ€๋Šฅ โŒ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ๋งŒ ๊ฐ€๋Šฅ โŒ ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ์— ์ตœ์ ํ™” โŒ ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ (๋ธŒ๋ผ์šฐ์ €๋‹น) โœ… ์–‘๋ฐฉํ–ฅ ํ†ต์‹  โœ… ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ ์ „์†ก ๊ฐ€๋Šฅ โœ… ๋‚ฎ์€ ์ง€์—ฐ ์‹œ๊ฐ„ โœ… ํ”„๋กœํ† ์ฝœ ํ™•์žฅ ๊ฐ€๋Šฅ โŒ ๊ตฌํ˜„ ๋ณต์žก๋„ ๋†’์Œ โŒ ๋ฐฉํ™”๋ฒฝ/ํ”„๋ก์‹œ ๋ฌธ์ œ ๊ฐ€๋Šฅ์„ฑ โŒ ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ ์—†์Œ

์–ธ์ œ SSE๋ฅผ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ? ๐ŸŽฏ

  1. ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ์˜ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ์ด ์ฃผ์š” ์š”๊ตฌ์‚ฌํ•ญ์ผ ๋•Œ

  2. ์•Œ๋ฆผ, ๋‰ด์Šค ํ”ผ๋“œ, ์ฃผ์‹ ์‹œ์„ธ ๋“ฑ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•  ๋•Œ

  3. ๊ธฐ์กด HTTP ์ธํ”„๋ผ๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์‹ถ์„ ๋•Œ

  4. ๊ตฌํ˜„ ๋ณต์žก๋„๋ฅผ ๋‚ฎ์ถ”๊ณ  ์‹ถ์„ ๋•Œ

  5. ์—ฐ๊ฒฐ์ด ๋Š๊ฒผ์„ ๋•Œ ์ž๋™ ์žฌ์—ฐ๊ฒฐ๊ณผ ์ด๋ฒคํŠธ ๋ณต๊ตฌ๊ฐ€ ์ค‘์š”ํ•  ๋•Œ

์‹ค์ œ ์‚ฌ๋ก€: ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ ์ œ์•ˆ์ด๋‚˜ ๋ฉ”์‹œ์ง€ ์•Œ๋ฆผ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ›๊ณ  ์‹ถ์„ ๋•Œ, 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)๋กœ ๋๋‚˜์•ผ ํ•œ๋‹ค๋Š” ์ ์„ ๊ธฐ์–ตํ•ด!

์„œ๋ฒ„ ํด๋ผ์ด์–ธํŠธ data: {"value": 42} event: update data: {"updated": true} HTTP ์—ฐ๊ฒฐ ์œ ์ง€ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ (๋‹จ๋ฐฉํ–ฅ)