EventSource๋Š” Server-Sent Events(SSE)๋ฅผ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. HTTP ์„œ๋ฒ„์™€์˜ **์ง€์†์ ์ธ ์—ฐ๊ฒฐ(persistent connection)**์„ ์—ด๊ณ , ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ text/event-stream ํฌ๋งท์œผ๋กœ ์ „์†ก๋˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค.

EventSource๋Š” ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ (์„œ๋ฒ„ โ†’ ํด๋ผ์ด์–ธํŠธ)์„ ์œ„ํ•ด ์„ค๊ณ„๋˜์—ˆ์œผ๋ฉฐ, ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•œ ์†Œ์…œ ๋ฏธ๋””์–ด ํ”ผ๋“œ, ๋‰ด์Šค ์—…๋ฐ์ดํŠธ, ๋ชจ๋‹ˆํ„ฐ๋ง ๋Œ€์‹œ๋ณด๋“œ ๋“ฑ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

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

  • ๊ฐ„๋‹จํ•œ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  ๊ตฌํ˜„: WebSocket์ฒ˜๋Ÿผ ๋ณต์žกํ•œ ํ”„๋กœํ† ์ฝœ ํ•ธ๋“œ์‰์ดํฌ ์—†์ด, ์ผ๋ฐ˜ HTTP ์—ฐ๊ฒฐ๋งŒ์œผ๋กœ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
  • ๋ธŒ๋ผ์šฐ์ € ๋„ค์ดํ‹ฐ๋ธŒ ์ง€์›: ๋ณ„๋„์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ œ๊ณตํ•˜๋Š” API๋งŒ์œผ๋กœ SSE๋ฅผ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
  • ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๋ฐ ์ƒํƒœ ๊ด€๋ฆฌ: ์—ฐ๊ฒฐ ๋Š๊น€, ์žฌ์—ฐ๊ฒฐ, ์—ฐ๊ฒฐ ์ƒํƒœ ์ถ”์  ๋“ฑ์„ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ณต์žกํ•œ ๋กœ์ง์„ ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

AS-IS: XMLHttpRequest๋กœ ํด๋ง ๊ตฌํ˜„

// ์ˆ˜๋™์œผ๋กœ ํด๋ง ๋ฐ ์žฌ์—ฐ๊ฒฐ ๋กœ์ง ๊ตฌํ˜„ ํ•„์š”
function pollServer() {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', '/api/updates');
 
  xhr.onload = function() {
    if (xhr.status === 200) {
      const data = JSON.parse(xhr.responseText);
      handleUpdate(data);
    }
    // 5์ดˆ ํ›„ ๋‹ค์‹œ ์š”์ฒญ
    setTimeout(pollServer, 5000);
  };
 
  xhr.onerror = function() {
    console.error('Request failed');
    // ์—๋Ÿฌ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง ํ•„์š”
    setTimeout(pollServer, 10000);
  };
 
  xhr.send();
}
 
pollServer();

TO-BE: EventSource ์‚ฌ์šฉ

// ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๋ฐ ์ง€์†์  ์—ฐ๊ฒฐ ์ œ๊ณต
const eventSource = new EventSource('/api/updates');
 
eventSource.onmessage = function(event) {
  const data = JSON.parse(event.data);
  handleUpdate(data);
};
 
eventSource.onerror = function(error) {
  console.error('Connection error:', error);
  // ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„
};
 
// ํ•„์š” ์‹œ ์—ฐ๊ฒฐ ์ข…๋ฃŒ
// eventSource.close();

EventSource ์ƒ์„ฑ์ž

const eventSource = new EventSource(url, options);

๋งค๊ฐœ๋ณ€์ˆ˜

  • url (string, ํ•„์ˆ˜): SSE ์ด๋ฒคํŠธ๋ฅผ ์ „์†กํ•˜๋Š” ์„œ๋ฒ„ ์—”๋“œํฌ์ธํŠธ URL
  • options (object, ์„ ํƒ):
    • withCredentials (boolean): CORS ์š”์ฒญ ์‹œ ์ธ์ฆ ์ •๋ณด(์ฟ ํ‚ค, Authorization ํ—ค๋” ๋“ฑ) ํฌํ•จ ์—ฌ๋ถ€. ๊ธฐ๋ณธ๊ฐ’์€ false

์˜ˆ์‹œ

// ๊ธฐ๋ณธ ์‚ฌ์šฉ
const eventSource = new EventSource('/api/events');
 
// CORS ์ธ์ฆ ์ •๋ณด ํฌํ•จ
const eventSourceWithCreds = new EventSource('https://api.example.com/events', {
  withCredentials: true
});

EventSource ์†์„ฑ (Properties)

์†์„ฑํƒ€์ž…์„ค๋ช…
readyStatenumber (์ฝ๊ธฐ ์ „์šฉ)ํ˜„์žฌ ์—ฐ๊ฒฐ ์ƒํƒœ: CONNECTING (0), OPEN (1), CLOSED (2)
urlstring (์ฝ๊ธฐ ์ „์šฉ)์—ฐ๊ฒฐ๋œ ์ด๋ฒคํŠธ ์†Œ์Šค์˜ URL
withCredentialsboolean (์ฝ๊ธฐ ์ „์šฉ)CORS ์ธ์ฆ ์ •๋ณด ํฌํ•จ ์—ฌ๋ถ€

