๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu๋Š” fastify-sse-v2 ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ **Server-Sent Events (SSE)**๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. SSE๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘ธ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

SSE๋ž€?

Server-Sent Events๋Š” ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ๋‹จ๋ฐฉํ–ฅ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์„ ์ œ๊ณตํ•˜๋Š” ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค.

HTTP vs SSE vs WebSocket

๋น„๊ตํ‘œ

ํŠน์ง•HTTPSSEWebSocket
๋ฐฉํ–ฅ์š”์ฒญโ†’์‘๋‹ต์„œ๋ฒ„โ†’ํด๋ผ์ด์–ธํŠธ์–‘๋ฐฉํ–ฅ
ํ”„๋กœํ† ์ฝœHTTPHTTPWebSocket
์žฌ์—ฐ๊ฒฐX์ž๋™์ˆ˜๋™
๋ณต์žก๋„๋‚ฎ์Œ์ค‘๊ฐ„๋†’์Œ
์šฉ๋„์ผ๋ฐ˜ API์‹ค์‹œ๊ฐ„ ํ‘ธ์‹œ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…

SSE ์‚ฌ์šฉ ์‚ฌ๋ก€

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

์ƒˆ ๋ฉ”์‹œ์ง€, ์ข‹์•„์š”, ๋Œ“๊ธ€ ๋“ฑ

์ง„ํ–‰ ์ƒํ™ฉ

ํŒŒ์ผ ์—…๋กœ๋“œ, ์ž‘์—… ์ฒ˜๋ฆฌ ์ง„ํ–‰๋ฅ 

๋ผ์ด๋ธŒ ํ”ผ๋“œ

๋‰ด์Šค ํ”ผ๋“œ, ์†Œ์…œ ๋ฏธ๋””์–ด ์—…๋ฐ์ดํŠธ

๋ชจ๋‹ˆํ„ฐ๋ง

์„œ๋ฒ„ ์ƒํƒœ, ๋กœ๊ทธ ์ŠคํŠธ๋ฆฌ๋ฐ

๊ธฐ๋ณธ ์„ค์ •

sonamu.config.ts

import { type SonamuConfig } from "sonamu";

export const config: SonamuConfig = {
  server: {
    plugins: {
      sse: true,  // SSE ํ”Œ๋Ÿฌ๊ทธ์ธ ํ™œ์„ฑํ™”
    }
  }
};
๊ธฐ๋ณธ ๋™์ž‘:
  • SSE ์—”๋“œํฌ์ธํŠธ ์ž๋™ ๋“ฑ๋ก
  • ์ž๋™ ์žฌ์—ฐ๊ฒฐ ์ง€์›
  • Keep-alive ์ž๋™ ์ „์†ก

SSE ํ”Œ๋Ÿฌ๊ทธ์ธ ์˜ต์…˜

๊ฐ„๋‹จํ•œ ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”
plugins: {
  sse: true,   // ํ™œ์„ฑํ™”
  sse: false,  // ๋น„ํ™œ์„ฑํ™”
}

SSE ์ž‘๋™ ๋ฐฉ์‹

์—ฐ๊ฒฐ ํ๋ฆ„

HTTP ํ—ค๋”

SSE๋Š” ํŠน์ˆ˜ํ•œ HTTP ํ—ค๋”๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
ํŠน์ง•:
  • text/event-stream: SSE ์ „์šฉ Content-Type
  • no-cache: ์บ์‹ฑ ๋ฐฉ์ง€
  • keep-alive: ์—ฐ๊ฒฐ ์œ ์ง€

์‹ค์ „ ์„ค์ • ์˜ˆ์ œ

1. ๊ธฐ๋ณธ ์„ค์ • (๊ถŒ์žฅ)

export const config: SonamuConfig = {
  server: {
    plugins: {
      sse: true,  // ๊ฐ„๋‹จํ•˜๊ฒŒ ํ™œ์„ฑํ™”
    }
  }
};

2. ๊ฐœ๋ฐœ/ํ”„๋กœ๋•์…˜ ๋ถ„๋ฆฌ

const isDevelopment = process.env.NODE_ENV === 'development';

