React SSE 실시간 알림 구현하기

회고록/일정 프로젝트
블로그 이미지

이챙(leechaeng)

﹒2024. 2. 20.

😃

이번 프로젝트에서 알림 기능이 있었다.
그래서 찾아보니 SSE로 구현을 해야한다는것. SSE라는걸 알림 구현하면서 첨 알게되서 정리를 해보았다!!
일단 서버와의 실시간 통신을 위해 살펴봐야할 기술 3가지가 있다.

 

실시간 통신

1. websoket

클라이언트와 서버와의 양방향 통신이 가능.
기존 HTTP 통신과 달리 클라이언트와 서버와의 지속적인 연결을 통해 데이터를 주고 받을 수 있다. ws 프로토콜을 통해 웹소켓 포트에 접속해 있는 모든 클라이언트(subscribe)에게 이벤트 방식으로 응답한다
처음 연결시 한번의 핸드셰이크만 수행하면 되기 때문에 통신 오버헤드가 낮다.

 

2. polling

클라이언트가 주기적으로 서버에게 request를 날려 응답을 받는 방식.
일정하게 요청을 보내므로 서버에 데이터가 업데이트가 되지 않았는데도 클라이언트가 계속 요청을 보낼수도 있기 때문에 빠른 응답을 얻기 어렵다.
주기적인 요청으로 오버헤드가 발생.

 

3. SSE(Server-Sent Events)

서버가 클라이언트에게 데이터를 푸쉬 할 수 있다. 단방향임.
클라이언트가 요청을 보내면 장기적으로 서버와 연결할 수 있다.
HTML5 표준안이라 가볍다.

1. 클라이언트가 서버에 요청
2. 클라이언트와 서버가 연결되면 통신이 열린 상태로 유지.
3. 서버에서 데이터가 업데이트 되면 클라이언트에게 응답이나 이벤트를 보낸다.

 

 

그렇다면 🔔실시간 알림🔔은 어떤걸 사용해야 할까..?

실시간 알림 같은경우 단방향이다. 서버에서 데이터가 업데이트 될시 업데이트 된 알림들을 클라이언트에게 보여주면 된다.
그래서 나도 SSE를 사용해 실시간 알림을 구현했다.

 

구현하기

  const EventSource = EventSourcePolyfill || NativeEventSource;
  const eventSource = useRef<null | EventSource>(null);
  
  eventSource.current = new EventSource(
    `${import.meta.env.VITE_SERVER}/notification/subscribe`,
    {
      headers: {
        Authorization: `Bearer ${localStorage.getItem(ACCESS_TOKEN)}`
      },
      // heartbeatTimeout: 3600000, // 서버에서 1시간 제한
      withCredentials: true
    }
  );

일단 나같은 경우 서버에 요청시 헤더에 토큰을 보내야 하기 때문에 EventSourcePolyfill로 호환성을 해결해줬다.
일부 브라우저가 header를 지원하지 않거나 올바르게 데이터를 처리하지 못하는 경우가 발생해 EventSourcePolyfill로 호환을 해줘야 한다고 함.

  eventSource.current.addEventListener('sse', () => {
    // e: MessageEvent
    // const res = await e.data;
    // const parsedData = JSON.parse(res);
    setRealtimeData(true);
  });

  eventSource.current.onerror = async () => {
    // e: Event
    eventSource.current?.close();
    // 재연결
    setTimeout(fetchSSE, 3000);
  };
  eventSource.current.onopen = () => {
    // console.log('연결', event.data);
  };

onopen: 이벤트가 연결 되었을때
onerror: 이벤트에 대한 오류 발생
onmessage: 이벤트에대한 응답

