์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ Server-Sent Events๋กœ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ๊ตฌํ˜„ํ•˜๊ธฐ: ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ƒ๋™๊ฐ์„ ๋ถˆ์–ด๋„ฃ๋Š” ๋งˆ๋ฒ• โœจ

๐Ÿš€ Server-Sent Events๋กœ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ๊ตฌํ˜„ํ•˜๊ธฐ: ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ƒ๋™๊ฐ์„ ๋ถˆ์–ด๋„ฃ๋Š” ๋งˆ๋ฒ• โœจ

์„œ๋ฒ„ ํด๋ผ์ด์–ธํŠธ 1 2 3 ์—ฐ๊ฒฐ ์š”์ฒญ ์ด๋ฒคํŠธ ์ „์†ก ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ Server-Sent Events ํ๋ฆ„๋„

์•ˆ๋…•ํ•˜์„ธ์š”, ๊ฐœ๋ฐœ์ž ์—ฌ๋Ÿฌ๋ถ„! ๐Ÿ™Œ ์˜ค๋Š˜์€ 2025๋…„ 3์›” 13์ผ, ์›น ๊ฐœ๋ฐœ์˜ ํ•ซํ•œ ํŠธ๋ Œ๋“œ ์ค‘ ํ•˜๋‚˜์ธ Server-Sent Events(SSE)์— ๋Œ€ํ•ด ์•Œ์•„๋ณผ๊ฒŒ์š”. ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋Œ€์„ธ์ธ ์š”์ฆ˜, SSE๋Š” ์ •๋ง ๊ฟ€ํ…œ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์ฃ ! ใ…‹ใ…‹ใ…‹

์žฌ๋Šฅ๋„ท์—์„œ ํ”„๋ฆฌ๋žœ์„œ ๊ฐœ๋ฐœ์ž๋กœ ํ™œ๋™ํ•˜์‹œ๋Š” ๋ถ„๋“ค์ด๋ผ๋ฉด ์ด ๊ธฐ์ˆ  ํ•˜๋‚˜ ์ œ๋Œ€๋กœ ์•Œ์•„๋‘๋ฉด ํด๋ผ์ด์–ธํŠธ์˜ ๋งŒ์กฑ๋„๋ฅผ ํ™• ์˜ฌ๋ฆด ์ˆ˜ ์žˆ์„ ๊ฑฐ์˜ˆ์š”. ํŠนํžˆ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ, ๋ผ์ด๋ธŒ ํ”ผ๋“œ, ์ฃผ์‹ ์‹œ์„ธ ์—…๋ฐ์ดํŠธ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ๋•Œ ์™„์ „ ์œ ์šฉํ•˜๋‹ต๋‹ˆ๋‹ค! ๐Ÿ˜Ž

๐Ÿ“Œ ๋ชฉ์ฐจ

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

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

Server-Sent Events(SSE)๋Š” ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ์„ ์ œ๊ณตํ•˜๋Š” ์›น ๊ธฐ์ˆ ์ด์—์š”. ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, ์„œ๋ฒ„๊ฐ€ "์•ผ! ์ƒˆ๋กœ์šด ์ •๋ณด๊ฐ€ ์žˆ์–ด!"๋ผ๊ณ  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์•Œ๋ ค์ฃผ๋Š” ๋ฐฉ์‹์ด์ฃ . ๊ธฐ์กด์˜ HTTP ์š”์ฒญ์ฒ˜๋Ÿผ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์— ๊ณ„์† ๋ฌผ์–ด๋ณด๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ์„œ๋ฒ„๊ฐ€ ํ•„์š”ํ•  ๋•Œ ์•Œ์•„์„œ ์ •๋ณด๋ฅผ ๋ณด๋‚ด์ค€๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

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

๐Ÿ’ก SSE์˜ ํ•ต์‹ฌ ํŠน์ง•

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

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

3. ์ด๋ฒคํŠธ ID: ๊ฐ ์ด๋ฒคํŠธ์— ID๋ฅผ ๋ถ€์—ฌํ•ด ์–ด๋””๊นŒ์ง€ ๋ฐ›์•˜๋Š”์ง€ ์ถ”์  ๊ฐ€๋Šฅ

4. ์ด๋ฒคํŠธ ํƒ€์ž…: ๋‹ค์–‘ํ•œ ์œ ํ˜•์˜ ์ด๋ฒคํŠธ๋ฅผ ๊ตฌ๋ถ„ํ•ด์„œ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ

5. ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜: JSON์ด๋‚˜ ๋‹ค๋ฅธ ํ…์ŠคํŠธ ํ˜•์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์ „์†ก

SSE๋Š” EventSource API๋ฅผ ํ†ตํ•ด JavaScript์—์„œ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ด API๋Š” ํ˜„์žฌ ๋Œ€๋ถ€๋ถ„์˜ ๋ชจ๋˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง€์›ํ•˜๊ณ  ์žˆ๋‹ต๋‹ˆ๋‹ค. (IE๋Š” ์ œ์™ธ...ใ… ใ…  ํ•˜์ง€๋งŒ ์ด์ œ 2025๋…„์ด๋‹ˆ๊นŒ IE ๊ฑฑ์ •์€ ์•ˆ ํ•ด๋„ ๋˜๊ฒ ์ฃ ? ใ…‹ใ…‹ใ…‹)

2. SSE vs WebSocket vs Long Polling: ๋ญ๊ฐ€ ๋‹ค๋ฅธ๊ฑฐ์ฃ ? ๐Ÿคทโ€โ™‚๏ธ

์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์—ฌ๋Ÿฌ ๊ฐ€์ง€๊ฐ€ ์žˆ๋Š”๋ฐ์š”, ๊ฐ๊ฐ์˜ ์žฅ๋‹จ์ ์„ ๋น„๊ตํ•ด๋ณผ๊ฒŒ์š”!

