React Ts에서 SSE 구현하기

오찬주·2025년 3월 9일

개발 log

목록 보기
16/23
post-thumbnail

웹에서 실시간 기능을 구현할 때가 종종 있다.
이때 우리는 주로 웹 소켓을 고려하게 된다.
지난 해커톤에서 프로젝트를 하면서 SSE를 알게 되어 둘이 차이점과 사용 방법을 알아보고자 한다.

WebSocket

웹소켓은 서버와 클라이언트 양방향으로 통신이 가능하다.
프로토콜은 websocket 프로토콜이 따로 있다. (ws:// or wss://)

특징

  • 양방향 통신이 가능하다. (클라이언트 <-> 서버)
  • 연결을 한 번 맺으면 지속적으로 데이터 교환 가능하다.
  • 서버와 클라이언트 간의 지연 시간이 짧다. 즉, 빠른 실시간 데이터 송수신이 가능하다.
  • HTTP보다 오버헤드가 적고 성능이 뛰어나다.
  • 브라우저 및 다양한 플랫폼에서 지원한다.

예를 들어 실시간 채팅, google docs 같은 협업 편집 도구 등에서 사용한다.

SSE

Server-Sent Events로, 서버 -> 클라이언트의 단방향이다.
프로토콜은 HTTP 기반으로 EventSource APIfetch() + ReadableStream를 사용한다.

특징

  • 서버에서 클라이언트로 자동 업데이트를 푸시하는 방식이다.
  • 클라이언트는 이벤트 스트림을 계속 열어두고 서버의 데이터를 수신한다.
  • 클라이언트에서 서버로는 메시지를 보낼 수 없다.

뉴스 업데이트, 실시간 알림 등에서 사용할 수 있다.

💡 EventSource API
사용 방식: const eventSource = new EventSource(url);
자동 재연결이 기본적으로 지원된다.
헤더 설정은 기본적으로 불가능하다.
CORS제한은 서버가 ccess-Control-Allow-Origin을 설정해야 사용 가능하다.
EventSource.onmessage, EventSource.onerror 이벤트 기반이다.

💡fetch() + ReadableStream
사용 방식: fetch(url).then(res => res.body.getReader().read())
직접 setTimeout()으로 재연결해야 한다.
fetch()를 사용하는 방식이기에 Authorization 등 헤더 설정이 가능하다. 또한, CORS 설정이 가능하고, 더 세밀한 제어가 가능하다.


나는 어떤 방식으로 구현했을까?

우리 팀이 필요한 기능은 친구 요청에서 필요한 실시간 알림이었다.
그렇기에 굳이 양방향인 웹소켓보다 SSE가 적절하다고 판단했다.

또한, 친구 요청은 JWT 토큰 인증이 필요했기에 EventSource API 대신 fetch() + ReadableStream을 사용해서 SSE를 구현했다. (로그인한 사용자만 가능했기에!)

👩🏻‍💻 코드 개요

  • SseNotification 컴포넌트를 만들고, 서버에서 보내는 알림을 받아서 처리하는 역할을 하도록 했다.
  • JWT 토큰을 사용해 서버에 인증된 SSE 연결을 설정하고, 데이터를 실시간으로 읽는다.
  • 특정 패턴을 포함한 메시지를 수신하면 모달을 표시한다.
  • SSE 연결이 끊어지면 자동으로 재연결하는 로직도 구현한다.

__1. useEffect로 SSE를 연결한다.

useEffect(() => {
  if (!TOKEN) {
    console.error("JWT 토큰이 없습니다.");
    return;
  }

  connectSSE();

  return () => {
    console.log("SSE 연결 해제");
    if (readerRef.current) {
      readerRef.current.cancel();
    }
  };
}, [TOKEN]);

이때 TOKEN은 로컬 스토리지에서 getItem을 해온다.
컴포넌트가 마운트(useEffect)될 때 SSE 연결을 시작하고, 언마운트 시 연결을 해제한다.
readerRef.current.cancel(); → 연결 해제 시 스트림 리더를 정리하는 코드다.

connectSSE: 서버와 연결

const connectSSE = async (): Promise<void> => {
  console.log("SSE 연결 시도");

  const attemptConnection = async (): Promise<void> => {
    try {
      const response = await fetch(
        `${import.meta.env.VITE_API_BASE_URL}/api/connect`,
        {
          method: "GET",
          headers: {
            Authorization: `Bearer ${TOKEN}`,
            "Content-Type": "application/json",
          },
          credentials: "include",
        }
      );

      if (!response.ok) {
        throw new Error(`서버 응답 실패: ${response.status}`);
      }
  • fetch()를 사용해 SSE 스트림을 요청하고, JWT 토큰으로 인증을 수행한다.
  • Vite 환경 변수에서 API URL을 가져와 사용한다.

서버에서 받은 메시지 읽기

const reader = response.body?.getReader();
if (!reader) {
  throw new Error("스트림 리더를 가져올 수 없습니다.");
}

readerRef.current = reader;
const decoder = new TextDecoder();

while (true) {
  const { value, done } = await reader.read();
  if (done) break;

  const text = decoder.decode(value, { stream: true }).trim();
  console.log("[MESSAGE] 새 알림 도착:", text);
  setHasNotification(true);
  • 서버에서 데이터를 받으면 read()로 읽고, TextDecoder를 이용해 문자열로 변환한다.
  • setHasNotification(true)로 알림이 도착했음을 상태 업데이트한다.

연결 실패 시 3초 후 재연결한다

} catch (error) {
  console.error("❌ SSE 연결 실패, 3초 후 재시도");
  setTimeout(() => {
    connectSSE();
  }, 3000);
}

이런식으로 Sse를 구현할 수 있다.


결론

SSE -> 단방향 업데이트(서버 -> 클라이언트)가 필요한 경우
WebSocket -> 양방향 실시간 통신이 필요한 경우

WebSocket이 더 강력한 기능을 제공하지만, 단순한 서버 푸시 업데이트라면 SSE가 더 간편하고 효율적일 수 있다.

각 특징을 고려해 본인 프로젝트에 어떤걸 선택할지 고르는게 좋을 것 같다 !!

profile
프론트엔드 엔지니어를 희망합니다 :-)

0개의 댓글