원래 처음에 응답처리를 onmessage로 해놨었다.
근데 응답이 안왔다.....😰 그래서 여기서 엄청 헤맸음;;;
근데 알고보니 백엔드쪽에서 알림 응답과 연결에대한 이벤트 이름을 따로 설정해놨었다.
백엔드 담당하시는 분이 이부분에 대해 따로 말씀을 안해주셔서...;;;;; 왜 안오지 왜 안오는걸까요 라고 계속 물어봤었던듯;
만약 백엔드에서 이벤트 이름을 따로 설정하지 않았다면 onmessage나 onopen으로 해도 되는거 같고 이름을 설정을 해놨다면 클라이언트에도 따로 이벤트 유형에대한 리스너 처리를 해줘야 한다고 함 (
참고)
그래서 응답받아오는 이벤트는 백엔드에서 설정한 이름으로 따로 설정해줬다

저기서 이제 응답이오면 그에 대한 프론트 로직을 짜주면 된다.

 

오류 사항

연결하고 1초..? 후 No activity 어쩌구~ 에러가 자꾸 나왔다.
찾아보니 polyfill 사용하는 경우 default timeout이 45초기 때문에 백엔드에서 그보다 크게 timeout을 설정한 경우 45초마다 재연결을 시도할 가능성이 있어서 hearteatTimeout 옵션 조정이 필요하다고함.
나는 해결법을 뒤늦게 알아서,,;; 현재 코드에는 에러 발생시 재연결을 해줬다.

 

전체코드

const Alarm = () => {
  const [realtimeData, setRealtimeData] = useState(false);

  const EventSource = EventSourcePolyfill || NativeEventSource;
  const eventSource = useRef<null | EventSource>(null);

  useEffect(() => {
    const fetchSSE = () => {
      eventSource.current = new EventSource(
        `${import.meta.env.VITE_SERVER}/notification/subscribe`,
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem(ACCESS_TOKEN)}`
          },
          // heartbeatTimeout: 3600000, // 서버에서 1시간 제한
          withCredentials: true
        }
      );
      eventSource.current.addEventListener('sse', () => {
        // e: MessageEvent
        // const res = await e.data;
        // const parsedData = JSON.parse(res);
        setRealtimeData(true);
      });

      eventSource.current.onerror = async () => {
        // e: Event
        eventSource.current?.close();
        // 재연결
        setTimeout(fetchSSE, 3000);
      };
      eventSource.current.onopen = () => {
        // e: Event
        // console.log('연결', event.data);
      };
    };
    fetchSSE();
    return () => {
      eventSource.current?.close();
    };
  }, []);
  return (
    <>
      <Link
        to="/alarm"
        className="w-14 h-full flex justify-center items-center absolute right-0 top-0"
      >
        <div className="relative">
          <FaRegBell className="text-xl text-bk" />
          {realtimeData && (
            <span className="absolute -top-1 -right-2 w-2 h-2 bg-red-500 rounded" />
          )}
        </div>
      </Link>
      {/* <button onClick={fetchData} type="button">
        요청
      </button> */}
    </>
  );
};
export default Alarm;

 

 

 

참고

https://velog.io/@wwlee94/Redis-PubSub-Base-Server-Sent-Event

 

Redis Pub/Sub 기반 SSE(Server-Sent Events) 실시간 알림 적용기

이 글에서는 서비스 이용자에게 알림을 제공하기 위해 30초마다 주기적으로 서버에 API 호출을 하여 데이터를 받아오던 Polling 방식에서 SSE (Server-Sent-Event) 를 활용하여 실시간 알림 기능을 구현하

velog.io

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-Polling-Long-Polling-Server-Sent-Event-WebSocket-%EC%9A%94%EC%95%BD-%EC%A0%95%EB%A6%AC

 

🌐 Polling / Long Polling / Server Sent Event / WebSocket 정리

서버의 event를 클라이언트로 보내는 4가지 방법 polling 클라이언트가 평범한 http request를 서버로 계속 날려서 이벤트 내용을 전달받는 방식이다. 가장 쉬운방법이지만 클라이언트가 계속적으로 re

inpa.tistory.com

https://dev.to/karanpratapsingh/system-design-long-polling-websockets-server-sent-events-sse-1hip

이챙(leechaeng)
이챙(leechaeng)

프론트엔드 개발도 하고 뛰기도 하고

'회고록/일정 프로젝트' 카테고리의 관련 글