Go와 React로 만들어보는 Server Sent Event

Created
Jul 2, 2024 04:55 AM
Tags
go
react
server-sent event

롱 폴링, 웹 소켓, SSE

폴링(Polling)

클라이언트가 정기적인 간격으로 서버에 반복적으로 데이터를 요청하는 방식이며 매번 새로 서버와 연결을 맺는 방식이다.
 
async function poll() { try { const response = await fetch("http://localhost:8080/data"); const data = await response.json(); // Do something with data } catch (error) { console.error("Fetch error:", error); } setTimeout(poll, 1000); // Schedule the next poll }
 
기본적으로 서버에서 push하는 방식이 아닌 client에서 pull하는 방식이고, 데이터가 언제 준비될지 모르기 때문에 주기적으로 API call을 하는 구조다.

롱 폴링 (Long Polling)

폴링과 유사하지만 새 데이터를 사용할 수 있을 때까지 서버와의 연결을 열려있는 상태로 유지하는 방법이다.
 
  1. 클라이언트에서 서버로 요청이 전송된다
  1. 서버는 커넥션을 닫지 않고 보낼 메시지가 준비될 때 까지 유지한다.
  1. 메시지가 준비되면 서버는 메시지를 전송하고 연결이 닫힌다.
  1. 클라이언트는 새 리퀘스트를 요청한다.
 

웹 소켓 (Web Socket)

클라이언트-서버 간 양방향 연결 (full-duplex) 통신 채널을 연결하는 방식이며, 브라우저와 서버가 자유롭게 데이터를 주고받을 수 있다. 한 번 연결을 맺고 나면 클라이언트와 서버가 독립적으로 데이터를 전송할 수 있다.
 
소켓이 끊어질 경우 다시 생성해야 하고, 연결이 여전히 유효한지 체크하는 로직 또한 구현이 필요하다.
 
const socket = new WebSocket('ws://example.com'); socket.onopen = function(event) { console.log('socket is opened'); // Send message to server socket.send('Hello from client'); }; socket.onmessage = function(event) { console.log('Hello from server', event.data); };
 

Server-sent Event

서버 업데이트를 HTTP를 통해 클라이언트로 푸시하는 표준. 웹 소켓과 달리 단방향 통신이다. SNS의 뉴스피드 등 클라이언트에 실시간으로 데이터를 업데이트 해줘야 하지만 클라이언트가 서버로 데이터를 보낼 필요는 없는 환경에서 사용한다.
 

구현해보기

React/Next.js로 프론트엔드를 구성하고 Go로 간단한 백엔드를 구성해 SSE를 구현해본다.
 
서버의 경우 메시지 전송 시 header를 SSE에 맞춰 작성해야한다.
{ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }
 

백엔드 서버

1초 단위로 timestamp를 전송하는 SSE 서버를 만들어본다.
package main import ( "fmt" "log" "net/http" "time" ) func main() { http.HandleFunc("/events", sseHandler) log.Println("Server started on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) } func sseHandler(w http.ResponseWriter, r *http.Request) { // Set headers for SSE w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") // Create a ticker to send events every second ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-r.Context().Done(): log.Println("Client closed connection") return case t := <-ticker.C: fmt.Fprintf(w, "data: %s\n\n", t.String()) w.(http.Flusher).Flush() } } }
 
커넥션이 닫히면 로깅을 하도록 해 커넥션이 닫혔는지 체크할 수 있게 했다.
 

웹프론트엔드

import { useEffect, useState } from "react"; export default function Home() { const [messaages, setMessages] = useState<string[]>([]); const [eventSource, setEventSource] = useState<EventSource | null>(null); useEffect(() => { const es = new EventSource("http://localhost:8080/events"); setEventSource(es); es.onmessage = (event) => { setMessages((prev) => [...prev, event.data]); }; es.onerror = (error) => { console.error(error); es.close(); }; return () => { es.close(); }; }, []); const handleStop = () => { if (eventSource) { console.log('Stopping EventSource'); eventSource.close(); } }; return ( <main className="flex min-h-screen flex-col items-center justify-between p-24"> <h1>SSE Client</h1> <button onClick={handleStop}>stop</button> <div className="flex flex-col w-full gap-4"> {messaages.map((message, index) => ( <p key={index}>{message}</p> ))} </div> </main> ); }
 
useEffect 훅에서 eventSource를 생성하고 state로 추가해 관리한다. “stop” 버튼을 두어 connection을 닫을 수 있게 했다.
 
메시지가 전송되면 es.onmessage를 통해 messages 배열에 메시지가 push되며 div 내에 p element로 메시지가 계속 추가된다.
 

SSE 정리

  • 커넥션이 끊어질 경우 자동으로 커넥션을 다시 맺도록 시도한다.
  • 웹소켓에 비해 배터리 소모량이 작다
    • 소켓의 경우 지속적으로 서버와 handshake를 하기 때문
 
위 예시 코드를 살펴보면 알 수 있지만 매우 간단히 구현 가능하다. 또, 커넥션이 끊기더라도 재연결을 자동으로 시도하기 때문에 구현이 편리함을 알 수 있다.
 
다만 단점도 존재한다.
  • GET 메소드만 지원하며 단방향 데이터 교환이다.
  • 커넥션을 계속 유지해야 하므로 여러 클라이언트가 동시에 연결을 유지한다면 서버의 부담이 커질 수 있다.
  • 브라우저는 하나의 서버와 맺을 수 있는 최대 커넥션 수를 제한하고 있고, 그 갯수 이하로만 SSE를 사용할 수 있다.
  • 자주 데이터를 주고 받는 환경에서는 웹소켓보다 데이터 송신이 느릴 수 있다.
 
상황에 따라 롱 폴링, 웹 소켓, server-sent event를 적절히 활용하는 지혜가 필요해 보인다.
 
참고로 브라우저에서 도메인 당 최대 연결 갯수는 HTTP 버전에 따라 상이하다.
  • HTTP 1.1: 최대 6개 (브라우저 탭 간에도 공유)
  • HTTP 2: 단일 연결에서 멀티플렉싱해 처리하므로 이론상 무한대 연결 가능하나, default 100으로 설정되어 있는 것이 일반적
 

참고자료