Notice
Recent Posts
Recent Comments
관리 메뉴

즐겁게, 코드

Server-Sent Event로 데이터 구독하기 본문

🎨 프론트엔드

Server-Sent Event로 데이터 구독하기

Chamming2 2024. 4. 27. 19:02

웹 서비스를 사용하다 보면 때때로 '이 기능은 어떻게 구현한 걸까?' 라는 의문이 들 때가 있지 않나요?

 

저는 최근 인스타그램을 사용하다가 "내가 올린 게시물에 좋아요가 달리면, 어떻게 실시간으로 알림이 오는 걸까?" 라는 궁금증을 가지게 되었는데요, 오늘 다룰 Server-Sent Event (SSE) 라는 키워드가 약간의 해답이 되었던 것 같아요.

 

그런데 ‘서버에서 보내는 이벤트’ 라는 명칭만 들어서는 이게 어떤 기술인지 금방 알아차리기 어려울 수 있는데요,
Server-Sent Event에 대해 더 이해할 수 있도록 이번 글을 작성해 보았습니다.

목차

  1. Server-Sent Event란?
  2. Server-Sent Event 구현하기
  3. SSE를 응용해 친구 초대 알람 만들어보기

1. Server-Sent Event (SSE) 란?

  • 주어진 API에 요청을 보내고, 요청의 응답을 출력하고 있는 간단한 예시에요.
const { data } = await axios("https://jsonplaceholder.typicode.com/todos");
console.log(data);

프론트엔드 또는 백엔드 개발을 경험해본 분이시라면 한번쯤은 요청 - 응답 형태의 HTTP 통신을 구현해본 적이 있을 거에요.
그런데 요청 - 응답 모델은 가장 단순하면서도 강력한 방법이지만, 실시간성이 필요한 경우에는 적합하지 않은 경우가 종종 있습니다.
예를 들어 기획자가 이런 요청사항을 요청한다면 어떨까요?

👩🏻‍💼 (SNS 서비스 기획자) : “친구 추가” 요청이 들어왔을 때, 실시간 알람과 함께 UI에 반영해 주세요!

 

저는 두 가지 방법이 떠오르네요.

방법 1. 롱 폴링 사용하기

  • 특징 : 일정 주기마다 HTTP 요청을 보내고, 응답이 올 때까지 기다린다.
    • 장점 : 다른 프로토콜에 대한 지식이 필요하지 않고, 구현이 비교적 간편하다.
    • 단점 : 서버에서 보낼 내용이 없을 때도 계속해서 요청을 보내야 한다.

방법 2. 웹소켓 사용하기

  • 특징 : 웹소켓 프로토콜로 클라이언트와 서버 간 커넥션을 유지한다.
    • 장점 : 클라이언트와 서버가 실시간으로 데이터를 주고받는 상황에 효과적이다.
    • 단점 : 구현이 상대적으로 복잡하고, 웹소켓 프로토콜에 대한 이해가 필요하다.

롱 폴링과 웹소켓 모두 서버에서 내려주는 응답을 실시간으로 구독해야 할 때 고려해볼 수 있는 방법이지만, 이런 상황에서는 오늘 다룰 SSE가 효율적일 수 있습니다.


  1. 서버에서 실시간으로 데이터를 수신해야 할 때
  2. 클라이언트에서 데이터를 전송할 필요 없이, 데이터를 수신하기만 하면 될 때

SSE는 웹소켓보다 구현이 간단하면서 롱 폴링보다 서버 자원을 덜 소모하는데요, 서버와의 연결은 유지한 채 서버에서 보내주는 데이터를 스트림 형태로 받아볼 수 있기 때문에 위와 같은 상황에 적절합니다.

2. Server-Sent Event 구현하기

SSE를 요약하면 "실시간성을 제공하면서 웹소켓보다 단순한" 방법이 될 것 같은데요, 이제 한번 코드를 작성해 볼까요?
Node.js와 리액트를 사용해 간단한 서버와 클라이언트를 구현하는 코드를 작성해 보겠습니다.