SSE ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹  HTTP ๊ธฐ๋ฐ˜ ์ž๋™ ์žฌ์—ฐ๊ฒฐ ํ…์ŠคํŠธ๋งŒ ์ „์†ก ๊ฐ€๋Šฅ ์ตœ๋Œ€ ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ IE ๋ฏธ์ง€์› WebSocket ์–‘๋ฐฉํ–ฅ ํ†ต์‹  ๋ณ„๋„ ํ”„๋กœํ† ์ฝœ ์ˆ˜๋™ ์žฌ์—ฐ๊ฒฐ ํ•„์š” ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ ๊ฐ€๋Šฅ ํ™•์žฅ์„ฑ ๋†’์Œ ๋ณต์žกํ•œ ๊ตฌํ˜„ ๋Œ€๋ถ€๋ถ„ ๋ธŒ๋ผ์šฐ์ € ์ง€์› Long Polling ์˜์‚ฌ ์‹ค์‹œ๊ฐ„ HTTP ๊ธฐ๋ฐ˜ ์—ฐ๊ฒฐ ํƒ€์ž„์•„์›ƒ ๋ฌธ์ œ ์„œ๋ฒ„ ๋ถ€ํ•˜ ๋†’์Œ ๋ชจ๋“  ๋ธŒ๋ผ์šฐ์ € ์ง€์› ๊ตฌํ˜„ ๊ฐ„๋‹จ ์ง€์—ฐ ์‹œ๊ฐ„ ์กด์žฌ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  ๊ธฐ์ˆ  ๋น„๊ต

SSE๋Š” ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ์˜ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ์— ์ตœ์ ํ™”๋˜์–ด ์žˆ์–ด์š”. ์•Œ๋ฆผ, ๋‰ด์Šค ํ”ผ๋“œ, ์ฃผ์‹ ์‹œ์„ธ ์—…๋ฐ์ดํŠธ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ๋•Œ ๋”ฑ์ด์ฃ ! ๋ฐ˜๋ฉด WebSocket์€ ์ฑ„ํŒ…, ์˜จ๋ผ์ธ ๊ฒŒ์ž„์ฒ˜๋Ÿผ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์— ๋” ์ ํ•ฉํ•ด์š”.

Long Polling์€... ์Œ... ์†”์งํžˆ 2025๋…„์—๋Š” ๊ฑฐ์˜ ๋ ˆ๊ฑฐ์‹œ ๊ธฐ์ˆ ์ด ๋˜์–ด๊ฐ€๊ณ  ์žˆ์–ด์š”. ใ…‹ใ…‹ใ…‹ ํ•˜์ง€๋งŒ ์•„์ง๋„ ํŠน์ • ์ƒํ™ฉ์—์„œ๋Š” ์‚ฌ์šฉ๋˜๊ธฐ๋„ ํ•œ๋‹ต๋‹ˆ๋‹ค. ํŠนํžˆ ์˜ค๋ž˜๋œ ์‹œ์Šคํ…œ๊ณผ์˜ ํ˜ธํ™˜์„ฑ์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ์—์š”.

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

1. ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์ด ํ•„์š”ํ•  ๋•Œ

2. ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ๋งŒ ์ „์†กํ•˜๋ฉด ๋  ๋•Œ

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

4. ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•  ๋•Œ

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

3. SSE ๊ตฌํ˜„ ๋ฐฉ๋ฒ•: ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๐Ÿ–ฅ๏ธ

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ SSE๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ์š”! ๋จผ์ € ์„œ๋ฒ„ ์‚ฌ์ด๋“œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ• ๊ฒŒ์š”. ์—ฌ๋Ÿฌ ๋ฐฑ์—”๋“œ ์–ธ์–ด/ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ SSE๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ด…์‹œ๋‹ค.

Node.js + Express์—์„œ SSE ๊ตฌํ˜„ํ•˜๊ธฐ

Node.js๋Š” SSE ๊ตฌํ˜„์— ์ •๋ง ์ฐฐ๋–ก๊ถํ•ฉ์ด์—์š”! ๋น„๋™๊ธฐ ํŠน์„ฑ ๋•Œ๋ฌธ์— ๋งŽ์€ ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ์„ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”.


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');
  
  // 2์ดˆ๋งˆ๋‹ค ํ˜„์žฌ ์‹œ๊ฐ„ ๋ณด๋‚ด๊ธฐ
  const intervalId = setInterval(() => {
    const data = `data: {"time": "${new Date().toLocaleTimeString()}"}\n\n`;
    res.write(data);
  }, 2000);
  
  // ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ์ด ๋Š์–ด์กŒ์„ ๋•Œ ์ธํ„ฐ๋ฒŒ ์ •๋ฆฌ
  req.on('close', () => {
    clearInterval(intervalId);
  });
});

app.listen(3000, () => {
  console.log('SSE ์„œ๋ฒ„๊ฐ€ 3000๋ฒˆ ํฌํŠธ์—์„œ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค.');
});
  

์œ„ ์ฝ”๋“œ์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์€ ํ—ค๋” ์„ค์ •์ด์—์š”. SSE๋Š” ํŠน๋ณ„ํ•œ HTTP ํ—ค๋”๋ฅผ ํ•„์š”๋กœ ํ•ฉ๋‹ˆ๋‹ค:

  1. Content-Type: text/event-stream - ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ ํ˜•์‹์ž„์„ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.
  2. Cache-Control: no-cache - ์บ์‹ฑ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  3. Connection: keep-alive - ์—ฐ๊ฒฐ์„ ๊ณ„์† ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ผ ๋•Œ๋Š” 'data: ' ์ ‘๋‘์‚ฌ์™€ '\n\n' ์ ‘๋ฏธ์‚ฌ๋ฅผ ๊ผญ ๋ถ™์—ฌ์•ผ ํ•ด์š”. ์ด๊ฒŒ SSE ํ”„๋กœํ† ์ฝœ์˜ ๊ทœ์น™์ด๋ž๋‹ˆ๋‹ค!

Python + Flask์—์„œ SSE ๊ตฌํ˜„ํ•˜๊ธฐ

