Never give up

React, Node - SSE example 본문

WEB

React, Node - SSE example

대기만성 개발자 2023. 3. 4. 00:31
반응형

기본적으로 우리가 사용하는 Rest API는 클라이언트에서 서버로 요청하는 형태로

 

단방향으로 클라이언트가 서버를 바라보는 형태인데

 

그 반대의 경우는 어떻게 처리해야될지 고민하다가 예전에 WebRTC문서를 읽을 때

 

SSE(Server Sent Event)에 관해 읽었던 기억이 있어서 찾아봤고, 약간 편법같지만 목적에 부합한다 생각해서

 

한번 예제를 만들어보게 되었습니다

 

시작하기 전에 무엇을 해결하기 위해 만들어졌을까 하는 생각을 하면서 찾아봤는데

 

1. 스포츠 중계처럼 서버가 클라리언트를 꾸준히 업데이트 해주지만 양방향 통신이 필요 없을 때

2. 알람을 줄 때

3. 서버가 특정 상황을 기다린 후 데이터를 전송해야될 때

 

이부분을 해결하기 위해

 

polling( + long polling), socket, sse 등을 사용하게 될텐데

 

각각 장단점이 있으니 필요하신 분들은 한번 검색해보시길 바랍니다

(구글에 치기만 해도 잘 정리되어있는 블로그가 너무 많아서 링크는 따로 없습니다..)

 

서버 부분을 먼저 보면

 

src/api/sse_api.js

const router = require('express').Router();
const SSEController = require('../controllers/sse_controller');

const controller = new SSEController();

router.get('/start', (req, res) => controller.subscribe(req, res));

router.get('/stop', (req,res) => controller.unsubscribe(res))

module.exports = router;

먼저 사용한 api는 2가지로

 

/sse/streaming/start (get) : subscribe 시작

/sse/streaming/stop (get) : subscribe 종료

 

src/controller/sse_controller.js

module.exports = class SSEController {
    constructor() {
        this.cancelStreaming = false;
    }

    subscribe(req, res) {
        const headers = {
            'Access-Control-Allow-Origin': '*',
            'Content-Type': 'text/event-stream',
            'Connection': 'keep-alive',
            'Cache-Control': 'no-cache'
        };

        res.writeHead(200, headers);

        this._changeData(res, 1, 10);
    };

    unsubscribe(res) {
        this.cancelStreaming = true;

        res.send(`<html><body>Streaming is cancelled.</body></html>`);
    }

    _changeData(res, id, count) {
        if (this.cancelStreaming) {
            res.end();
            this.cancelStreaming = false;
            return;
        }

        console.log('[sse] changeData', { id });

        res.write(`id: ${id}\n`);
        res.write('event: message\n');
        res.write(`data: ${this._generateNumber()}\n\n`);
        if (count > 1) {
            const timer = setTimeout(() => {
                this._changeData(res, id + 1, count - 1);
                clearTimeout(timer);
            }, 1000);
        } else {
            res.write(`id: N/A\n`);
            res.write('event: message\n');
            res.write(`data: finished\n\n`);
            res.end();
        };
    }

    _generateNumber() {
        const min = 10;
        const max = 99;

        return Math.floor(Math.random() * (max - min + 1)) + min;
    }
};

먼저 subscribe부분을 보면 header에

 

content-type : text/event-stream, connection : keep-alive 옵션을 주는데

 

일반 Rest API와 달리 단발성이 아닌 연속적으로 데이터를 보내기 때문에 사용하는 옵션입니다

(참고로 sse는 http 프로토콜을 사용합니다)

 

그리고 _changeData 메소드 부분을 보면 res.write부분에 특정 데이터를 밀어넣는데

 

형식을 맞춰줘야 정상적으로 작동합니다

id : blah
message : message
data : blah

형태로 말이죠

 

다음으로 클라이언트에서 받을 때

 

EventSource라는 클래스를 이용하면 간단하게 구현 가능합니다

(링크 : https://developer.mozilla.org/en-US/docs/Web/API/EventSource)

 

src/components/chart/chart_componenet.tsx

import React, { useEffect, useRef, useState } from 'react';
import CommonBtn from '../common_btn/common_btn';
import './chart.css';
import CustomChart, { chartType } from './custom_chart';

type ChartProps = {
    width?: string;
};

const ChartComponenet: React.FC<ChartProps> = ({ width }) => {
    const [isStarted, setIsStarted] = useState(false);

    const es = useRef<EventSource>();

    let chart: CustomChart;

    useEffect(() => {
        const model = {
            chartTitle: '테스트',
            type: chartType.line,
            data: { labels: [], data: [] }
        };

        chart = new CustomChart(model);

        return () => {
            chart?.dispose();
        };
    }, []);

    useEffect(() => {
        es.current = new EventSource("http://localhost:8080/sse/streaming/start");

        console.log({ es });

        es.current.onopen = (e) => {
            setIsStarted(true);
            console.log('[sse] open', { e });
        };

        es.current.onmessage = (event) => {
            console.log('[sse] message', { event });

            if (event.data === 'finished') {
                es.current?.close();
                return;
            }
            updateChart(event.data);
        };

        es.current.onerror = (err) => {
            console.log('[sse] error', { err });
        };

        return () => {
            unsubscribe();
        };
    }, []);

    const updateChart = (data: any) => {
        chart.updateChart(data);
    };

    const unsubscribe = async () => {
        es.current?.close();
        await fetch('http://localhost:8080/sse/streaming/stop');
    };

    return (
        <div>
            <div id='div_chart' style={{ '--width': width } as React.CSSProperties} />
            {isStarted && <CommonBtn margin='20px auto 0 auto' onClick={() => unsubscribe()}>stop</CommonBtn>}
        </div>
    );
};

export default ChartComponenet;

useEffect가 2개가 있는데 첫번째는 cdn으로 chart 가져오는 부분

 

그리고 EventSource를 불러오는 부분입니다

 

셋팅은 소켓과 비슷한데

 

connect, message, error 만 정의해서 사용했습니다

 

message부분에서 데이터를 받아와서 chart를 업데이트 시켜주는 형태 입니다

 

< 구현된 화면 및 데이터 >

개발자 툴에서 network탭에 보면 type에 웹소켓(ws)과 Rest API(xhr, fetch)과 달리 eventsource라 나오고

 

데이터가 들어오는것을 확인할 수 있습니다

 

(전체 소스 링크 : https://github.com/devmemory/sse_example)

 

매달 포스트를 하다가 2달 건너뛰고 간만에 포스팅을 하게되었는데..

 

이전 회사 퇴사 후 해외여행을 다녀오느라 포스팅 할 시간도 애매했고.. 주제도 거의 고갈돼서 못했었는데

 

다시 열심히(?) 포스팅 할 예정입니다

반응형

'WEB' 카테고리의 다른 글

React - localization without library  (2) 2023.11.18
React - image resize example  (0) 2023.07.16
React - Axios common settings  (0) 2022.12.21
React - html to doc (feat.quill2)  (0) 2022.11.15
React - Drag & Drop file  (0) 2022.08.20
Comments