export const config: SonamuConfig = {
  server: {
    plugins: {
      sse: !isDevelopment,  // ํ”„๋กœ๋•์…˜์—์„œ๋งŒ ํ™œ์„ฑํ™”
    }
  }
};
์ด์œ : ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” HMR๋กœ ์ธํ•ด ์—ฐ๊ฒฐ์ด ์ž์ฃผ ๋Š๊น€

3. ์กฐ๊ฑด๋ถ€ ํ™œ์„ฑํ™”

export const config: SonamuConfig = {
  server: {
    plugins: {
      sse: process.env.ENABLE_SSE === 'true',
    }
  }
};
ENABLE_SSE=true

์••์ถ• ๋น„ํ™œ์„ฑํ™”

SSE๋Š” ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต์ด๋ฏ€๋กœ ์••์ถ•์„ ๋น„ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
export const config: SonamuConfig = {
  server: {
    plugins: {
      sse: true,
      compress: {
        global: true,  // ์ „์—ญ ์••์ถ• ํ™œ์„ฑํ™”
        threshold: 1024,
        encodings: ["gzip"],
      }
    }
  }
};
@stream ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์—์„œ ์ž๋™ ์ฒ˜๋ฆฌ:
@stream({
  type: 'sse',
  events: z.object({
    message: z.string(),
  })
})
@api({
  compress: false,  // ์ž๋™์œผ๋กœ ์••์ถ• ๋น„ํ™œ์„ฑํ™” (๊ถŒ์žฅ)
})
async *streamUpdates() {
  // ...
}

CORS ์„ค์ •

SSE๋ฅผ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด CORS ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
export const config: SonamuConfig = {
  server: {
    plugins: {
      sse: true,
      cors: {
        origin: ['https://example.com', 'https://app.example.com'],
        credentials: true,
      }
    }
  }
};
์ฃผ์˜: SSE๋Š” ์ธ์ฆ ์ฟ ํ‚ค ๋“ฑ์„ ์ „์†กํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ credentials: true ์„ค์ • ํ•„์š”

ํ™˜๊ฒฝ๋ณ„ ์ „๋žต

๊ฐœ๋ฐœ ํ™˜๊ฒฝ

export const config: SonamuConfig = {
  server: {
    plugins: {
      sse: true,
      cors: {
        origin: 'http://localhost:5173',  // Vite ๊ฐœ๋ฐœ ์„œ๋ฒ„
        credentials: true,
      }
    }
  }
};

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ

export const config: SonamuConfig = {
  server: {
    plugins: {
      sse: true,
      cors: {
        origin: process.env.FRONTEND_URL,
        credentials: true,
      }
    }
  }
};
FRONTEND_URL=https://app.example.com

ํƒ€์ž„์•„์›ƒ ์„ค์ •

SSE ์—ฐ๊ฒฐ์€ ์žฅ์‹œ๊ฐ„ ์œ ์ง€๋˜๋ฏ€๋กœ ํƒ€์ž„์•„์›ƒ ์„ค์ •์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.
export const config: SonamuConfig = {
  server: {
    fastify: {
      connectionTimeout: 0,  // ํƒ€์ž„์•„์›ƒ ๋น„ํ™œ์„ฑํ™”
      keepAliveTimeout: 5000,  // Keep-alive: 5์ดˆ
    },
    plugins: {
      sse: true,
    }
  }
};
์„ค์ • ๊ฐ’:
  • connectionTimeout: 0: ์—ฐ๊ฒฐ ํƒ€์ž„์•„์›ƒ ๋น„ํ™œ์„ฑํ™” (SSE๋Š” ์žฅ์‹œ๊ฐ„ ์œ ์ง€)
  • keepAliveTimeout: Keep-alive ๊ฐ„๊ฒฉ (๊ธฐ๋ณธ๊ฐ’: 5์ดˆ)

ํ”„๋ก์‹œ ์„ค์ • (Nginx)

