๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
  • What would life be If we had no courage to attemp anything?
๐“๐จ๐๐š๐ฒ ๐ˆ ๐‹๐ž๐š๐ซ๐ง

์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ๊ตฌํ˜„ ํ•˜๊ธฐ : 1. SSE(Server Sent Events)๋Š” ๋ฌด์—‡์ธ๊ฐ€?

by DevIseo 2025. 12. 1.

SSE(Server Sent Events)

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


๋™์ž‘ ํ๋ฆ„

์™ผ : ๊ธฐ์กด HTTP, ์˜ค : SSE

  1. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์—๊ฒŒ SSE๋กœ ํ†ต์‹ ํ•˜์ž๋Š” ์š”์ฒญ์„ ๋ณด๋ƒ„. (Connection Request)
  2. ์„œ๋ฒ„๋Š” ์ด ์š”์ฒญ์„ ์ˆ˜์‹ ํ•˜๊ณ  ์ˆ˜๋ฝํ–ˆ์Œ์„ ์•Œ๋ฆฌ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋ƒ„. (Connection Response)
  3. ํด๋ผ์ด์–ธํŠธ๋Š” ์ด๋ฅผ ๋ฐ›๊ณ , ์ง€๊ธˆ๋ถ€ํ„ฐ ์„œ๋ฒ„๊ฐ€ ๋ณด๋‚ด์ฃผ๋Š” ๋ฐ์ดํ„ฐ๋“ค์— ๋ฐ˜์‘ํ•  ์ค€๋น„
  4. ์ด์ œ ์„œ๋ฒ„๋Š” ์ •ํ•ด์ง„ ์ด๋ฒคํŠธ๊ฐ€ ์žˆ์„ ๋•Œ๋งˆ๋‹ค ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋ƒ„. (Event Response)
    ์ด๋•Œ, ํด๋ผ์ด์–ธํŠธ๋Š” ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ์ด๋ฏ€๋กœ ์ด์— ์‘๋‹ตํ•  ์ˆ˜๋Š” ์—†์Œ!
  5. ํด๋ผ์ด์–ธํŠธ๋Š” ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฉ”์‹œ์ง€๊ฐ€ ๋„์ฐฉํ•  ๋•Œ๋งˆ๋‹ค ์ด์— ๋ฐ˜์‘ํ•˜์—ฌ ํ™”๋ฉด์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋“ฑ ํ•„์š”ํ•œ ์ž‘์—….
  6. ์ด ๊ณผ์ •๋“ค์„ ํ•˜๋‚˜์˜ ์—ฐ๊ฒฐ ์•ˆ์—์„œ ๊ณ„์† ์ด๋ค„์ง€๊ณ , ๋งŒ์•ฝ ์—ฐ๊ฒฐ์ด ๋Š๊ธฐ๋ฉด ํด๋ผ์ด์–ธํŠธ๋Š” ์ž๋™์œผ๋กœ ์žฌ์—ฐ๊ฒฐ์„ ์š”์ฒญํ•˜์—ฌ ํ†ต์‹ ์„ ์žฌ๊ฐœ.
  7. ํ•„์š”ํ•œ ์ž‘์—…์„ ๋งˆ์น˜๋ฉด ํด๋ผ์ด์–ธํŠธ ๋˜๋Š” ์„œ๋ฒ„์—์„œ ์ƒ๋Œ€๋ฐฉ์—๊ฒŒ ์ข…๋ฃŒ๋ฅผ ํ†ต๋ณดํ•˜๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋ƒ„์œผ๋กœ์จ ์—ฐ๊ฒฐ์ด ๋.(Connection Closed)

๋ฉ”์‹œ์ง€ ๊ตฌ์กฐ

content-type : text/event-stream

id: 123
event: message
data: Hello World
data: This is another message line

๊ฐ ๋ฉ”์‹œ์ง€๋Š” ์—ฌ๋Ÿฌ ํ•„๋“œ๋กœ ๊ตฌ์„ฑ๋˜๋ฉฐ, ๊ฐ ํ•„๋“œ๋Š” ์ค„๋ฐ”๊ฟˆ(\\n)์œผ๋กœ ๊ตฌ๋ถ„๋˜๊ณ , ๋ฉ”์‹œ์ง€์˜ ๋์€ ๋‘ ์ค„๋ฐ”๊ฟˆ(\\n\\n)์œผ๋กœ ํ‘œ์‹œ.

 

