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

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

ํ•ด๋‹น ๊ฐœ๋…์ด ํ•„์š”ํ•œ ์ด์œ 

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

AS-IS: Polling ๋ฐฉ์‹

sequenceDiagram
    autonumber
    participant Client
    participant Server

    loop ์ฃผ๊ธฐ์  ํด๋ง (์˜ˆ: 5์ดˆ๋งˆ๋‹ค)
        Client->>Server: HTTP GET /api/updates
        Server->>Client: 200 OK (๋ฐ์ดํ„ฐ ์—†์Œ)
        Note over Client: 5์ดˆ ๋Œ€๊ธฐ
        Client->>Server: HTTP GET /api/updates
        Server->>Client: 200 OK (๋ฐ์ดํ„ฐ ์—†์Œ)
        Note over Client: 5์ดˆ ๋Œ€๊ธฐ
        Client->>Server: HTTP GET /api/updates
        Server->>Client: 200 OK { data: "new update" }
    end

    Note over Client,Server: ๋ถˆํ•„์š”ํ•œ ์š”์ฒญ์ด ๋งŽ๊ณ  ์‹ค์‹œ๊ฐ„์„ฑ ๋‚ฎ์Œ

TO-BE: SSE ๋ฐฉ์‹

sequenceDiagram
    autonumber
    participant Client
    participant Server

    Client->>Server: HTTP GET /api/events (EventSource ์—ฐ๊ฒฐ)
    Server->>Client: 200 OK (์—ฐ๊ฒฐ ์œ ์ง€)
    Note over Client,Server: ์—ฐ๊ฒฐ์ด ์—ด๋ฆฐ ์ƒํƒœ๋กœ ์œ ์ง€๋จ

    Note over Server: ์ด๋ฒคํŠธ ๋ฐœ์ƒ (๋ฐ์ดํ„ฐ ์ค€๋น„๋จ)
    Server->>Client: data: {"message": "update 1"}

    Note over Server: ๋˜ ๋‹ค๋ฅธ ์ด๋ฒคํŠธ ๋ฐœ์ƒ
    Server->>Client: data: {"message": "update 2"}

    Note over Server: ์ถ”๊ฐ€ ์ด๋ฒคํŠธ
    Server->>Client: data: {"message": "update 3"}

    Note over Client,Server: ํ•˜๋‚˜์˜ ์—ฐ๊ฒฐ๋กœ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ์ˆ˜์‹ 

SSE vs WebSocket ๋น„๊ต