Python ๊ฐœ๋ฐœ์ž๋ถ„๋“ค์„ ์œ„ํ•œ ์˜ˆ์ œ๋„ ์ค€๋น„ํ–ˆ์–ด์š”! Flask๋ฅผ ์‚ฌ์šฉํ•œ SSE ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์ด์—์š”.


from flask import Flask, Response
import time
import json

app = Flask(__name__)

@app.route('/events')
def events():
    def generate():
        # ์ดˆ๊ธฐ ์—ฐ๊ฒฐ ๋ฉ”์‹œ์ง€
        yield f"data: {json.dumps({'message': '์—ฐ๊ฒฐ ์„ฑ๊ณต!'})}\n\n"
        
        # 2์ดˆ๋งˆ๋‹ค ํ˜„์žฌ ์‹œ๊ฐ„ ์ „์†ก
        while True:
            time.sleep(2)
            yield f"data: {json.dumps({'time': time.strftime('%H:%M:%S')})}\n\n"
    
    return Response(generate(), mimetype='text/event-stream')

if __name__ == '__main__':
    app.run(debug=True, port=3000)
  

Python์—์„œ๋Š” ์ œ๋„ˆ๋ ˆ์ดํ„ฐ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ŠคํŠธ๋ฆผ์„ ๋งŒ๋“ค์–ด์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ŠคํŠธ๋ฆฌ๋ฐํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค. ์™„์ „ ํŒŒ์ด์ฌ์Šค๋Ÿฌ์šด ๋ฐฉ์‹์ด์ฃ ! ใ…‹ใ…‹

Java + Spring Boot์—์„œ SSE ๊ตฌํ˜„ํ•˜๊ธฐ

์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํ™˜๊ฒฝ์—์„œ ์ผํ•˜์‹œ๋Š” ๋ถ„๋“ค์„ ์œ„ํ•œ Spring Boot ์˜ˆ์ œ๋„ ์žˆ์–ด์š”!


@RestController
public class SSEController {

    @GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> streamEvents() {
        return Flux.interval(Duration.ofSeconds(2))
            .map(sequence -> ServerSentEvent.<String>builder()
                .id(String.valueOf(sequence))
                .event("periodic-event")
                .data("{\"time\": \"" + LocalTime.now().toString() + "\"}")
                .build());
    }
}
  

Spring WebFlux๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์œผ๋กœ SSE๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”. ๋น„๋™๊ธฐ ๋…ผ๋ธ”๋กœํ‚น ๋ฐฉ์‹์ด๋ผ ์„ฑ๋Šฅ๋„ ์ข‹๊ณ , ์ฝ”๋“œ๋„ ๊น”๋”ํ•˜๋‹ต๋‹ˆ๋‹ค! ๐Ÿ‘

4. SSE ๊ตฌํ˜„ ๋ฐฉ๋ฒ•: ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๐Ÿ’ป

์ด์ œ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ์—์„œ SSE๋ฅผ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์•Œ์•„๋ณผ๊ฒŒ์š”. ๋‹คํ–‰ํžˆ๋„ ๋ธŒ๋ผ์šฐ์ €์—๋Š” EventSource API๊ฐ€ ๋‚ด์žฅ๋˜์–ด ์žˆ์–ด์„œ ์ •๋ง ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”!


// SSE ์—ฐ๊ฒฐ ์ƒ์„ฑ
const eventSource = new EventSource('/events');

// ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
eventSource.onmessage = function(event) {
  const data = JSON.parse(event.data);
  console.log('์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ :', data);
  
  // ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋กœ UI ์—…๋ฐ์ดํŠธ
  if (data.time) {
    document.getElementById('current-time').textContent = data.time;
  }
};

// ์—ฐ๊ฒฐ ์„ฑ๊ณต ์ด๋ฒคํŠธ
eventSource.onopen = function() {
  console.log('SSE ์—ฐ๊ฒฐ์ด ์—ด๋ ธ์Šต๋‹ˆ๋‹ค!');
};

// ์—๋Ÿฌ ์ฒ˜๋ฆฌ
eventSource.onerror = function(error) {
  console.error('SSE ์—ฐ๊ฒฐ ์—๋Ÿฌ:', error);
  // ์—ฐ๊ฒฐ ์ข…๋ฃŒ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ)
  // eventSource.close();
};

// ํŠน์ • ์ด๋ฒคํŠธ ํƒ€์ž… ๋ฆฌ์Šค๋‹
eventSource.addEventListener('custom-event', function(event) {
  const data = JSON.parse(event.data);
  console.log('์ปค์Šคํ…€ ์ด๋ฒคํŠธ ์ˆ˜์‹ :', data);
});

// ์—ฐ๊ฒฐ ์ข…๋ฃŒ (ํŽ˜์ด์ง€ ์–ธ๋กœ๋“œ ์‹œ)
window.addEventListener('beforeunload', function() {
  eventSource.close();
});
  

์ •๋ง ๊ฐ„๋‹จํ•˜์ฃ ? new EventSource()๋กœ ์—ฐ๊ฒฐ์„ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋“ฑ๋กํ•˜๋ฉด ๋์ด์—์š”! ์„œ๋ฒ„์—์„œ ์ƒˆ ๋ฐ์ดํ„ฐ๊ฐ€ ์˜ค๋ฉด ์ž๋™์œผ๋กœ onmessage ์ฝœ๋ฐฑ์ด ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก EventSource์˜ ์ฃผ์š” ์ด๋ฒคํŠธ

1. message: ์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ์‹œ ๋ฐœ์ƒ

2. open: ์—ฐ๊ฒฐ์ด ์—ด๋ฆด ๋•Œ ๋ฐœ์ƒ

3. error: ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ํ˜ธ์ถœ

4. ์ปค์Šคํ…€ ์ด๋ฒคํŠธ: ์„œ๋ฒ„์—์„œ ์ •์˜ํ•œ ํŠน์ • ์ด๋ฒคํŠธ ํƒ€์ž…