์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ

const eventSource = new EventSource('/api/events');
 
console.log(eventSource.readyState); // 0 (CONNECTING)
 
eventSource.onopen = function() {
  console.log(eventSource.readyState); // 1 (OPEN)
};
 
eventSource.close();
console.log(eventSource.readyState); // 2 (CLOSED)

์—ฐ๊ฒฐ ์ƒํƒœ ์ƒ์ˆ˜

EventSource.CONNECTING = 0  // ์—ฐ๊ฒฐ ์ˆ˜๋ฆฝ ์ค‘
EventSource.OPEN = 1        // ์—ฐ๊ฒฐ ์—ด๋ฆผ, ์ด๋ฒคํŠธ ์ˆ˜์‹  ์ค‘
EventSource.CLOSED = 2      // ์—ฐ๊ฒฐ ๋‹ซํž˜

EventSource ๋ฉ”์„œ๋“œ

close()

์—ฐ๊ฒฐ์„ ๋‹ซ๊ณ  readyState๋ฅผ CLOSED๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ๋‹ซํ˜€ ์žˆ์œผ๋ฉด ์•„๋ฌด ๋™์ž‘๋„ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

const eventSource = new EventSource('/api/events');
 
// ํŠน์ • ์กฐ๊ฑด์—์„œ ์—ฐ๊ฒฐ ์ข…๋ฃŒ
if (userLoggedOut) {
  eventSource.close();
  console.log('Connection closed');
}

EventSource ์ด๋ฒคํŠธ

์ด๋ฒคํŠธ์„ค๋ช…
open์„œ๋ฒ„์™€์˜ ์—ฐ๊ฒฐ์ด ์—ด๋ ธ์„ ๋•Œ ๋ฐœ์ƒ
message์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์‹ ํ–ˆ์„ ๋•Œ ๋ฐœ์ƒ (์ด๋ฆ„ ์—†๋Š” ์ด๋ฒคํŠธ)
error์—ฐ๊ฒฐ ์‹คํŒจ ๋˜๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋ฐœ์ƒ

์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก ๋ฐฉ๋ฒ•

๋ฐฉ๋ฒ• 1: on* ์†์„ฑ ์‚ฌ์šฉ

const eventSource = new EventSource('/api/events');
 
eventSource.onopen = function(event) {
  console.log('Connection opened', event);
};
 
eventSource.onmessage = function(event) {
  console.log('Message received:', event.data);
};
 
eventSource.onerror = function(event) {
  console.error('Error occurred:', event);
 
  if (eventSource.readyState === EventSource.CLOSED) {
    console.log('Connection closed');
  }
};

๋ฐฉ๋ฒ• 2: addEventListener ์‚ฌ์šฉ

const eventSource = new EventSource('/api/events');
 
eventSource.addEventListener('open', (event) => {
  console.log('Connection opened');
});
 
eventSource.addEventListener('message', (event) => {
  console.log('Message:', event.data);
});
 
eventSource.addEventListener('error', (event) => {
  if (eventSource.readyState === EventSource.CLOSED) {
    console.log('Connection lost');
  }
});

์ด๋ฆ„ ์žˆ๋Š” ์ด๋ฒคํŠธ (Named Events)

์„œ๋ฒ„์—์„œ ํŠน์ • ์ด๋ฒคํŠธ ํƒ€์ž…์„ ์ง€์ •ํ•˜์—ฌ ์ „์†กํ•˜๋ฉด, ํด๋ผ์ด์–ธํŠธ๋Š” ํ•ด๋‹น ์ด๋ฒคํŠธ๋งŒ ์„ ๋ณ„์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ

const sse = new EventSource('/api/v1/sse');
 
// "notice" ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
sse.addEventListener('notice', (event) => {
  console.log('Notice:', event.data);
  showNotification(event.data);
});
 
// "update" ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
sse.addEventListener('update', (event) => {
  console.log('Update:', event.data);
  refreshData(JSON.parse(event.data));
});
 
// ์ด๋ฆ„ ์—†๋Š” ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
sse.addEventListener('message', (event) => {
  console.log('Generic message:', event.data);
});
 
// ์—๋Ÿฌ ์ฒ˜๋ฆฌ
sse.addEventListener('error', (event) => {
  if (event.readyState === EventSource.CLOSED) {
    console.log('Connection closed');
  }
});

์„œ๋ฒ„ ์ฝ”๋“œ (Node.js/Express)