1. Data

  • ์—ญํ• : ์‹ค์ œ ์ „์†กํ•  ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด๋Š” ํ•„๋“œ
  • ํŠน์ง•:
    • ํ•œ ๋ฉ”์‹œ์ง€์— ์—ฌ๋Ÿฌ data: ํ•„๋“œ๋ฅผ ํฌํ•จ
    • ๊ฐ data: ๋ผ์ธ์€ ์ค„๋ฐ”๊ฟˆ(\\n)์œผ๋กœ ๊ตฌ๋ถ„
    • ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ˆ˜์‹  ์‹œ, ์—ฌ๋Ÿฌ data: ์ค„์€ ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด๋กœ ํ•ฉ์ณ์ ธ ์ „๋‹ฌ
  • ์˜ˆ์‹œ:
data: Hello data: World

→ ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” "Hello\\nWorld"๋กœ ์ˆ˜์‹ 

 

2. Event

  • ์—ญํ• : ๊ธฐ๋ณธ message ์™ธ์— ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ํƒ€์ž…์„ ์ •์˜
  • ํŠน์ง•:
    • event: ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ์ด๋ฒคํŠธ ์ด๋ฆ„์„ ์ง€์ •ํ•˜๋ฉด,
    • ํด๋ผ์ด์–ธํŠธ์—์„œ addEventListener('customEventName', ...) ํ˜•์‹์œผ๋กœ ์ˆ˜์‹ ๊ฐ€๋Šฅ
  • ์˜ˆ์‹œ:
event: update data: New data available

→ ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” eventSource.addEventListener('update', handler)๋กœ ์ฒ˜๋ฆฌ

 

3. ID

  • ์—ญํ• : ๋ฉ”์‹œ์ง€์˜ ๊ณ ์œ  ID๋ฅผ ์ง€์ •
  • ์šฉ๋„:
    • ์žฌ์—ฐ๊ฒฐ ์‹œ์  ๊ด€๋ฆฌ์— ์‚ฌ์šฉ
    • ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋Š๊ธด ํ›„ ๋‹ค์‹œ ์—ฐ๊ฒฐํ•  ๋•Œ, ๋งˆ์ง€๋ง‰์œผ๋กœ ๋ฐ›์€ id ๊ฐ’์„ Last-Event-ID ํ—ค๋”์— ํฌํ•จํ•˜์—ฌ ์ „์†กํ•˜๋ฏ€๋กœ, ์„œ๋ฒ„๋Š” ์ค‘๋‹จ๋œ ์‹œ์  ์ดํ›„์˜ ๋ฐ์ดํ„ฐ๋งŒ ๋‹ค์‹œ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Œ.
  • ์˜ˆ์‹œ:
  • id: 123 data: New message

4. ๋ฉ”์‹œ์ง€ ์ข…๋ฃŒ

  • ๊ฐ ์ด๋ฒคํŠธ ๋ฉ”์‹œ์ง€๋Š” ๋ฐ˜๋“œ์‹œ ๋‘ ์ค„๋ฐ”๊ฟˆ(\\n\\n)์œผ๋กœ ๋๋‚˜์•ผ ํ•จ.
  • ์ด ๊ตฌ๋ถ„์ž๊ฐ€ ์—†์œผ๋ฉด ๋ธŒ๋ผ์šฐ์ €๋Š” ๋ฉ”์‹œ์ง€๋ฅผ “์•„์ง ๋๋‚˜์ง€ ์•Š์€ ์ƒํƒœ”๋กœ ๊ฐ„์ฃผ

โœ… ์ข…ํ•ฉ ์˜ˆ์‹œ

id: 42
event: update
data: {"status": "ok", "message": "Data refreshed"}
\\n

→ ํด๋ผ์ด์–ธํŠธ ์ธก ๋™์ž‘ ์˜ˆ์‹œ

const eventSource = new EventSource('/events');

eventSource.addEventListener('update', (e) => {
  console.log('Received event:', e.data);
  // {"status": "ok", "message": "Data refreshed"}
});


์ฆ‰, SSE ๋ฉ”์‹œ์ง€๋Š” id, event, data ํ•„๋“œ๋ฅผ ์กฐํ•ฉํ•œ ๋‹จ์ˆœํ•œ ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ํ”„๋กœํ† ์ฝœ์ด๋ฉฐ, ์ค„๋ฐ”๊ฟˆ๊ณผ ๋ฉ”์‹œ์ง€ ๊ตฌ๋ถ„ ๊ทœ์น™์„ ํ†ตํ•ด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์‹ค์‹œ๊ฐ„ ์ด๋ฒคํŠธ๋ฅผ ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋จ.

 

 

 

