[Daily morning study] WebSocket vs Server-Sent Events

#daily morning study

Image


WebSocket๊ณผ SSE, ์–ธ์ œ ๋ฌด์—‡์„ ์“ธ๊นŒ

์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์ด ํ•„์š”ํ•œ ์ƒํ™ฉ์ด๋ฉด WebSocket ์•„๋‹ˆ๋ฉด SSE(Server-Sent Events) ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ฅด๊ฒŒ ๋œ๋‹ค. ๋‘˜ ๋‹ค HTTP ๊ธฐ๋ฐ˜์ด์ง€๋งŒ ๋ฐฉํ–ฅ์„ฑ๊ณผ ๊ตฌ์กฐ๊ฐ€ ๋‹ค๋ฅด๋‹ค.


WebSocket

WebSocket์€ ์–‘๋ฐฉํ–ฅ(full-duplex) ํ†ต์‹  ํ”„๋กœํ† ์ฝœ์ด๋‹ค. ํด๋ผ์ด์–ธํŠธ๊ฐ€ HTTP ์—…๊ทธ๋ ˆ์ด๋“œ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด, ์„œ๋ฒ„๊ฐ€ 101 Switching Protocols๋กœ ์‘๋‹ตํ•˜๋ฉด์„œ TCP ์—ฐ๊ฒฐ์ด WebSocket ์—ฐ๊ฒฐ๋กœ ์ „ํ™˜๋œ๋‹ค. ์—ฐ๊ฒฐ์ด ์ˆ˜๋ฆฝ๋œ ์ดํ›„์—๋Š” ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๋ชจ๋‘ ์–ธ์ œ๋“  ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

โ†’ HTTP/1.1 101 Switching Protocols
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

์ด ํ•ธ๋“œ์…ฐ์ดํฌ๊ฐ€ ๋๋‚˜๋ฉด HTTP ์—ฐ๊ฒฐ์ด ์ข…๋ฃŒ๋˜๊ณ , ๋™์ผํ•œ TCP ์†Œ์ผ“ ์œ„์—์„œ WebSocket ํ”„๋ ˆ์ž„์„ ์ฃผ๊ณ ๋ฐ›๋Š”๋‹ค.

Node.js ์„œ๋ฒ„ ์˜ˆ์‹œ (ws ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  ws.on('message', (message) => {
    console.log('๋ฐ›์€ ๋ฉ”์‹œ์ง€:', message.toString());
    ws.send('์—์ฝ”: ' + message.toString());
  });

  ws.on('close', () => {
    console.log('ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ์ข…๋ฃŒ');
  });
});

ํด๋ผ์ด์–ธํŠธ ์˜ˆ์‹œ

const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {
  ws.send('์•ˆ๋…•ํ•˜์„ธ์š”');
};

ws.onmessage = (event) => {
  console.log('์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ:', event.data);
};

ws.onclose = () => {
  console.log('์—ฐ๊ฒฐ ๋Š๊น€ โ€” ์žฌ์—ฐ๊ฒฐ ๋กœ์ง ํ•„์š”');
};

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


Server-Sent Events (SSE)

SSE๋Š” ์„œ๋ฒ„ โ†’ ํด๋ผ์ด์–ธํŠธ ๋‹จ๋ฐฉํ–ฅ ์ŠคํŠธ๋ฆฌ๋ฐ์ด๋‹ค. ๋ณ„๋„ ํ”„๋กœํ† ์ฝœ ์—…๊ทธ๋ ˆ์ด๋“œ ์—†์ด Content-Type: text/event-stream์„ ์‚ฌ์šฉํ•˜๋Š” ์ผ๋ฐ˜ HTTP ๋กฑ-๋ฆฌ๋น™ ์—ฐ๊ฒฐ์ด๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” EventSource API๋กœ ๊ตฌ๋…ํ•˜๊ณ , ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ ์žฌ์—ฐ๊ฒฐ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

Express ์„œ๋ฒ„ ์˜ˆ์‹œ

app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // ์ด๋ฒคํŠธ ID๋ฅผ ๋ณด๋‚ด๋ฉด ์žฌ์—ฐ๊ฒฐ ์‹œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ Last-Event-ID ํ—ค๋”๋กœ ๋ณด๋‚ด์คŒ
  let id = 0;

  const timer = setInterval(() => {
    res.write(`id: ${id++}\n`);
    res.write(`event: update\n`);
    res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
  }, 1000);

  req.on('close', () => {
    clearInterval(timer);
  });
});

SSE ์ด๋ฒคํŠธ ํ˜•์‹์€ \n\n์œผ๋กœ ๊ฐ ์ด๋ฒคํŠธ๋ฅผ ๊ตฌ๋ถ„ํ•œ๋‹ค. ํ•„๋“œ๋Š” data:, event:, id:, retry: ๋„ค ๊ฐ€์ง€๋‹ค.

ํด๋ผ์ด์–ธํŠธ ์˜ˆ์‹œ

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

eventSource.addEventListener('update', (event) => {
  const data = JSON.parse(event.data);
  console.log('์‹œ๊ฐ„:', data.time);
});

eventSource.onerror = () => {
  // ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™ ์žฌ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•จ
  console.log('์—ฐ๊ฒฐ ์˜ค๋ฅ˜ โ€” ์ž๋™ ์žฌ์‹œ๋„ ์ค‘');
};

// ๋” ์ด์ƒ ํ•„์š” ์—†์œผ๋ฉด ๋ช…์‹œ์ ์œผ๋กœ ๋‹ซ๊ธฐ
// eventSource.close();

ํ•ต์‹ฌ ๋น„๊ต

