개발일기

Javascript - SSE로 Client와 Server간 통신하기 본문

Javascript

Javascript - SSE로 Client와 Server간 통신하기

Flashback 2025. 5. 2. 22:56
728x90
반응형

 

1. SSE(Server Side Events)

SSE는 HTTP기반의 스트리밍 기술이며 Websocket과 다르게 서버에서 클라이언트로 메시지를 보내기만 하면 되는 단방향 통신이 필요할때 사용하기 적합한 기술이다. 즉, 클라이언트가 요청을 하지 않아도 서버에서 자동으로 데이터를 보내야 하는 경우에 사용한다. 만약 클라이언트에서 서버로 요청을 보내야하면 RestAPI를 사용하여 요청을 보내면 SSE의 단점을 보완하여 사용할 수 있다.

// Client
const eventSource = new EventSource('url...')

new EventSource를 사용하여 클라이언트에서 SSE 연결을 시작하게 할 수 있다. 입력된 url로 클라이언트가 GET요청을 보내게 되고 서버는 text/event-stream 형식으로 데이터를 지속적으로 전송하게 된다. 이를 통해 클라이언트는 서버에서 보낸 데이터를 실시간으로 수신할 수 있다.

2. 서버 구성

SSE를 사용하려면 서버가 있어야 하는데 간단하게 express를 사용하여 서버를 구성한다.

// Server
const express = require('express')
const http = require('http')
const data = https.createServer()
const port = 3000
const app = express()

http.createServer()

app.use(express.json()) // request body data to json
app.listen(port, () => {
    console.log('Server Start ' + port)
})
app.get('/connection', (req, res) => {
	  res.setHeader('Content-Type', 'text/event-stream')
    res.setHeader('Cache-Control', 'no-cache')
    res.setHeader('Connection', 'keep-alive')
    
    console.log(res) // check connection
})
  • Content-Type text/event-stream: 서버에서 클라이언트로 실시간 데이터를 전송하기 위해 사용하는 MIME 타입
  • Cache-Control no-cache: 클라이언트가 항상 최신 데이터를 받을 수 있게 하기 위해 응답을 캐싱하지 않도록 설정한다.
  • Connection keep-alive: HTTP연결 세션을 지속적으로 유지시키기 위해 사용한다.

3. 클라이언트 확인

localhost:3000에 express 서버를 오픈하였다. SSE를 사용하려면 서버와 클라이언트를 연결시킬 수 있는 EndPoint를 생성해야 한다. 이는 GET타입으로 지정해야 하며 위와 같이 ‘/connection’ 또는 원하는 주소로 설정한다. 이 EndPoint를 통해 클라이언트가 요청을 보내면 서로 연결되어 데이터를 보낼 준비가 완료되게 된다. 이 경우에는 연결 주소가 localhost:3000/connection 이 된다.

클라이언트에서 서버의 SSE 엔드포인트로 요청을 보내 실제로 연결이 되었는지 확인할 수 있다.

// Client
const eventSource = new EventSource(`http://localhost:3000/connection`)

요청을 보낸 후 서버 콘솔에서 연결 확인을 위해 추가한 로그가 찍히면 최종적으로 연결이 완료된 것이다.

콘솔에 찍힌 res 로그를 보면 연결된 클라이언트들을 구분할만한 명확한 데이터를 찾기가 힘들다. 이를 보완하기 위해 SSE EndPoint에 query parameter 를 추가하여 연결하면 클라이언트를 구분하여 관리하기 쉬워진다.

4. 클라이언트 관리

// Client
const userId = 'orange'
const eventSource = new EventSource(`http://localhost:3000/connection?userId=${ userId }`)
// Server
app.get('/connection', (req, res) => {
	  res.setHeader('Content-Type', 'text/event-stream')
    res.setHeader('Cache-Control', 'no-cache')
    res.setHeader('Connection', 'keep-alive')
    
    const userId = res.query.userId
    console.log('userId: ', userId) // check connection
})

이렇게 하면 연결이 되었을 때 서버에서 클라이언트의 userId 를 확인하여 사용자 구분을 원활하게 할 수 있다. 이렇게 클라이언트 구분이 가능해진 상태에서 서버에 코드를 몇 줄 추가하여 현재 연결된 클라이언트를 관리할 수 있다.

 