์•Œ์•„์•ผ ํ•  ๊ฐœ๋…

  • Event ID ๋ฐ Last-Event-ID ํ—ค๋”
    • ์ด๋ฒคํŠธ์˜ ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด event ID๋ฅผ ์‚ฌ์šฉ๊ฐ€๋Šฅ
    • ํด๋ผ์ด์–ธํŠธ๋Š” Last-Event-ID ํ—ค๋”๋ฅผ ํ†ตํ•ด ๋งˆ์ง€๋ง‰์œผ๋กœ ์ˆ˜์‹ ํ•œ ์ด๋ฒคํŠธ ID๋ฅผ ์„œ๋ฒ„์— ์ „๋‹ฌํ•˜์—ฌ, ์—ฐ๊ฒฐ์ด ์žฌ์„ค์ •๋œ ๊ฒฝ์šฐ์—๋„ ๋ฐ์ดํ„ฐ ์†์‹ค ์—†์ด ์ด์–ด์„œ ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ
  • Custom Event Types
    • ๊ธฐ๋ณธ ์ด๋ฒคํŠธ ํƒ€์ž… ์™ธ์—๋„, ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ํƒ€์ž… ์ •์˜ ๊ฐ€๋Šฅ
    • ์„œ๋ฒ„์—์„œ ํŠน์ • ์ด๋ฒคํŠธ ํƒ€์ž…์„ ์„ค์ •ํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ํ•ด๋‹น ํƒ€์ž…์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์„ค์ •ํ•˜์—ฌ ๋‹ค์–‘ํ•œ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌ
    • ๋ฉ”์‹œ์ง€์— ์ด๋ฒคํŠธ ์ด๋ฆ„์ด ์ง€์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ onmessage ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ
  • Retry Mechanism
    • SSE๋Š” ์ž๋™์œผ๋กœ ์žฌ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•˜๋ฉฐ, ๊ธฐ๋ณธ ์žฌ์—ฐ๊ฒฐ ๊ฐ„๊ฒฉ์€ 3์ดˆ
    • ์ •์ˆ˜ ๊ฐ’์ด ์•„๋‹Œ ๊ฒฝ์šฐ retry ํ•„๋”๋Š” ๋ฌด์‹œ๋จ
    • ์„œ๋ฒ„๋Š” retry ํ•„๋“œ๋ฅผ ํ†ตํ•ด ํด๋ผ์ด์–ธํŠธ์˜ ์žฌ์—ฐ๊ฒฐ ๊ฐ„๊ฒฉ์„ ์กฐ์ •
    • res.write(`retry: 5000\\n`); // 5์ดˆ ํ›„ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„
  • ๋ธŒ๋ผ์šฐ์ € ์ง€์› ๋ฐ ํด๋ฆฌํ•„
    • ๋Œ€๋ถ€๋ถ„์˜ ํ˜„๋Œ€์ ์ธ ๋ธŒ๋ผ์šฐ์ €๋Š” SSE๋ฅผ ์ง€์›ํ•˜์ง€๋งŒ, Internet Explorer์™€ ๊ฐ™์€ ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์ง€์›๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ
    • ์ด๋Ÿฐ ๊ฒฝ์šฐ ํด๋ฆฌํ•„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ˜ธํ™˜์„ฑ์„ ๋ณด์žฅ

