[React] SSE (Server-Sent Events) 실시간 알림, Header에 값 담기

2023. 7. 4. 23:13개발/React

 

SSE를 통해서 실시간 알림을 구현해야 했다. 많은 웹사이트에서 확인 가능한 기능이며, 작은 부분이지만 구현하기는 꽤 어려웠다.
특히 서버쪽에서 헤더에 토큰을 담아서 보내달라고 하셨는데 EventSource 객체는 헤더에 접근 권한이 없기 때문에 다른 방법을 사용해야 했다. 또 컴포넌트가 랜더링 되었을때 최초 한번만 구독요청을 보내고 유지하도록 하는것이 중요했다.

SSE란

SSE(Server-Sent Events)는 클라이언트(또는 브라우저)에 메시지를 받는 방법으로, 클라이언트와 서버 간에 초기 연결이 설정되면 서버가 메시지를 클라이언트로 전달할 수 있게 된다. 이때 한 방향으로만 (서버 to 클라이언트) 작동합니다. 한번 연결이 설정되면 이벤트가 발생하면 클로즈 하기 전까지 서버에서 데이터를 넘겨준다. 따라서 Server-Sent Events는 대화형 작업이 필요하지 않은 실시간 데이터를 처리할 때 적합하며, 양방향을 계속 데이터를 주고 받는 웹 소캣 보다 실시간 알람 구현에 적합하다.

헤더에 값 담기 with EventSourcePolyfill

기존 코드

  1. Web API인 Server-Sent Events에서 기본으로 제공하는 EventSource 인터페이스 객체를 그대로 사용해서 구현하였다. 일반적으로 사용 가능한 방법이며, 해당 방법으로 코드를 작성할 경우 컴포넌트가 랜더링 되고나서 해당 useEffect가 실행되면서 정상적으로 코드가 실행된다.
  2. EventSource 인터페이스 객체를 생성할때 SSE 연결 처리를 하는 서버단 앤드포인트 주소를 포함하여 생성하여야 하는데 서버단에서 ACCESS_KEY를 검증한 다음 연결처리를 하기때문에 ACCESS_KEY가 제대로 담겨서 넘어가지 않아서 구독이 처리가 되지 않았다.
    // SSE
    useEffect(() => {
        // 로그인 상태일때 최초 한번만 구독 실행
        if(isLogin){
            console.log("[SSE] 구독요청")
            const eventSource = new EventSource(
                `${process.env.REACT_APP_SERVER_URL}/api/subscribe`, 
                {
                    headers : {
                        ACCESS_KEY : getCookie('token'),
                    },
                    withCredentials: true, // 토큰 값 전달을 위해 필요한 옵션
                }
            )
            eventSource.addEventListener('message', (event) => {
                const data = JSON.parse(event.data)
                console.log("[SSE] message ", data)
                // 메세지 응답 처리
            })

            return () => {
              eventSource.close(); // 컴포넌트 언마운트 시 SSE 연결 종료
            };
        }
    }, [isLogin]);

변경한 코드

  1. event-source-polyfill를 import 한 다음 EventSourcePolyfill를 EventSource로 선언을 하고 해당 이름으로 새로운 EventSource 객체를 생성한다.
  2. 이렇게 작성한 이유는 기존의 Server-Sent Events에서 기본으로 제공하는 EventSource 인터페이스 객체는 헤더의 접근 권한이 없기 때문에 헤더에 ACCESS_KEY를 담거나 할 수 없기 때문이다.
import { EventSourcePolyfill } from "event-source-polyfill";
/*... 중략 ...*/
    useEffect(() => {
        // 세션 스토리지에서 SSE 구독 상태를 확인
        const isSubscribed = sessionStorage.getItem('isSubscribed');

        if (isLogin && !isSubscribed) {
            // 로그인 상태일때 최초 한번만 구독 실행
            const subcribeSSE = async () => {
                const accessKey = await getCookie('token')
                const EventSource = EventSourcePolyfill
                if (isLogin && accessKey && !isSubscribed) {
                    eventSourceRef.current = new EventSource(
                        //헤더에 토큰
                        `${process.env.REACT_APP_SERVER_URL}/sse/subscribe`,
                        {
                            headers: {
                                'ACCESS_KEY': accessKey,
                            },
                            withCredentials: true, // 토큰 값 전달을 위해 필요한 옵션
                        }
                    )

                    if (eventSourceRef.current.readyState === 1) {
                        // console.log("[INFO] SSE connection 상태")
                    }

                    eventSourceRef.current.addEventListener('open', (event) => {
                        sessionStorage.setItem('isSubscribed', true);
                    })

                    eventSourceRef.current.addEventListener('message', (event) => {
                        const data = event.data
                        if (data.indexOf('EventStream Created') === -1) {
                            console.log("[INFO] 알람 발생", data)
                            dispatcher(__alarmSender(data))
                        }
                    })
                    return () => {
                        if (eventSourceRef.current && !isLogin) {
                            sessionStorage.setItem('isSubscribed', false)
                            //dispatcher(__alarmClean())
                            eventSourceRef.current.close() // 로그아웃 시 SSE 연결 종료
                        }
                    };
                }
            };
            subcribeSSE()
            avataGenHandler()
        }
    }, [isLogin]);

EventSource 인터페이스 객체 vs EventSourcePolyfill

  1. 차이
    1. EventSource 인터페이스
      1. IE를 제외한 대부분의 브라우저에서 지원하며 별도의 설치가 필요하지 않다.
      2. 헤더에 특정 값을 담는 메소드나 속성을 제공하지 않는다.
      3. https://developer.mozilla.org/ko/docs/Web/API/EventSource
    2. EventSourcePolyfill
      1. IE도 지원하는 라이브러리
      2. 헤더에 특정 값을 담을 수 있다
      3. https://www.npmjs.com/package/event-source-polyfill
  2. EventSourcePolyfill 설치하기
    npm install event-source-polyfill
    yarn add event-source-polyfill