// Server
let clients = {} // clients list
app.get('/connection', (req, res) => {
	  res.setHeader('Content-Type', 'text/event-stream')
    res.setHeader('Cache-Control', 'no-cache')
    res.setHeader('Connection', 'keep-alive')
    
    const userId = res.query.userId // userId
    clients[userId] = res
        
    req.on('close', () => { // Connection Closed
        console.log('Connection Closed')
        delete clients[userId]
    })
    
    console.log('clients: ', Object.keys(clients)) // Connected ClientList
})

연결된 클라이언트를 clients 객체에 추가하여 관리한다. Object.keys(clients )를 출력하면 현재 연결된 사용자들을 확인할 수 있다.

  • req.on(’close’): 클라이언트의 연결이 끊겼을 때 즉, 웹 페이지라면 해당 웹 페이지에서 다른 페이지로 이동했을 때 실행되는 메서드이다. 연결이 끊긴 클라이언트에게는 메시지를 보낼 필요가 없다. delete를 사용하여 해당 클라이언트를 객체에서 삭제시켜 목록을 관리한다.

5. 메시지 전달

서버와 클라이언트가 연결되었다면 서버에서 클라이언트에게 메시지를 전달하도록 할 수 있다. 서버에서 write() 함수를 사용하여 메시지를 전달할 수 있다.

// Server
const data = {
    'price': 1000,
    'taste': 'yummy'
}
clients[userId].write(`data: ${ JSON.stringify(data) }\\n\\n`) // Specific Client

/*
    clients.forEach(client => {
        client.write(`data: ${ JSON.stringify(data) }\\n\\n`)
    })
*/ // All Clients
// app.get('/connection' ~) 안에 추가
  • write(): 클라이언트에 메시지를 보내기 위해 사용한다.

가격과 맛에 대한 데이터를 담은 메시지를 클라이언트에 전송한다. 특정한 클라이언트에게 메시지를 보내기 위해 key로 구분하여 전송한다. 모든 클라이언트에게 메시지를 보내려면 주석 된 코드를 활용하여 전송할 수 있다. write()의 마지막 부분을 보면 \n\n 개행 문자가 있는 것을 확인할 수 있다. 전송 데이터의 마지막 부분이라는 것을 명시하기 위해 \n\n를 사용한다. \n\n를 추가하지 않으면 메시지가 전송되지 않기 때문에 데이터의 마지막 부분에는 꼭 \n\n를 추가해야 한다.

// Client
eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data)
    
    console.log('Data: ', data)
}

서버에서 전송된 메시지를 client의 onmessage를 통해 전달된다. 클라이언트에서 로그를 확인하면 전송된 메시지에 대한 내용이 출력 된 것을 확인할 수 있다.

 

6. 이벤트 타입 지정

서버가 클라이언트에게 메시지를 전달할 때 이벤트 타입을 지정하여 이벤트로 구분할 수도 있다.

// Server
const data = {
    'price': 1000,
    'taste': 'yummy'
}
clients[userId].write(`event: farm\\n`); // event타입을 지정 가능
clients[userId].write(`data: ${ JSON.stringify(data) }\\n\\n`)
// Client
// add SSE events
eventSource.addEventListener('farm', (event) => {
    console.log('farm event: ', JSON.parse(event.data))
}) // farm event

eventSource.addEventListener('valley', (event) => {
    console.log('valley event: ', JSON.parse(event.data))
}) // valley event

addEventListener로 이벤트를 추가하여 해당 이벤트에 대한 메시지를 받아볼 수 있게 할 수 있다. 위의 Server쪽 코드를 보면 이벤트 명이 farm으로 설정하고 메시지를 전달하였다. 그러면 클라이언트는 farm 이벤트의 콜백 함수에서 메시지를 확인할 수 있다. 이벤트 명과 일치하는 메시지가 클라이언트에 도달한 경우에는 onmessage로 메시지가 전달되지 않고 addEventListener의 콜백 함수에서 메시지를 확인할 수 있다.

 

7. SSE 연결 종료

단순하게 웹 페이지를 이탈하는 거 외에도 클라이언트에서 SSE연결을 종료하는 방법이 존재한다.

// Client
eventSource.close()
  • eventSource.close()를 통해 SSE연결을 종료한다. 연결이 종료되면 위의 코드에서 본 ‘close’ 이벤트가 서버에서 실행된다.