ํ•œ๊ณ„์ 

  1. Open connection ์ œํ•œ๊ณผ HTTP/2
    • ํ•œ ๋ธŒ๋ผ์šฐ์ € domain ๋‹น open connection ์ œํ•œ
    • ์ฃผ์˜: HTTP/2 ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ๋•Œ SSE๋Š” ํ™œ์„ฑํ™”๋œ ์—ฐ๊ฒฐ์˜ ์ตœ๋Œ€ ๊ฐœ์ˆ˜ ์ œํ•œ์œผ๋กœ ์ธํ•œ ํ•œ๊ณ„๋ฅผ ๊ฒช์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด ์ œํ•œ์€ ๋ธŒ๋ผ์šฐ์ €๋‹น ์ ์šฉ๋  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋งค์šฐ ๋‚ฎ์€ ์ˆ˜(6)๋กœ ์„ค์ •๋˜์–ด ์žˆ์–ด ํŠนํžˆ ์—ฌ๋Ÿฌ ํƒญ์„ ์—ด ๋•Œ ๋ฌธ์ œ๋ฅผ ๊ฒช์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฌธ์ œ๋Š” Chrome๊ณผ Firefox์—์„œ ์ˆ˜์ •๋˜์ง€ ์•Š์„ ๊ฒƒ์œผ๋กœ ํ‘œ์‹œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ์ œํ•œ์€ ๋ธŒ๋ผ์šฐ์ €์™€ ๋„๋ฉ”์ธ๋ณ„๋กœ ์ ์šฉ๋˜๋ฏ€๋กœ www.example1.com ์— ๋Œ€ํ•ด ๋ชจ๋“  ํƒญ์—์„œ 6๊ฐœ์˜ SSE ์—ฐ๊ฒฐ์„ ์—ด ์ˆ˜ ์žˆ๊ณ , www.example2.com ์— ๋Œ€ํ•ด์„œ๋„ 6๊ฐœ์˜ SSE ์—ฐ๊ฒฐ์„ ์—ด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์ถœ์ฒ˜: Stackoverflow). HTTP/2๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋™์‹œ์— ์—ด ์ˆ˜ ์žˆ๋Š” HTTP ์ŠคํŠธ๋ฆผ์˜ ์ตœ๋Œ€ ๊ฐœ์ˆ˜๊ฐ€ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ๊ฐ„์— ํ˜‘์ƒ๋˜๋ฉฐ, ๊ธฐ๋ณธ๊ฐ’์€ 100์ž…๋‹ˆ๋‹ค.
  2. EventSource์˜ ํ•œ๊ณ„์™€ polyffils
  • Web API๋กœ ์ฃผ์–ด์ง€๋Š” EventSource๋Š” ํŽธ๋ฆฌํ•˜์ง€๋งŒ, GET ์š”์ฒญ ๋ฐ–์— ์•ˆ๋˜๋Š” ์ header ์ˆ˜์ •์ด ์•ˆ๋˜๋Š” ์  ๋“ฑ์˜ ํ•œ๊ณ„
  • polyfill ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด ๊ทน๋ณต ๊ฐ€๋Šฅ

 

๊ธฐ๋ณธ ๊ตฌ์„ฑ

ํด๋ผ์ด์–ธํŠธ - ํ”„๋ก ํŠธ ๊ตฌ์„ฑ

  • ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก ๋ฐฉ์‹ - addEventListner ํ™œ์šฉ ์‹œ
// ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌ ๋ฐ›๊ธฐ ์œ„ํ•ด์„œ ์„œ๋ฒ„๋กœ ์ ‘์†์„ ์‹œ์ž‘ํ•˜๋ ค๋ฉด ์šฐ์„ , 
// ์ด๋ฒคํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์„œ๋ฒ„์ธก ์Šคํฌ๋ฆฝํŠธ๋ฅผ URI๋กœ ์ง€์ •ํ•˜์—ฌ ์ƒˆ๋กœ์šด EventSource ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค
// EventSource ์ƒ์„ฑ์ž์— ์ „๋‹ฌ ๋œ URL์ด ์ ˆ๋Œ€ URL ์ธ ๊ฒฝ์šฐ 
// ํ•ด๋‹น ์ถœ์ฒ˜ (scheme, domain, port)๊ฐ€ ํ˜ธ์ถœ ํŽ˜์ด์ง€์˜ ์ถœ์ฒ˜์™€ ์ผ์น˜ํ•ด์•ผ ํ•œ๋‹ค.
const eventSource = new EventSource(`/subscribe`); // sse์ฝ”๋“œ๊ฐ€ ์žˆ๋Š” ์„œ๋ฒ„๋‹จ ํŒŒ์ผ

// ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์˜ค๋ฉด
eventSource.addEventListener('message', function(e) {
  console.log(e.data);
});

// connection๋˜๋ฉด
eventSource.addEventListener('open', function(e) {
  // Connection was opened.
});

// error ๋‚˜๋ฉด
eventSource.addEventListener('error', function(e) {
  if (e.readyState == EventSource.CLOSED) {
    // Connection was closed.
  }
});
  • ํ”„๋กœํผํ‹ฐ ํ•ธ๋“ค๋Ÿฌ ๋ฐฉ์‹ - on