💡 글을 작성하다 보니 코드가 길어지게 되어, 완성된 코드를 미리 올려두었어요.
다소 번거로우시더라도 아래 링크에서 완성된 코드를 참고해주시면 감사드려요.

링크 : https://github.com/C17AN/sse-example

2. 1. 서버 준비하기

Node.js와 Express.js로 간단한 서버를 준비해 보겠습니다.

// index.js
const express = require("express");
const cors = require("cors")
const app = express();

const PORT = 5500;

app.use(cors())

app.listen(PORT, () => {
    console.log(`Server is running at ${PORT}`)
})

위와 같이 간단한 코드를 작성한 다음, 아래 커맨드로 서버를 시작할 수 있습니다.

npm install
node index.js

서버가 실행되었다면 SSE 요청을 처리할 수 있는 경로를 만들어줘야 할 텐데요, SSE로 데이터를 전송할 때 몇 가지 지켜야 할 규칙이 있습니다.


SSE를 구현할 때 지켜져야 할 형식

  1. 응답 헤더의 Content-Type 속성은 text/event-stream 으로 설정되어야 합니다.
  2. SSE로 전송되는 데이터는 반드시 텍스트여야 하며, 항상 “data:” 로 시작해야만 합니다.
  3. SSE로 전송된 데이터의 끝에는 반드시 끝을 알리는 "\n\n" 를 붙여주어야 합니다.
  • (예시 : "data: ${JSON.stringify(데이터)}\n\n")

이제 SSE 요청을 처리할 수 있는 첫 번째 경로를 만들어 보겠습니다.
아직은 실습 단계니, 예시로 3초마다 현재 시각을 전송하는 간단한 로직을 추가해 보겠습니다.

// index.js
const express = require("express");
const cors = require("cors")
const app = express();

const PORT = 5500;

app.use(cors())

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

    const intervalId = setInterval(() => {
        const date = new Date();
        // 1. SSE로 전송할 데이터는 반드시 텍스트 형식이어야 합니다.
        // 2. 데이터의 끝은 \n\n로 구분되어야 합니다.
        const data = `data: ${date.toISOString()}\n\n`;

        // res.send()가 아닌 res.write() 메서드를 사용하도록 주의해 주세요.
        res.write(data);
    }, 3000);
})

app.listen(PORT, () => {
    console.log(`Server is running at ${PORT}`)
})

 

2. 2. 클라이언트 준비하기

이제 서버에서 전송한 데이터를 수신할 수 있도록 클라이언트를 구현할 차례입니다.
서버에서 text/event-stream 이라는 특별한 헤더 값을 사용했던 것처럼 클라이언트에서도 특별한 방법을 통해 요청을 보내야 하는데요, 바로 eventSource 객체를 사용한 방법입니다.

// ❌ : 일반 HTTP 요청을 보내는 예시
axios.get("http://localhost:5500/friend/check-request")

// ✅ : EventSource 객체를 사용해 SSE 요청을 보내는 예시
new EventSource("http://localhost:5500/friend/check-request")

이번 예제에서는 리액트를 사용하고 있기 때문에 SSE 요청을 보내는 간단한 훅을 작성해 보겠습니다.

import { useEffect } from "react";
import ENDPOINT from "../constant/api";

export const useCheckFriendRequest = () => {
  useEffect(() => {
    const eventSource = new EventSource(
      `${ENDPOINT.BASE}${ENDPOINT.CHECK_FRIEND_REQUEST}`
    );

    /*
      onmessage 리스너를 작성하면 SSE 요청의 응답을 다룰 수 있습니다.
    */
    eventSource.onmessage = (message) => {
      console.log(message.data);
    };

    /*
      EventSource 객체는 자체적으로 에러를 throw하지 않기 때문에 try-catch의 영향을 받지 않습니다.
        따라서,  리스너를 작성해야 SSE 요청이 실패했을 때의 상황을 다룰 수 있습니다.
    */
    eventSource. = (error) => {
      eventSource.close();
    };

    /*
      Graceful close를 위해 컴포넌트 언마운트 시점에 연결을 종료해 주세요.
    */
    return () => {
      eventSource.close();
    };
  }, []);
};