ํŠน์ง•SSEWebSocket
ํ†ต์‹  ๋ฐฉํ–ฅ๋‹จ๋ฐฉํ–ฅ (์„œ๋ฒ„ โ†’ ํด๋ผ์ด์–ธํŠธ)์–‘๋ฐฉํ–ฅ (์„œ๋ฒ„ โ†” ํด๋ผ์ด์–ธํŠธ)
ํ”„๋กœํ† ์ฝœHTTP/HTTPSWebSocket ํ”„๋กœํ† ์ฝœ (ws://, wss://)
์žฌ์—ฐ๊ฒฐ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™ ์ฒ˜๋ฆฌ์ˆ˜๋™์œผ๋กœ ๊ตฌํ˜„ ํ•„์š”
๊ตฌํ˜„ ๋ณต์žก๋„๊ฐ„๋‹จ (EventSource API)์ƒ๋Œ€์ ์œผ๋กœ ๋ณต์žก
์˜ค๋ฒ„ํ—ค๋“œ๋‚ฎ์Œ (๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ์— ์ตœ์ ํ™”)๋†’์Œ (์–‘๋ฐฉํ–ฅ ํ†ต์‹  ์ง€์›)
๋ฐฉํ™”๋ฒฝ ํ†ต๊ณผ์šฉ์ด (HTTP ๊ธฐ๋ฐ˜)์ œํ•œ์  (์ผ๋ถ€ ํ”„๋ก์‹œ์—์„œ ์ฐจ๋‹จ)
์‚ฌ์šฉ ์‚ฌ๋ก€์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ, ํ”ผ๋“œ, ๋Œ€์‹œ๋ณด๋“œ, ์ฃผ์‹ ์‹œ์„ธ์ฑ„ํŒ…, ๊ฒŒ์ž„, ํ˜‘์—… ๋„๊ตฌ ๋“ฑ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ 

์–ธ์ œ SSE๋ฅผ ์‚ฌ์šฉํ• ๊นŒ?

  • โœ… ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ๋งŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด๋ฉด ๋˜๋Š” ๊ฒฝ์šฐ
  • โœ… ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•˜์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ž์ฃผ ๋ณด๋‚ผ ํ•„์š”๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ
  • โœ… ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ๊ณผ ๋ฐฉํ™”๋ฒฝ ํ†ต๊ณผ๊ฐ€ ์ค‘์š”ํ•œ ๊ฒฝ์šฐ
  • โœ… ๊ตฌํ˜„์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ์œ ์ง€ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ

์–ธ์ œ WebSocket์„ ์‚ฌ์šฉํ• ๊นŒ?

  • โœ… ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„ ์–‘๋ฐฉํ–ฅ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ (์ฑ„ํŒ…, ๊ฒŒ์ž„)
  • โœ… ๋‚ฎ์€ ์ง€์—ฐ ์‹œ๊ฐ„(latency)์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ
  • โœ… ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„๋กœ ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ

EventSource API ์‚ฌ์šฉ๋ฒ•

๊ธฐ๋ณธ ์—ฐ๊ฒฐ ๋ฐ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 

// SSE ์—ฐ๊ฒฐ ์ƒ์„ฑ
const eventSource = new EventSource('/api/events');
 
// ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
eventSource.onmessage = function(event) {
  console.log('์ƒˆ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ :', event.data);
 
  // JSON ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ
  const data = JSON.parse(event.data);
  updateUI(data);
};
 
// ์—ฐ๊ฒฐ ์„ฑ๊ณต
eventSource.onopen = function() {
  console.log('SSE ์—ฐ๊ฒฐ ์„ฑ๊ณต');
};
 
// ์—๋Ÿฌ ์ฒ˜๋ฆฌ
eventSource.onerror = function(error) {
  console.error('SSE ์—ฐ๊ฒฐ ์—๋Ÿฌ:', error);
  // ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„
};
 
// ์—ฐ๊ฒฐ ์ข…๋ฃŒ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ)
// eventSource.close();

์ปค์Šคํ…€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ

const eventSource = new EventSource('/api/events');
 
// ํŠน์ • ์ด๋ฒคํŠธ ํƒ€์ž… ๋ฆฌ์Šค๋‹
eventSource.addEventListener('notification', function(event) {
  const notification = JSON.parse(event.data);
  showNotification(notification);
});
 
eventSource.addEventListener('stock-update', function(event) {
  const stockData = JSON.parse(event.data);
  updateStockPrice(stockData);
});

์„œ๋ฒ„ ์ธก ๊ตฌํ˜„ ์˜ˆ์‹œ (Node.js/Express)

app.get('/api/events', (req, res) => {
  // SSE ํ—ค๋” ์„ค์ •
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
 
  // ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฉ”์‹œ์ง€ ์ „์†ก
  const sendEvent = (data) => {
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };
 
  // ์ดˆ๊ธฐ ๋ฉ”์‹œ์ง€
  sendEvent({ message: 'Connected to SSE' });
 
  // ์ฃผ๊ธฐ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ ์ „์†ก
  const intervalId = setInterval(() => {
    sendEvent({
      timestamp: Date.now(),
      message: 'Server update'
    });
  }, 5000);
 
  // ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์‹œ ์ •๋ฆฌ
  req.on('close', () => {
    clearInterval(intervalId);
    console.log('Client disconnected');
  });
});

์ปค์Šคํ…€ ์ด๋ฒคํŠธ ํƒ€์ž… ์ „์†ก (์„œ๋ฒ„)

// ์ด๋ฒคํŠธ ํƒ€์ž… ์ง€์ •
res.write(`event: notification\n`);
res.write(`data: ${JSON.stringify({ title: 'New alert', body: 'Something happened' })}\n\n`);
 
res.write(`event: stock-update\n`);
res.write(`data: ${JSON.stringify({ symbol: 'AAPL', price: 150.23 })}\n\n`);

SSE์˜ ์ฃผ์š” ํŠน์ง• 4๊ฐ€์ง€

1. HTTP ๊ธฐ๋ฐ˜ ํ”„๋กœํ† ์ฝœ

SSE๋Š” ์ผ๋ฐ˜์ ์ธ HTTP ์—ฐ๊ฒฐ ์œ„์—์„œ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๋ณ„๋„์˜ ํ”„๋กœํ† ์ฝœ ์—…๊ทธ๋ ˆ์ด๋“œ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ๊ธฐ์กด HTTP ์ธํ”„๋ผ(ํ”„๋ก์‹œ, ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ, ๋ฐฉํ™”๋ฒฝ)์™€ ํ˜ธํ™˜๋ฉ๋‹ˆ๋‹ค.

GET /api/events HTTP/1.1
Host: example.com
Accept: text/event-stream
 
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
 
data: {"message": "Hello"}
 
data: {"message": "World"}

2. ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๋ฉ”์ปค๋‹ˆ์ฆ˜

๋ธŒ๋ผ์šฐ์ €์˜ EventSource ๊ตฌํ˜„์ฒด๋Š” ์—ฐ๊ฒฐ์ด ๋Š์–ด์ง€๋ฉด ์ž๋™์œผ๋กœ ์žฌ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ณ„๋„์˜ ์žฌ์—ฐ๊ฒฐ ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

// ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ ์žฌ์—ฐ๊ฒฐ ์ฒ˜๋ฆฌ
eventSource.onerror = function(error) {
  // ์—๋Ÿฌ ๋กœ๊น…๋งŒ ํ•˜๋ฉด ๋จ
  console.log('์—ฐ๊ฒฐ ๋Š๊น€, ์ž๋™ ์žฌ์—ฐ๊ฒฐ ์ค‘...');
  // ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์•Œ์•„์„œ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„
};

์„œ๋ฒ„ ์ธก์—์„œ ์žฌ์—ฐ๊ฒฐ ์‹œ๊ฐ„์„ ์ œ์–ดํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค:

// ์„œ๋ฒ„: ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ 3์ดˆ ํ›„ ์žฌ์—ฐ๊ฒฐํ•˜๋ผ๊ณ  ์ง€์‹œ
res.write(`retry: 3000\n`);
res.write(`data: ${JSON.stringify({ message: 'retry after 3s' })}\n\n`);

3. ์ด๋ฒคํŠธ ID ๋ฐ ๋งˆ์ง€๋ง‰ ์ด๋ฒคํŠธ ์ถ”์ 

SSE๋Š” ๊ฐ ๋ฉ”์‹œ์ง€์— ๊ณ ์œ  ID๋ฅผ ๋ถ€์—ฌํ•˜์—ฌ, ์—ฐ๊ฒฐ์ด ๋Š๊ฒผ๋‹ค๊ฐ€ ์žฌ์—ฐ๊ฒฐ๋  ๋•Œ ๋งˆ์ง€๋ง‰์œผ๋กœ ๋ฐ›์€ ์ด๋ฒคํŠธ ์ดํ›„๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„œ๋ฒ„ ์ธก:

let messageId = 0;
 
res.write(`id: ${messageId}\n`);
res.write(`data: ${JSON.stringify({ content: 'message' })}\n\n`);
messageId++;

ํด๋ผ์ด์–ธํŠธ ์žฌ์—ฐ๊ฒฐ ์‹œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ Last-Event-ID ํ—ค๋”๋ฅผ ์ „์†ก:

GET /api/events HTTP/1.1
Last-Event-ID: 42

์„œ๋ฒ„๋Š” ์ด๋ฅผ ํ™•์ธํ•˜๊ณ  42๋ฒˆ ์ดํ›„์˜ ์ด๋ฒคํŠธ๋งŒ ์ „์†กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4. Web Workers ์ง€์›

SSE๋Š” Web Workers์—์„œ๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜์—ฌ, ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋ฅผ ์ฐจ๋‹จํ•˜์ง€ ์•Š๊ณ  ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// worker.js
const eventSource = new EventSource('/api/events');
 
eventSource.onmessage = function(event) {
  // ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋กœ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ
  self.postMessage({
    type: 'sse-update',
    data: event.data
  });
};

์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€

1. ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์‹œ์Šคํ…œ

// ์‚ฌ์šฉ์ž์—๊ฒŒ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ํ‘ธ์‹œ
const eventSource = new EventSource('/api/notifications');
 
eventSource.addEventListener('notification', (event) => {
  const notification = JSON.parse(event.data);
  showToast(notification.title, notification.message);
});

2. ๋ผ์ด๋ธŒ ๋Œ€์‹œ๋ณด๋“œ

// ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง ๋Œ€์‹œ๋ณด๋“œ
const eventSource = new EventSource('/api/metrics');
 
eventSource.onmessage = (event) => {
  const metrics = JSON.parse(event.data);
  updateChart(metrics.cpu, metrics.memory, metrics.requests);
};

3. ์†Œ์…œ ๋ฏธ๋””์–ด ํ”ผ๋“œ

// ์ƒˆ ํฌ์ŠคํŠธ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
const eventSource = new EventSource('/api/feed');
 
eventSource.addEventListener('new-post', (event) => {
  const post = JSON.parse(event.data);
  prependPostToFeed(post);
});

4. ์ฃผ์‹ ์‹œ์„ธ ์—…๋ฐ์ดํŠธ

// ์‹ค์‹œ๊ฐ„ ์ฃผ์‹ ๊ฐ€๊ฒฉ
const eventSource = new EventSource('/api/stocks?symbols=AAPL,GOOGL');
 
eventSource.addEventListener('price-update', (event) => {
  const { symbol, price, change } = JSON.parse(event.data);
  updateStockTicker(symbol, price, change);
});

์ฐธ๊ณ  ๋ฌธ์„œ