์„œ๋ฒ„์—์„œ ํŠน์ • ์ด๋ฒคํŠธ ํƒ€์ž…์„ ์ง€์ •ํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด 'event:' ์ ‘๋‘์‚ฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ์š”:


// ์„œ๋ฒ„ ์ฝ”๋“œ (Node.js)
res.write('event: custom-event\n');
res.write('data: {"specialData": "ํŠน๋ณ„ํ•œ ์ด๋ฒคํŠธ์ž…๋‹ˆ๋‹ค!"}\n\n');
  

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ์—์„œ addEventListener('custom-event', ...)๋กœ ํŠน์ • ์ด๋ฒคํŠธ๋งŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”. ์™„์ „ ๊ฟ€ํŒ! ๐Ÿฏ

5. ์‹ค์ „ ์˜ˆ์ œ: ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๋งŒ๋“ค๊ธฐ ๐Ÿ””

์ด์ œ ๋ฐฐ์šด ๋‚ด์šฉ์„ ํ™œ์šฉํ•ด์„œ ๊ฐ„๋‹จํ•œ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”! ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€๋‚˜ ๊ฑฐ๋ž˜ ์ œ์•ˆ์ด ๋“ค์–ด์™”์„ ๋•Œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์•Œ๋ ค์ฃผ๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์„œ๋ฒ„ ์‚ฌ์ด๋“œ ์ฝ”๋“œ (Node.js + Express)


const express = require('express');
const app = express();
const path = require('path');

// ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ์„ ์ €์žฅํ•  ๋ฐฐ์—ด
const clients = [];
// ์•Œ๋ฆผ ID ์นด์šดํ„ฐ
let notificationId = 1;

// ์ •์  ํŒŒ์ผ ์ œ๊ณต
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());

// SSE ์—”๋“œํฌ์ธํŠธ
app.get('/notifications', (req, res) => {
  // SSE ํ—ค๋” ์„ค์ •
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  
  // ํด๋ผ์ด์–ธํŠธ ID ์ƒ์„ฑ (์‹ค์ œ๋กœ๋Š” ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด ์‚ฌ์šฉ)
  const clientId = Date.now();
  
  // ์—ฐ๊ฒฐ ์„ฑ๊ณต ๋ฉ”์‹œ์ง€
  const data = `data: ${JSON.stringify({ id: 0, message: '์•Œ๋ฆผ ์„œ๋น„์Šค์— ์—ฐ๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.' })}\n\n`;
  res.write(data);
  
  // ํด๋ผ์ด์–ธํŠธ ๋ชฉ๋ก์— ์ถ”๊ฐ€
  const newClient = {
    id: clientId,
    response: res
  };
  clients.push(newClient);
  
  // ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์‹œ ํด๋ผ์ด์–ธํŠธ ์ œ๊ฑฐ
  req.on('close', () => {
    console.log(`${clientId} ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ์ข…๋ฃŒ`);
    const index = clients.findIndex(client => client.id === clientId);
    if (index !== -1) {
      clients.splice(index, 1);
    }
  });
});

// ์ƒˆ ์•Œ๋ฆผ ์ƒ์„ฑ API
app.post('/send-notification', (req, res) => {
  const { message, type } = req.body;
  
  if (!message) {
    return res.status(400).json({ error: '๋ฉ”์‹œ์ง€๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.' });
  }
  
  // ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์•Œ๋ฆผ ์ „์†ก
  const notification = {
    id: notificationId++,
    message,
    type: type || 'info',
    timestamp: new Date().toISOString()
  };
  
  sendNotificationToAll(notification);
  
  res.status(200).json({ success: true, notification });
});

// ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์•Œ๋ฆผ ์ „์†กํ•˜๋Š” ํ•จ์ˆ˜
function sendNotificationToAll(notification) {
  clients.forEach(client => {
    client.response.write(`event: ${notification.type}\n`);
    client.response.write(`id: ${notification.id}\n`);
    client.response.write(`data: ${JSON.stringify(notification)}\n\n`);
  });
}

// ํ…Œ์ŠคํŠธ์šฉ ์ฃผ๊ธฐ์  ์•Œ๋ฆผ
setInterval(() => {
  const testNotification = {
    id: notificationId++,
    message: `์ž๋™ ํ…Œ์ŠคํŠธ ์•Œ๋ฆผ #${notificationId}`,
    type: 'system',
    timestamp: new Date().toISOString()
  };
  sendNotificationToAll(testNotification);
}, 60000); // 1๋ถ„๋งˆ๋‹ค

app.listen(3000, () => {
  console.log('์•Œ๋ฆผ ์„œ๋ฒ„๊ฐ€ 3000๋ฒˆ ํฌํŠธ์—์„œ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค.');
});
  

ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์ฝ”๋“œ (HTML + JavaScript)