const eventSource = new EventSource('http://localhost:3000/events');

eventSource.onopen = () => {
  console.log('SSE ์—ฐ๊ฒฐ ์„ฑ๊ณต');
  setError(null);
};

eventSource.onmessage = (event: MessageEvent) => {
  const eventData = JSON.parse(event.data);
  const newNotification: Notification = {
    id: Date.now(),
    message: eventData.message,
  };
  
  setMessages((prevMessages) => [...prevMessages, newNotification]);
};

eventSource.onerror = (err) => {
  console.error('SSE ์—๋Ÿฌ:', err);
  setError('์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์—ฐ๊ฒฐ์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
	
	
	// ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ ๋‹ซ๊ธฐ
  eventSource.close();
};

 

 

 

โ“eventSource์˜ addEventListner vs on (ํ”„๋กœํผํ‹ฐ ํ•ธ๋“ค๋Ÿฌ) ๋ฐฉ์‹์€ ์–ด๋–ค ์ฐจ์ด๋ฅผ ๊ฐ€์งˆ๊นŒ?

  • ํ”„๋กœํผํ‹ฐ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๊ณผ addEventListner๋ฅผ ํ™œ์šฉํ•œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก ๋ฐฉ์‹์€ ์ž‘๋™์€ ๊ฐ™๋‹ค. ํ•˜์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” addEventListner๊ฐ€ ํ›จ์”ฌ ๋” ์œ ์—ฐํ•˜๊ณ  ํ™•์žฅ์„ฑ ์žˆ๋Š” ๋ฐฉ๋ฒ•
  • onmessage / onopen / onerror → ๊ธฐ๋ณธ ์ด๋ฒคํŠธ 3์ข…๋งŒ ์ง€์›
  • addEventListener() → ์ปค์Šคํ…€ ์ด๋ฒคํŠธ๊นŒ์ง€ ํฌํ•จํ•ด ๋ชจ๋“  ์ด๋ฒคํŠธ ์ˆ˜์‹  ๊ฐ€๋Šฅ
  • ์ฃผ์š” ์ฐจ์ด์  ์š”์•ฝ 
    ๊ตฌ๋ถ„ onmessage / onopen / onerror  addEventListener()
    ์ง€์› ์ด๋ฒคํŠธ 'open', 'message', 'error' 'open', 'message', 'error', ์ปค์Šคํ…€ ์ด๋ฒคํŠธ
    ์ค‘๋ณต ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก โŒ ๋ถˆ๊ฐ€๋Šฅ (๋งˆ์ง€๋ง‰ ํ• ๋‹น๋งŒ ์œ ํšจ) โœ… ๊ฐ€๋Šฅ (์—ฌ๋Ÿฌ ๊ฐœ ๋“ฑ๋ก ๊ฐ€๋Šฅ)
    ์ด๋ฒคํŠธ ์ œ๊ฑฐ โŒ ์ง์ ‘ ์ œ๊ฑฐ ๋ถˆ๊ฐ€๋Šฅ โœ… removeEventListener()๋กœ ์ œ๊ฑฐ ๊ฐ€๋Šฅ
    ์ฝ”๋“œ ๊ตฌ์กฐ ๊ฐ„๋‹จ ๋” ์œ ์—ฐ, ํ™•์žฅ์„ฑ ๋†’์Œ
    ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ์ˆ˜์‹  โŒ ๋ถˆ๊ฐ€๋Šฅ โœ… ๊ฐ€๋Šฅ (event: customName)
  • ์ƒํ™ฉ ๋ณ„ ์ถ”์ฒœ ๋ฐฉ์‹ 
    ์ƒํ™ฉ ์ถ”์ฒœ ๋ฐฉ์‹
    ๋‹จ์ˆœํ•œ ์—ฐ๊ฒฐ / ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋งŒ ํ•„์š”ํ•  ๋•Œ onmessage, onopen, onerror
    ์ปค์Šคํ…€ ์ด๋ฒคํŠธ(event: ํ•„๋“œ) ์‚ฌ์šฉ / ์—ฌ๋Ÿฌ ๋ฆฌ์Šค๋„ˆ ํ•„์š” / ์œ ์ง€๋ณด์ˆ˜ ์ค‘์š” addEventListener() ๋ฐฉ์‹ ๊ถŒ์žฅ

 

์„œ๋ฒ„ - ๋ฐฑ์—”๋“œ(node.js) ๊ตฌ์„ฑ

import express from 'express';
import cors from 'cors';

const app = express();

const FRONTEND_URL = '<http://localhost:5173>';

const corsOptions = {
  origin: FRONTEND_URL,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
	// CORS ์š”์ฒญ ์‹œ ์ธ์ฆ์ •๋ณด(์ฟ ํ‚ค, ์„ธ์…˜, Authorization ํ—ค๋” ๋“ฑ) ํฌํ•จ ์—ฌ๋ถ€ ์„ค์ •
	// false(๊ธฐ๋ณธ๊ฐ’): ๊ต์ฐจ ์ถœ์ฒ˜ ์š”์ฒญ ์‹œ ์ฟ ํ‚ค ๋“ฑ ์ธ์ฆ์ •๋ณด๋ฅผ ์ „์†กํ•˜์ง€ ์•Š์Œ
	// true: ์„œ๋ฒ„์—์„œ Access-Control-Allow-Credentials: true ์‘๋‹ต ์‹œ ์ธ์ฆ์ •๋ณด ํฌํ•จ ๊ฐ€๋Šฅ
	// ์ฃผ์˜: credentials: true ์‚ฌ์šฉ ์‹œ origin์„ ๋ฐ˜๋“œ์‹œ ๋ช…์‹œํ•ด์•ผ ํ•˜๋ฉฐ, '*'์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ
  **credentials: true,**
};

app.use(cors(corsOptions));

const port = 5000;
app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});