ํ•ญ๋ชฉWebSocketSSE
ํ†ต์‹  ๋ฐฉํ–ฅ์–‘๋ฐฉํ–ฅ์„œ๋ฒ„ โ†’ ํด๋ผ์ด์–ธํŠธ ๋‹จ๋ฐฉํ–ฅ
ํ”„๋กœํ† ์ฝœWS / WSSHTTP / HTTPS
๋ฐ์ดํ„ฐ ํ˜•์‹ํ…์ŠคํŠธ + ๋ฐ”์ด๋„ˆ๋ฆฌํ…์ŠคํŠธ๋งŒ (UTF-8)
์ž๋™ ์žฌ์—ฐ๊ฒฐ์—†์Œ (์ง์ ‘ ๊ตฌํ˜„)์žˆ์Œ (๋ธŒ๋ผ์šฐ์ € ๋‚ด์žฅ)
๋ฐฉํ™”๋ฒฝ ํ†ต๊ณผํ”„๋ก์‹œ ์„ค์ • ๋”ฐ๋ผ ๋‹ค๋ฆ„ํ‘œ์ค€ HTTP๋ผ ๋Œ€์ฒด๋กœ ๋ฌด๋‚œ
๋ธŒ๋ผ์šฐ์ € ์ง€์›๋งค์šฐ ๋„“์Œ๋„“์Œ (IE ์ œ์™ธ)
HTTP/2 ๋ฉ€ํ‹ฐํ”Œ๋ ‰์‹ฑ๋ถˆ๊ฐ€๊ฐ€๋Šฅ
๋™์‹œ ์—ฐ๊ฒฐ ์ œํ•œ์—†์ŒHTTP/1.1์—์„œ ๋„๋ฉ”์ธ๋‹น 6๊ฐœ

HTTP/2๋ฅผ ์“ฐ๋ฉด SSE์˜ ๋™์‹œ ์—ฐ๊ฒฐ 6๊ฐœ ์ œํ•œ์ด ์‚ฌ๋ผ์ง„๋‹ค. ํ•˜๋‚˜์˜ TCP ์—ฐ๊ฒฐ์—์„œ ์—ฌ๋Ÿฌ ์ŠคํŠธ๋ฆผ์„ ๋ฉ€ํ‹ฐํ”Œ๋ ‰์‹ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.


์ˆ˜ํ‰ ํ™•์žฅ ์‹œ ๊ณ ๋ ค์‚ฌํ•ญ

WebSocket

WebSocket ์—ฐ๊ฒฐ์€ ํŠน์ • ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค์™€ ๋งบ์–ด์ง„๋‹ค. ์—ฌ๋Ÿฌ ์„œ๋ฒ„๋กœ ์Šค์ผ€์ผ์•„์›ƒํ•  ๋•Œ ๊ฐ™์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋งค๋ฒˆ ๋™์ผํ•œ ์„œ๋ฒ„๋กœ ๋ผ์šฐํŒ…๋˜๋„๋ก ์Šคํ‹ฐํ‚ค ์„ธ์…˜์„ ์„ค์ •ํ•˜๊ฑฐ๋‚˜, Redis Pub/Sub ๊ฐ™์€ ๊ณต์œ  ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ๋‘์–ด ์„œ๋ฒ„ ๊ฐ„ ๋ฉ”์‹œ์ง€๋ฅผ ๋™๊ธฐํ™”ํ•ด์•ผ ํ•œ๋‹ค.

ํด๋ผ์ด์–ธํŠธ A โ”€โ”€โ†’ ์„œ๋ฒ„ 1 โ”€โ”€โ†’ Redis Pub/Sub โ”€โ”€โ†’ ์„œ๋ฒ„ 2 โ”€โ”€โ†’ ํด๋ผ์ด์–ธํŠธ B

SSE

SSE๋„ ๋กฑ-๋ฆฌ๋น™ HTTP ์—ฐ๊ฒฐ์ด๋ผ ๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ Redis๋‚˜ ๋ฉ”์‹œ์ง€ ํ๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ๋ฅผ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ์ผ๋ฐ˜์ ์ด๋‹ค.


์–ด๋–ค ๊ฑธ ์„ ํƒํ• ๊นŒ

WebSocket์ด ์ ํ•ฉํ•œ ๊ฒฝ์šฐ

  • ์ฑ„ํŒ…, ํ˜‘์—… ํŽธ์ง‘๊ธฐ์ฒ˜๋Ÿผ ํด๋ผ์ด์–ธํŠธ๋„ ์„œ๋ฒ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ž์ฃผ ๋ณด๋‚ด์•ผ ํ•  ๋•Œ
  • ์˜จ๋ผ์ธ ๊ฒŒ์ž„, ์ฃผ์‹ ๊ฑฐ๋ž˜ ํ”Œ๋žซํผ์ฒ˜๋Ÿผ ๋งค์šฐ ๋‚ฎ์€ ๋ ˆ์ดํ„ด์‹œ๊ฐ€ ํ•„์š”ํ•  ๋•Œ
  • ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ(์ด๋ฏธ์ง€, ํŒŒ์ผ ์กฐ๊ฐ)๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ „์†กํ•ด์•ผ ํ•  ๋•Œ

SSE๊ฐ€ ์ ํ•ฉํ•œ ๊ฒฝ์šฐ

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

WebSocket์€ ์–‘๋ฐฉํ–ฅ์ด ํ•„์š”ํ•  ๋•Œ ์“ฐ๊ณ , ์„œ๋ฒ„ ํ‘ธ์‹œ๋งŒ ํ•„์š”ํ•˜๋‹ค๋ฉด SSE๊ฐ€ ๊ตฌํ˜„์ด ๋” ๋‹จ์ˆœํ•˜๊ณ  ํ‘œ์ค€ HTTP ์ธํ”„๋ผ์™€ ์นœํ™”์ ์ด๋‹ค.