// notifications.js
document.addEventListener('DOMContentLoaded', () => {
  const notificationsList = document.getElementById('notifications-list');
  const notificationSound = document.getElementById('notification-sound');
  const connectionStatus = document.getElementById('connection-status');
  
  // ์•Œ๋ฆผ ์นด์šดํ„ฐ
  let unreadCount = 0;
  updateUnreadBadge();
  
  // SSE ์—ฐ๊ฒฐ ์„ค์ •
  const eventSource = new EventSource('/notifications');
  
  // ์—ฐ๊ฒฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
  eventSource.onopen = () => {
    connectionStatus.textContent = '์—ฐ๊ฒฐ๋จ';
    connectionStatus.className = 'status-connected';
  };
  
  // ์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
  eventSource.onmessage = (event) => {
    const notification = JSON.parse(event.data);
    addNotificationToUI(notification, 'info');
  };
  
  // ํŠน์ • ํƒ€์ž…์˜ ์•Œ๋ฆผ ์ฒ˜๋ฆฌ
  eventSource.addEventListener('info', (event) => {
    const notification = JSON.parse(event.data);
    addNotificationToUI(notification, 'info');
  });
  
  eventSource.addEventListener('success', (event) => {
    const notification = JSON.parse(event.data);
    addNotificationToUI(notification, 'success');
  });
  
  eventSource.addEventListener('warning', (event) => {
    const notification = JSON.parse(event.data);
    addNotificationToUI(notification, 'warning');
  });
  
  eventSource.addEventListener('error', (event) => {
    const notification = JSON.parse(event.data);
    addNotificationToUI(notification, 'error');
  });
  
  eventSource.addEventListener('system', (event) => {
    const notification = JSON.parse(event.data);
    addNotificationToUI(notification, 'system');
  });
  
  // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
  eventSource.onerror = () => {
    connectionStatus.textContent = '์—ฐ๊ฒฐ ๋Š๊น€ - ์žฌ์—ฐ๊ฒฐ ์ค‘...';
    connectionStatus.className = 'status-disconnected';
  };
  
  // UI์— ์•Œ๋ฆผ ์ถ”๊ฐ€ํ•˜๋Š” ํ•จ์ˆ˜
  function addNotificationToUI(notification, type) {
    // ์•Œ๋ฆผ์Œ ์žฌ์ƒ
    notificationSound.play();
    
    // ์ฝ์ง€ ์•Š์€ ์•Œ๋ฆผ ์นด์šดํŠธ ์ฆ๊ฐ€
    unreadCount++;
    updateUnreadBadge();
    
    // ์•Œ๋ฆผ ์š”์†Œ ์ƒ์„ฑ
    const notificationElement = document.createElement('div');
    notificationElement.className = `notification ${type}`;
    notificationElement.dataset.id = notification.id;
    
    // ์‹œ๊ฐ„ ํฌ๋งทํŒ…
    const time = new Date(notification.timestamp).toLocaleTimeString();
    
    // ์•Œ๋ฆผ ๋‚ด์šฉ ์„ค์ •
    notificationElement.innerHTML = `
      <div class="notification-time">${time}</div>
      <div class="notification-message">${notification.message}</div>
      <button class="notification-close">ร—</button>
    `;
    
    // ๋‹ซ๊ธฐ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
    const closeButton = notificationElement.querySelector('.notification-close');
    closeButton.addEventListener('click', () => {
      notificationElement.classList.add('removing');
      setTimeout(() => {
        notificationElement.remove();
      }, 300);
    });
    
    // ๋ชฉ๋ก ๋งจ ์œ„์— ์ถ”๊ฐ€
    notificationsList.prepend(notificationElement);
    
    // ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ
    setTimeout(() => {
      notificationElement.classList.add('show');
    }, 10);
  }
  
  // ์ฝ์ง€ ์•Š์€ ์•Œ๋ฆผ ๋ฐฐ์ง€ ์—…๋ฐ์ดํŠธ
  function updateUnreadBadge() {
    const badge = document.getElementById('notifications-badge');
    if (unreadCount > 0) {
      badge.textContent = unreadCount;
      badge.classList.add('active');
      // ํƒ€์ดํ‹€์—๋„ ํ‘œ์‹œ
      document.title = `(${unreadCount}) ์•Œ๋ฆผ ์‹œ์Šคํ…œ`;
    } else {
      badge.classList.remove('active');
      document.title = '์•Œ๋ฆผ ์‹œ์Šคํ…œ';
    }
  }
  
  // ๋ชจ๋“  ์•Œ๋ฆผ ์ฝ์Œ ์ฒ˜๋ฆฌ
  document.getElementById('mark-all-read').addEventListener('click', () => {
    unreadCount = 0;
    updateUnreadBadge();
  });
  
  // ํ…Œ์ŠคํŠธ ์•Œ๋ฆผ ์ „์†ก
  document.getElementById('send-test').addEventListener('click', () => {
    fetch('/send-notification', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        message: 'ํ…Œ์ŠคํŠธ ์•Œ๋ฆผ์ž…๋‹ˆ๋‹ค!',
        type: 'success'
      })
    });
  });
  
  // ํŽ˜์ด์ง€ ์–ธ๋กœ๋“œ ์‹œ ์—ฐ๊ฒฐ ์ข…๋ฃŒ
  window.addEventListener('beforeunload', () => {
    eventSource.close();
  });
});
  

์ด ์˜ˆ์ œ๋Š” ๋‹ค์–‘ํ•œ ํƒ€์ž…์˜ ์•Œ๋ฆผ์„ ์ฒ˜๋ฆฌํ•˜๊ณ , ์•Œ๋ฆผ์Œ ์žฌ์ƒ, ์ฝ์ง€ ์•Š์€ ์•Œ๋ฆผ ์นด์šดํŠธ ํ‘œ์‹œ ๋“ฑ ์‹ค์ œ ์„œ๋น„์Šค์—์„œ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ๊ตฌํ˜„ํ–ˆ์–ด์š”. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€, ๊ฑฐ๋ž˜ ์ œ์•ˆ, ๊ฒฐ์ œ ์™„๋ฃŒ ๋“ฑ ๋‹ค์–‘ํ•œ ์•Œ๋ฆผ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค! ๐Ÿ˜Š

์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ 3 14:32 ์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€๊ฐ€ ๋„์ฐฉํ–ˆ์Šต๋‹ˆ๋‹ค! 14:28 ๊ฑฐ๋ž˜๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. 14:15 ์ƒˆ๋กœ์šด ๊ฑฐ๋ž˜ ์ œ์•ˆ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ UI ์˜ˆ์‹œ

6. SSE์˜ ํ•œ๊ณ„์ ๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• ๐Ÿšง

SSE๋Š” ์ •๋ง ์œ ์šฉํ•œ ๊ธฐ์ˆ ์ด์ง€๋งŒ, ๋ช‡ ๊ฐ€์ง€ ํ•œ๊ณ„์ ๋„ ์žˆ์–ด์š”. ์ด๋Ÿฐ ํ•œ๊ณ„์ ๋“ค์„ ์•Œ๊ณ  ์ ์ ˆํ•œ ํ•ด๊ฒฐ์ฑ…์„ ์ ์šฉํ•˜๋ฉด ๋” ์•ˆ์ •์ ์ธ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค!