위에서 만든 훅을 리액트 프로젝트의 아무 컴포넌트에서나 불러와 보면 요청이 수행되는 것을 확인할 수 있어요.

3. SSE 형태 살펴보기

SSE 요청을 보내게 되면 어떤 형태로 응답이 돌아오는지 크롬 개발자 도구를 통해 살펴보겠습니다.

먼저 요청의 유형이 fetch나 XHR이 아닌 eventsource로 설정되어 있고, 요청 헤더와 응답 헤더의 Content-Type 속성은 text/event-stream 으로 설정된 것을 확인할 수 있습니다.

 

흥미로운 점은 일반적인 HTTP 요청을 보내면 “Response” 라는 탭이 존재해야 하는데 SSE는 “EventStream” 이라는 탭이 존재하는 모습인데요, “EventStream” 탭을 확인해 보면 서버에서 전송한 데이터와 타임스탬프가 존재하는 것을 확인할 수 있습니다.

Untitled.png

3. SSE를 응용해 친구 초대 알람 만들어보기

이제 SSE가 무엇인지는 어느 정도 알았으니, 기획자의 요청을 들어주러 가볼까요?

시나리오 : 유저 A가 유저 B에 '친구 초대' 를 보내면 유저 B에게 알람을 보내주는 로직 구현하기

SSE를 알기 전까지는 이런 로직을 구현해야 했을 거에요.

  • 유저 A가 서버에 “친구 초대” 요청을 보낸다.
  • 서버에서 해당 요청을 검증하고 유저 A의 요청에 대한 응답을 보낸다.
  • 유저 B는 N분마다 요청을 보내 새로운 친구 요청이 있는지 확인한다.

이제 우리는 SSE가 무엇인지 알고 있기 때문에, 새로운 방법을 시도해볼 수 있어요.

  • 유저 A가 서버에 “친구 초대” 요청을 보낸다.
  • 서버에서 해당 요청을 검증하고 유저 A의 요청에 대한 응답을 보낸다.
  • 유저 B에게 친구 초대 정보가 포함된 데이터를 SSE로 보낸다.

3. 1. Express.js로 구현한 예시

친구 요청에 사용하는 /friend/request API로 요청이 들어오면 이벤트를 발생시켜 클라이언트에게 SSE로 데이터를 내려줄 수 있도록 작성한 코드에요.

// index.js
app.post("/friend/request", (req, res) => {
  // 친구 초대를 처리하는 로직
  // ....

  // 친구 초대 처리를 마치면 SSE 응답을 전송하는 이벤트를 발생시킨다.
  eventEmitter.emit("newFriendRequest", {
    friendName: req.body.friendName,
  });
  res.send("친구 요청이 전송되었습니다.");
});

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

  // 초대를 보낸 친구의 이름을 클라이언트에 전송한다.
  const sendEvent = (e) => {
    res.write(`data: ${JSON.stringify({ friendName: e.friendName })}\n\n`);
  };

  eventEmitter.on("newFriendRequest", sendEvent);
});

결과

  • 좌측은 유저 A, 우측은 유저 B의 화면이라고 가정하고 제작한 예제에요.
  • 유저 A가 "친구 신청하기" 버튼을 눌렀더니, 유저 B의 화면에서 실시간으로 알림이 노출되는 모습이에요.

화면 기록 2024-03-31 23.20.09.gif
마치며

만약 SSE를 모르는 상황이었다면 우리는 웹소켓과 롱 폴링, 또는 새로운 방법을 찾아야 했을 텐데요, 이번 글을 통해 비슷한 상황에서 여러분들이 고를 수 있는 선택지가 늘어났으면 하는 바램이 있어요.


"아는 것이 힘이다" 라는 격언처럼, 앞으로도 여러분들에게 힘이 되어줄 수 있는 다양한 기술들을 더 들고 찾아올게요.
그럼, 즐거운 코딩 하세요!

반응형
Comments
소소한 팁 : 광고를 눌러주시면, 제가 뮤지컬을 마음껏 보러다닐 수 있어요!
와!! 바로 눌러야겠네요! 😆