app.get('/events', (req, res) => {
	**// Content-Type์„ 'text/event-stream'์œผ๋กœ ์ „์†กํ•ด์•ผ SSE ๋ฉ”์‹œ์ง€ ์ „์†ก ๊ฐ€๋Šฅ"
  res.setHeader('Content-Type', 'text/event-stream');**
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  setInterval(()=>{
    const data = { message: `5์ดˆ์— 1๋ฒˆ์”ฉ ๋ณด๋‚ด๋Š” ๋ฉ”์‹œ์ง€ : ${new Date().toISOString()}` };
    res.write(`data: ${JSON.stringify(data)}\\n\\n`);
  },5000)
});
  • Content-Type text/event-stream
    • ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” MIME ํƒ€์ž…
  • Cache-Control no-cache
    • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•ญ์ƒ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ์‘๋‹ต์„ ์บ์‹ฑํ•˜์ง€ ์•Š๋„๋ก ์„ค์ •
  • Connection keep-alive
    • HTTP์—ฐ๊ฒฐ ์„ธ์…˜์„ ์ง€์†์ ์œผ๋กœ ์œ ์ง€ ์‹œํ‚ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ

์ปค์Šคํ…€ ์ด๋ฒคํŠธ ํƒ€์ž… ์„ค์ •

// ์„œ๋ฒ„์—์„œ ์ด๋ฒคํŠธ ํƒ€์ž… ์„ค์ •
res.write(`event: customEvent\\n`);
res.write(`data: ${JSON.stringify({ message: 'Custom Event Data' })}\\n\\n`);
  • ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” addEventListner๋ฅผ ํ†ตํ•ด์„œ ์ด๋ฒคํŠธ ํƒ€์ž…์„ ์„ค์ •ํ•œ๋‹ค.

 

 

Reference

https://sanghee01.tistory.com/186

https://developer.mozilla.org/ko/docs/Web/API/Server-sent_events/Using_server-sent_events

https://techblog.woowahan.com/23199/

https://phsun102.tistory.com/202

https://velog.io/@blessoms2017/Event-Source-Polyfill๋กœ-๋ชจ๋“ -๋ธŒ๋ผ์šฐ์ €์—์„œ-SSE-๊ตฌํ˜„ํ•˜๊ธฐ

https://itsfuad.medium.com/understanding-server-sent-events-sse-with-node-js-3e881c533081

https://velog.io/@overslept/React์—์„œ-์•ˆ์ „ํ•œ-SSEServer-Sent-Events-์—ฐ๊ฒฐ-๊ด€๋ฆฌํ•˜๊ธฐ

https://cocococo331.tistory.com/76

https://boseong.dev/post/2025-09-05-sse-deep-dive

https://github.com/Azure/fetch-event-source?tab=readme-ov-file

๋Œ“๊ธ€