โš ๏ธ SSE์˜ ์ฃผ์š” ํ•œ๊ณ„์ 

  1. ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ: ๋ธŒ๋ผ์šฐ์ €๋‹น ๋„๋ฉ”์ธ๋ณ„ ๋™์‹œ ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ (Chrome: 6๊ฐœ)
  2. ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ : ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์—†์Œ
  3. IE ๋ฏธ์ง€์›: Internet Explorer์—์„œ๋Š” ์ง€์›ํ•˜์ง€ ์•Š์Œ
  4. ํ”„๋ก์‹œ ๋ฌธ์ œ: ์ผ๋ถ€ ํ”„๋ก์‹œ๊ฐ€ ์žฅ์‹œ๊ฐ„ ์—ฐ๊ฒฐ์„ ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ์Œ
  5. ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ: ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ๋งŒ ์ „์†ก ๊ฐ€๋Šฅ

ํ•œ๊ณ„์  ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ ํ•ด๊ฒฐ

๋ธŒ๋ผ์šฐ์ €๋Š” ๋„๋ฉ”์ธ๋‹น ์—ฐ๊ฒฐ ์ˆ˜๋ฅผ ์ œํ•œํ•˜๋Š”๋ฐ, ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•์ด ์žˆ์–ด์š”:

  • ์„œ๋ธŒ๋„๋ฉ”์ธ ํ™œ์šฉ: events1.example.com, events2.example.com ๋“ฑ
  • ์—ฐ๊ฒฐ ๊ณต์œ : ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ์ด ํ•˜๋‚˜์˜ SSE ์—ฐ๊ฒฐ์„ ๊ณต์œ ํ•˜๋„๋ก ์„ค๊ณ„
  • ํ•„์š”ํ•  ๋•Œ๋งŒ ์—ฐ๊ฒฐ: ํ•ญ์ƒ ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•˜์ง€ ์•Š๊ณ  ํ•„์š”ํ•  ๋•Œ๋งŒ ์—ฐ๊ฒฐ

2. ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹  ํ•œ๊ณ„ ๊ทน๋ณต

SSE๋Š” ์„œ๋ฒ„โ†’ํด๋ผ์ด์–ธํŠธ ๋ฐฉํ–ฅ์œผ๋กœ๋งŒ ํ†ต์‹ ์ด ๊ฐ€๋Šฅํ•ด์š”. ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด:

  • SSE + AJAX ์กฐํ•ฉ: SSE๋กœ ์ˆ˜์‹ ํ•˜๊ณ , ํ•„์š”ํ•  ๋•Œ AJAX๋กœ ๋ฐ์ดํ„ฐ ์ „์†ก
  • WebSocket ๊ณ ๋ ค: ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์ด ๋งŽ์ด ํ•„์š”ํ•˜๋ฉด WebSocket์œผ๋กœ ์ „ํ™˜

3. IE ์ง€์› ๋ฌธ์ œ

2025๋…„์—๋Š” IE ์ง€์›์ด ๊ฑฐ์˜ ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์ง€๋งŒ, ์—ฌ์ „ํžˆ ๋ ˆ๊ฑฐ์‹œ ์‹œ์Šคํ…œ์„ ์ง€์›ํ•ด์•ผ ํ•œ๋‹ค๋ฉด:

  • ํด๋ฆฌํ•„ ์‚ฌ์šฉ: EventSource ํด๋ฆฌํ•„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ™œ์šฉ
  • ํด๋ฐฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜: IE์—์„œ๋Š” Long Polling์œผ๋กœ ๋Œ€์ฒด

// IE์šฉ ํด๋ฐฑ ์ฝ”๋“œ ์˜ˆ์‹œ
let eventSource;

if (typeof EventSource !== 'undefined') {
  // SSE ์ง€์› ๋ธŒ๋ผ์šฐ์ €
  eventSource = new EventSource('/events');
  // ... ์ผ๋ฐ˜์ ์ธ SSE ์ฒ˜๋ฆฌ
} else {
  // SSE ๋ฏธ์ง€์› ๋ธŒ๋ผ์šฐ์ € (IE)
  function longPoll() {
    fetch('/events-poll')
      .then(response => response.json())
      .then(data => {
        // ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
        handleEventData(data);
        // ๋‹ค์‹œ ํด๋ง
        longPoll();
      })
      .catch(error => {
        console.error('ํด๋ง ์—๋Ÿฌ:', error);
        // ์ž ์‹œ ํ›„ ์žฌ์‹œ๋„
        setTimeout(longPoll, 5000);
      });
  }
  
  longPoll();
}
  

4. ํ”„๋ก์‹œ ๋ฌธ์ œ ํ•ด๊ฒฐ

์ผ๋ถ€ ํ”„๋ก์‹œ๋Š” ์žฅ์‹œ๊ฐ„ ์—ฐ๊ฒฐ์„ ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ์–ด์š”:

  • ์ฃผ๊ธฐ์ ์ธ ๋”๋ฏธ ๋ฐ์ดํ„ฐ: ์—ฐ๊ฒฐ ์œ ์ง€๋ฅผ ์œ„ํ•ด ์ฃผ๊ธฐ์ ์œผ๋กœ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ „์†ก
  • ์ž๋™ ์žฌ์—ฐ๊ฒฐ: ์—ฐ๊ฒฐ์ด ๋Š์–ด์ง€๋ฉด ์ž๋™์œผ๋กœ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„
  • ์—ฐ๊ฒฐ ํƒ€์ž„์•„์›ƒ ์„ค์ •: ์„œ๋ฒ„์—์„œ ์ ์ ˆํ•œ ํƒ€์ž„์•„์›ƒ ์„ค์ •

5. ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ ์ „์†ก