EventSource는 서버와 연결이 되지 않으면 지속적으로 재연결을 시도한다. 재연결을 시도하면서 onerror함수가 실행되며 eventSource.readyState로 현재 연결 상태를 확인할 수 있다.

  • CONNECTION: 0 현재 연결을 시도 중인 상태
  • OPEN: 1 서버에 연결된 상태
  • CLOSED: 2 서버와 연결이 끊긴 상태

8. 재연결 방지

지속적인 재연결 시도를 방지하려면 재연결 횟수를 제한하는 코드를 추가하여 이를 방지할 수 있다.

// Client
let count = 0
eventSource.onerror = () => {
    console.log('Error State: ', evenSource.readyState)
    
    switch(eventSource.readyState) {
        case eventSource.CONNECTION: // 0
            count++
            
            if(count >= 5) { // 재연결 횟수가 5번을 넘어가면
                eventSource.close() // 연결 종료
            }
            break
    }
}

 

9. 종합 코드

// Server
const express = require('express')
const http = require('http')
const data = https.createServer()
const port = 3000
const app = express()

http.createServer()

app.use(express.json()) // request body data to json
app.listen(port, () => {
    console.log('Server Start ' + port)
})
app.get('/connection', (req, res) => {
	  res.setHeader('Content-Type', 'text/event-stream')
    res.setHeader('Cache-Control', 'no-cache')
    res.setHeader('Connection', 'keep-alive')
    
    const userId = res.query.userId // userId
    const data = {
        'price': 1000,
        'taste': 'yummy'
    }
    clients[userId] = res
    clients[userId].write(`event: farm\\n`); // event타입을 지정 가능
    clients[userId].write(`data: ${ JSON.stringify(data) }\\n\\n`)
        
    req.on('close', () => { // Connection Closed
        console.log('Connection Closed')
        delete clients[userId]
    })
    
    console.log('clients: ', Object.keys(clients)) // Connected ClientList
})
// Client
import React, { useEffect } from "react"

const App = () => {
    useEffect(() => {
        const userId = 'orange'
        const eventSource = new EventSource(`http://localhost:3000/connection?userId=${ userId }`)
        console.log(eventSource)

        // add SSE events
        eventSource.addEventListener('farm', (event) => {
            console.log('farm event: ', JSON.parse(event.data))
        }) // farm event

        eventSource.addEventListener('valley', (event) => {
            console.log('valley event: ', JSON.parse(event.data))
        }) // valley event

        eventSource.onmessage = (event) => {
            const data = JSON.parse(event.data)
            console.log('Data: ', data)
        }

        return () => {
            eventSource.close()
        }
    }, [])
    
    return (
        <></>
    )
}

export default App

Client에서 SSE 연결 부분을 useEffect에 추가하고 두번째 인자에 빈배열을 넣어 준다. CleanUp부분에 SSE 연결을 끊는 코드를 넣어주면 해당 페이지에 접속했을 때, 자동으로 SSE 연결을 하고 페이지를 이탈하면 자동으로 연결이 끊기도록 코드를 구성할 수 있다.

 


참고 사이트:

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

 

Understanding Server-Sent Events (SSE) with Node.js

Real-time data exchange is crucial for modern web applications, from live sports updates and stock prices to chat apps and news feeds…

itsfuad.medium.com

 

https://stackoverflow.com/questions/20123762/what-the-difference-between-onmessage-and-addeventlistener

 

What the difference between `onmessage` and `.addEventListener`?

I'm trying to get data with server-sent event, what the different using source.onmessage vs source.addEventListener?

stackoverflow.com

 

https://developer.mozilla.org/en-US/docs/Web/API/EventSource/readyState

 

EventSource: readyState property - Web APIs | MDN

The readyState read-only property of the EventSource interface returns a number representing the state of the connection.

developer.mozilla.org

 

https://stackoverflow.com/questions/16652276/signalr-in-sse-mode-meaning-of-eventsource-readystate-property

 

SignalR in SSE Mode: Meaning of EventSource readyState Property

What is the meaning of these entries in the SignalR client-side logs? EventSource readyState: 0 (then, after 20-30 minutes of 0's, they all switch to 2's and SignalR stops communicating until I r...

stackoverflow.com

 

728x90
반응형
Comments