app.get('/api/v1/sse', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
 
  // ์ด๋ฆ„ ์žˆ๋Š” ์ด๋ฒคํŠธ ์ „์†ก
  res.write('event: notice\n');
  res.write('data: {"message": "System maintenance in 10 minutes"}\n\n');
 
  res.write('event: update\n');
  res.write('data: {"stock": "AAPL", "price": 150.23}\n\n');
 
  // ์ด๋ฆ„ ์—†๋Š” ๋ฉ”์‹œ์ง€ ์ „์†ก (๊ธฐ๋ณธ "message" ์ด๋ฒคํŠธ)
  res.write('data: {"type": "generic", "content": "Hello"}\n\n');
});

EventSource์˜ ์ฃผ์š” ํŠน์ง• 3๊ฐ€์ง€

1. ์ž๋™ ์žฌ์—ฐ๊ฒฐ

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

const eventSource = new EventSource('/api/events');
 
eventSource.onerror = function() {
  // ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„
  console.log('Connection lost, reconnecting...');
};
 
// ์„œ๋ฒ„์—์„œ ์žฌ์—ฐ๊ฒฐ ๊ฐ„๊ฒฉ์„ ์ œ์–ดํ•  ์ˆ˜๋„ ์žˆ์Œ
// res.write('retry: 3000\n'); // 3์ดˆ ํ›„ ์žฌ์—ฐ๊ฒฐ

2. HTTP/1.1๊ณผ HTTP/2์—์„œ์˜ ์—ฐ๊ฒฐ ์ œํ•œ

์ค‘์š”ํ•œ ์ œํ•œ ์‚ฌํ•ญ: HTTP/1.1์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋„๋ฉ”์ธ๋‹น ์•ฝ 6๊ฐœ์˜ SSE ์—ฐ๊ฒฐ๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ํƒญ์„ ์—ด๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

  • HTTP/2 ์‚ฌ์šฉ: HTTP/2์—์„œ๋Š” 100๊ฐœ ์ด์ƒ์˜ ๋™์‹œ ์ŠคํŠธ๋ฆผ์„ ์ง€์›ํ•˜๋ฏ€๋กœ ์—ฐ๊ฒฐ ์ œํ•œ์ด ํฌ๊ฒŒ ์™„ํ™”๋ฉ๋‹ˆ๋‹ค
  • ์—ฐ๊ฒฐ ํ†ตํ•ฉ: ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ํ•˜๋‚˜์˜ SSE ์—ฐ๊ฒฐ๋กœ ํ†ตํ•ฉ
  • WebSocket ์‚ฌ์šฉ: ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ WebSocket์œผ๋กœ ์ „ํ™˜
graph TD
    A[HTTP/1.1] -->|์—ฐ๊ฒฐ ์ œํ•œ| B[๋„๋ฉ”์ธ๋‹น 6๊ฐœ]
    A --> C[๋‹ค์ค‘ ํƒญ ์‚ฌ์šฉ ์‹œ ๋ฌธ์ œ ๋ฐœ์ƒ]

    D[HTTP/2] -->|์—ฐ๊ฒฐ ์ œํ•œ| E[100+ ๋™์‹œ ์ŠคํŠธ๋ฆผ]
    D --> F[๋‹ค์ค‘ ํƒญ ์‚ฌ์šฉ ๊ฐ€๋Šฅ]

    style B fill:#ff6b6b
    style E fill:#51cf66

3. Web Workers ์ง€์›

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

worker.js

const eventSource = new EventSource('/api/events');
 
eventSource.onmessage = function(event) {
  // ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋กœ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
  self.postMessage({
    type: 'sse-data',
    data: event.data
  });
};
 
eventSource.onerror = function() {
  self.postMessage({
    type: 'sse-error',
    message: 'Connection failed'
  });
};

main.js

const worker = new Worker('worker.js');
 
worker.onmessage = function(event) {
  if (event.data.type === 'sse-data') {
    console.log('Received from worker:', event.data.data);
    updateUI(event.data.data);
  }
};

์‹ค์ œ ์‚ฌ์šฉ ์˜ˆ์‹œ

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

const notifications = new EventSource('/api/notifications');
 
notifications.addEventListener('notification', (event) => {
  const data = JSON.parse(event.data);
 
  // ๋ธŒ๋ผ์šฐ์ € ์•Œ๋ฆผ ํ‘œ์‹œ
  if (Notification.permission === 'granted') {
    new Notification(data.title, {
      body: data.message,
      icon: data.icon
    });
  }
});

๋ผ์ด๋ธŒ ๋ชจ๋‹ˆํ„ฐ๋ง ๋Œ€์‹œ๋ณด๋“œ

const metrics = new EventSource('/api/metrics');
 
metrics.onmessage = (event) => {
  const data = JSON.parse(event.data);
 
  // ์ฐจํŠธ ์—…๋ฐ์ดํŠธ
  updateChart({
    cpu: data.cpu,
    memory: data.memory,
    requests: data.requests
  });
};

์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  (๋‹จ๋ฐฉํ–ฅ)

const chat = new EventSource('/api/chat/messages');
 
chat.addEventListener('new-message', (event) => {
  const message = JSON.parse(event.data);
 
  appendMessageToChat({
    user: message.user,
    text: message.text,
    timestamp: message.timestamp
  });
});

์ฐธ๊ณ  ๋ฌธ์„œ