SSE๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํ…์ŠคํŠธ๋งŒ ์ „์†ก ๊ฐ€๋Šฅํ•ด์š”. ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด:

  • Base64 ์ธ์ฝ”๋”ฉ: ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ Base64๋กœ ์ธ์ฝ”๋”ฉํ•˜์—ฌ ์ „์†ก
  • ๋งํฌ ์ „์†ก: ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ ๋Œ€์‹  ๋‹ค์šด๋กœ๋“œ ๋งํฌ ์ „์†ก
  • WebSocket ๊ณ ๋ ค: ๋Œ€์šฉ๋Ÿ‰ ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ๋‹ค๋ฉด WebSocket ์‚ฌ์šฉ

7. 2025๋…„ ์ตœ์‹  SSE ํ™œ์šฉ ํŠธ๋ Œ๋“œ ๐Ÿš€

2025๋…„ ํ˜„์žฌ, SSE๋Š” ๋‹ค์–‘ํ•œ ๋ฐฉ์‹์œผ๋กœ ์ง„ํ™”ํ•˜๊ณ  ์žˆ์–ด์š”. ์ตœ์‹  ํŠธ๋ Œ๋“œ์™€ ํ™œ์šฉ ์‚ฌ๋ก€๋ฅผ ์‚ดํŽด๋ณผ๊ฒŒ์š”!

๋งˆ์ดํฌ๋กœํ”„๋ก ํŠธ์—”๋“œ์—์„œ์˜ SSE ํ™œ์šฉ

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


// ์ค‘์•™ ์ด๋ฒคํŠธ ๋ฒ„์Šค ๊ตฌ๋… (๊ฐ ๋งˆ์ดํฌ๋กœํ”„๋ก ํŠธ์—”๋“œ์—์„œ)
const eventSource = new EventSource('/state-events');

eventSource.addEventListener('state-change', event => {
  const { module, data } = JSON.parse(event.data);
  
  // ๊ด€๋ จ ๋ชจ๋“ˆ์˜ ์ƒํƒœ ๋ณ€ํ™”๋งŒ ์ฒ˜๋ฆฌ
  if (module === 'shopping-cart' || module === 'all') {
    updateShoppingCartUI(data);
  }
});
  

Edge Computing๊ณผ SSE

2025๋…„์—๋Š” Edge Computing์ด ๋”์šฑ ๋ณดํŽธํ™”๋˜์—ˆ์–ด์š”. Cloudflare Workers, Vercel Edge Functions ๊ฐ™์€ ์„œ๋น„์Šค๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž์™€ ๊ฐ€๊นŒ์šด ์œ„์น˜์—์„œ SSE๋ฅผ ์ œ๊ณตํ•˜๋ฉด ์ง€์—ฐ ์‹œ๊ฐ„์„ ํฌ๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค!


// Cloudflare Worker์—์„œ SSE ๊ตฌํ˜„ ์˜ˆ์‹œ
addEventListener('fetch', event => {
  if (event.request.url.includes('/events')) {
    return event.respondWith(handleSSE(event));
  }
  // ๋‹ค๋ฅธ ์š”์ฒญ ์ฒ˜๋ฆฌ
});

async function handleSSE(event) {
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    start(controller) {
      // ์ดˆ๊ธฐ ๋ฉ”์‹œ์ง€
      controller.enqueue(encoder.encode("data: ์—ฐ๊ฒฐ๋จ\n\n"));
      
      // KV์—์„œ ์ƒํƒœ ๋ณ€ํ™” ๊ฐ์ง€ํ•˜์—ฌ ์ด๋ฒคํŠธ ์ „์†ก
      const intervalId = setInterval(async () => {
        const updates = await NAMESPACE.get('updates');
        if (updates) {
          controller.enqueue(encoder.encode(`data: ${updates}\n\n`));
          await NAMESPACE.delete('updates');
        }
      }, 1000);
      
      // ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์‹œ ์ •๋ฆฌ
      event.waitUntil(
        new Promise(() => {
          // ์—ฐ๊ฒฐ์ด ๋Š์–ด์งˆ ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
          // (์‹ค์ œ๋กœ๋Š” ๋” ๋ณต์žกํ•œ ๋กœ์ง ํ•„์š”)
        }).finally(() => {
          clearInterval(intervalId);
        })
      );
    }
  });
  
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    }
  });
}
  

AI ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ์ถ”์ฒœ ์‹œ์Šคํ…œ

2025๋…„์—๋Š” AI ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ์ถ”์ฒœ์ด ๋Œ€์„ธ์˜ˆ์š”! ์‚ฌ์šฉ์ž์˜ ํ–‰๋™์— ๋”ฐ๋ผ AI ๋ชจ๋ธ์ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ถ”์ฒœ ํ•ญ๋ชฉ์„ ์ƒ์„ฑํ•˜๊ณ , SSE๋ฅผ ํ†ตํ•ด ์ฆ‰์‹œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์ด ์ธ๊ธฐ๋ž๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์žฌ๋Šฅ๋„ท์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ํƒ์ƒ‰ํ•˜๋ฉด, AI๊ฐ€ ๊ด€๋ จ ์„œ๋น„์Šค๋‚˜ ์ธ๊ธฐ ํŒ๋งค์ž๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ถ”์ฒœํ•ด์ค„ ์ˆ˜ ์žˆ์–ด์š”. ์ด๋Ÿฐ ๊ธฐ๋Šฅ์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ค๊ณ  ์ „ํ™˜์œจ๋„ ๋†’์—ฌ์ค€๋‹ต๋‹ˆ๋‹ค! ๐ŸŽฏ

WebTransport์™€์˜ ํ†ตํ•ฉ

2025๋…„์—๋Š” WebTransport๋ผ๋Š” ์ƒˆ๋กœ์šด ์›น API๊ฐ€ ๋„๋ฆฌ ์ง€์›๋˜๊ณ  ์žˆ์–ด์š”. WebTransport๋Š” HTTP/3 ๊ธฐ๋ฐ˜์˜ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์„ ์ œ๊ณตํ•˜๋Š”๋ฐ, SSE์˜ ๋‹จ๋ฐฉํ–ฅ ํ•œ๊ณ„๋ฅผ ๋ณด์™„ํ•˜๊ธฐ ์œ„ํ•ด ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜๋Š” ์ถ”์„ธ๋ž๋‹ˆ๋‹ค.