Nginx๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ SSE๋ฅผ ์œ„ํ•œ ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
server {
    listen 80;
    server_name api.example.com;

    location /stream/ {
        proxy_pass http://localhost:3000;
        
        # SSE ํ•„์ˆ˜ ์„ค์ •
        proxy_set_header Connection '';
        proxy_http_version 1.1;
        chunked_transfer_encoding off;
        
        # ๋ฒ„ํผ๋ง ๋น„ํ™œ์„ฑํ™”
        proxy_buffering off;
        proxy_cache off;
        
        # ํƒ€์ž„์•„์›ƒ
        proxy_read_timeout 24h;
        proxy_send_timeout 24h;
    }
}
ํ•ต์‹ฌ ์„ค์ •:
  • proxy_buffering off: ๋ฒ„ํผ๋ง ๋น„ํ™œ์„ฑํ™” (์ฆ‰์‹œ ์ „์†ก)
  • proxy_cache off: ์บ์‹ฑ ๋น„ํ™œ์„ฑํ™”
  • proxy_read_timeout 24h: ์ฝ๊ธฐ ํƒ€์ž„์•„์›ƒ (์žฅ์‹œ๊ฐ„)

๋””๋ฒ„๊น…

๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ

Network ํƒญ โ†’ ์ŠคํŠธ๋ฆผ ์š”์ฒญ ์„ ํƒ โ†’ Headers ํƒญ

Request Headers:
  Accept: text/event-stream
  
Response Headers:
  Content-Type: text/event-stream
  Cache-Control: no-cache
  Connection: keep-alive

EventStream ํƒญ:
  event: message
  data: {"text": "Hello"}
  
  event: update
  data: {"count": 5}

curl ํ…Œ์ŠคํŠธ

# SSE ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ
curl -N http://localhost:3000/stream/updates

# ๊ฒฐ๊ณผ:
# event: message
# data: {"text": "Hello"}
#
# event: update
# data: {"count": 5}
#
# event: end
# data: END
์˜ต์…˜:
  • -N: ๋ฒ„ํผ๋ง ๋น„ํ™œ์„ฑํ™” (์ฆ‰์‹œ ์ถœ๋ ฅ)

์ฃผ์˜์‚ฌํ•ญ

SSE ์„ค์ • ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. ์••์ถ• ๋น„ํ™œ์„ฑํ™”: SSE๋Š” ์ŠคํŠธ๋ฆฌ๋ฐ์ด๋ฏ€๋กœ ์••์ถ• ๊ธˆ์ง€
    @api({ compress: false })
    async *streamData() { ... }
    
  2. ํƒ€์ž„์•„์›ƒ ์„ค์ •: ์žฅ์‹œ๊ฐ„ ์—ฐ๊ฒฐ ์œ ์ง€
    fastify: {
      connectionTimeout: 0,
    }
    
  3. CORS ์„ค์ •: ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์—์„œ ์‚ฌ์šฉ ์‹œ ํ•„์š”
    cors: {
      origin: 'https://example.com',
      credentials: true,
    }
    
  4. ์žฌ์—ฐ๊ฒฐ ์ฒ˜๋ฆฌ: ํด๋ผ์ด์–ธํŠธ๋Š” ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๊ตฌํ˜„ ํ•„์š”
    // EventSource๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ
    const source = new EventSource('/stream/updates');
    
  5. ๋ธŒ๋ผ์šฐ์ € ์ œํ•œ: ๋™์‹œ SSE ์—ฐ๊ฒฐ ์ˆ˜ ์ œํ•œ (๋„๋ฉ”์ธ๋‹น 6๊ฐœ)
    • ํ•ด๊ฒฐ: HTTP/2 ์‚ฌ์šฉ ๋˜๋Š” ์—ฐ๊ฒฐ ์žฌ์‚ฌ์šฉ
  6. ํ”„๋ก์‹œ ๋ฒ„ํผ๋ง: Nginx ๋“ฑ ๋ฒ„ํผ๋ง ๋น„ํ™œ์„ฑํ™” ํ•„์š”
    proxy_buffering off;
    

๋ธŒ๋ผ์šฐ์ € ์ง€์›

SSE๋Š” ๋ชจ๋“  ๋ชจ๋˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง€์›๋ฉ๋‹ˆ๋‹ค:
๋ธŒ๋ผ์šฐ์ €์ง€์›
Chromeโœ…
Firefoxโœ…
Safariโœ…
Edgeโœ…
IE 11โŒ (polyfill ํ•„์š”)
IE 11 ์ง€์›: event-source-polyfill

๋‹ค์Œ ๋‹จ๊ณ„