// WebTransport์™€ SSE ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ
// SSE๋กœ ๊ฐ€๋ฒผ์šด ์•Œ๋ฆผ ์ˆ˜์‹ 
const eventSource = new EventSource('/notifications');

eventSource.onmessage = event => {
  const data = JSON.parse(event.data);
  
  // ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ WebTransport๋กœ ์š”์ฒญ
  if (data.type === 'large-data-available') {
    fetchViaWebTransport(data.id);
  } else {
    // ์ผ๋ฐ˜ ์•Œ๋ฆผ ์ฒ˜๋ฆฌ
    showNotification(data);
  }
};

// WebTransport๋กœ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์š”์ฒญ
async function fetchViaWebTransport(dataId) {
  const transport = new WebTransport('https://example.com/webtransport');
  await transport.ready;
  
  const stream = await transport.createBidirectionalStream();
  const writer = stream.writable.getWriter();
  const reader = stream.readable.getReader();
  
  // ๋ฐ์ดํ„ฐ ID ์š”์ฒญ
  const encoder = new TextEncoder();
  const decoder = new TextDecoder();
  
  await writer.write(encoder.encode(JSON.stringify({ id: dataId })));
  
  // ์‘๋‹ต ์ฒ˜๋ฆฌ
  const { value, done } = await reader.read();
  if (!done) {
    const data = JSON.parse(decoder.decode(value));
    processLargeData(data);
  }
  
  await writer.close();
  transport.close();
}
  

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

์ง€๊ธˆ๊นŒ์ง€ Server-Sent Events์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์•Œ์•„๋ดค์–ด์š”! SSE๋Š” ๋‹จ์ˆœํ•˜๋ฉด์„œ๋„ ๊ฐ•๋ ฅํ•œ ์‹ค์‹œ๊ฐ„ ๊ธฐ์ˆ ๋กœ, ํŠนํžˆ ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ์˜ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์— ์ตœ์ ์˜ ์„ ํƒ์ด ๋  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

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

๐Ÿ“Œ ์š”์•ฝ ์ •๋ฆฌ

  1. SSE๋ž€? ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ณด๋‚ด๋Š” ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹  ๊ธฐ์ˆ 
  2. ์žฅ์ : ํ‘œ์ค€ HTTP ์‚ฌ์šฉ, ์ž๋™ ์žฌ์—ฐ๊ฒฐ, ๊ตฌํ˜„ ๊ฐ„๋‹จ, ์ด๋ฒคํŠธ ID ์ง€์›
  3. ์„œ๋ฒ„ ๊ตฌํ˜„: ํŠน์ˆ˜ HTTP ํ—ค๋” ์„ค์ • ๋ฐ 'data: ' ํ˜•์‹์œผ๋กœ ๋ฉ”์‹œ์ง€ ์ „์†ก
  4. ํด๋ผ์ด์–ธํŠธ ๊ตฌํ˜„: EventSource API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ„๋‹จํžˆ ๊ตฌํ˜„ ๊ฐ€๋Šฅ
  5. ํ™œ์šฉ ์‚ฌ๋ก€: ์•Œ๋ฆผ, ์‹ค์‹œ๊ฐ„ ํ”ผ๋“œ, ์ฃผ์‹ ์‹œ์„ธ, ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋“ฑ
  6. ํ•œ๊ณ„์ : ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ , ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ, IE ๋ฏธ์ง€์› ๋“ฑ
  7. ์ตœ์‹  ํŠธ๋ Œ๋“œ: Edge Computing, AI ์ถ”์ฒœ, WebTransport ํ†ตํ•ฉ ๋“ฑ

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

SSE์— ๋Œ€ํ•ด ๋” ๊นŠ์ด ์•Œ๊ณ  ์‹ถ๋‹ค๋ฉด, ๋‹ค์Œ ์ž๋ฃŒ๋“ค์„ ์ฐธ๊ณ ํ•ด๋ณด์„ธ์š”:

  1. MDN Web Docs - Server-sent events
  2. HTML Living Standard - Server-Sent Events
  3. Can I Use - EventSource API ๋ธŒ๋ผ์šฐ์ € ์ง€์› ํ˜„ํ™ฉ
  4. GitHub - EventSource ํด๋ฆฌํ•„
  5. Web.dev - Server-Sent Events ๊ธฐ์ดˆ

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

์งˆ๋ฌธ์ด๋‚˜ ์˜๊ฒฌ์ด ์žˆ์œผ์‹œ๋ฉด ์–ธ์ œ๋“ ์ง€ ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ฃผ์„ธ์š”! ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ๋งŒ๋“ค์–ด๊ฐ€์š”! ๐Ÿ‘จโ€๐Ÿ’ป๐Ÿ‘ฉโ€๐Ÿ’ป

SSE๋กœ ๋‹น์‹ ์˜ ์›น ์•ฑ์„ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜์„ธ์š”! ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ EventSource API ์‚ฌ์šฉ ํ‘œ์ค€ HTTP ํ”„๋กœํ† ์ฝœ ๋ณ„๋„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ถˆํ•„์š” ๋น ๋ฅธ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ ๋›ฐ์–ด๋‚œ ์„ฑ๋Šฅ ๋‹จ์ผ HTTP ์—ฐ๊ฒฐ ์œ ์ง€ ๋‚ฎ์€ ์˜ค๋ฒ„ํ—ค๋“œ ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ ์„œ๋ฒ„ ์ž์› ํšจ์œจ์  ์‚ฌ์šฉ ๋‹ค์–‘ํ•œ ํ™œ์šฉ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ๋ผ์ด๋ธŒ ํ”ผ๋“œ ์—…๋ฐ์ดํŠธ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง AI ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ์ถ”์ฒœ ์ง€๊ธˆ ๋ฐ”๋กœ ์‹œ์ž‘ํ•ด๋ณด์„ธ์š”!
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0