๐ŸŒฟ Spring

Server-Sent Event (SSE)๋ž€? feat Node.js

์—ฐ_์šฐ๋ฆฌ 2022. 9. 20. 01:35
๋ฐ˜์‘ํ˜•

๋ชฉ์ฐจ



    ๊ธฐ์กด ํ”„๋กœ์ ํŠธ์—์„  ๋ฐฑ์—”๋“œ -> ํ”„๋ก ํŠธ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด์ค„๋•Œ ์›น์†Œ์ผ“์„ ์‚ฌ์šฉํ•˜์˜€๋‹ค.
    ์›น์†Œ์ผ“์€ ์–‘๋ฐฉํ–ฅ์ธ๋ฐ, ๊ตณ์ด ํ”„๋ก ํŠธ -> ๋ฐฑ์—”๋“œ๋ฐฉํ–ฅ์œผ๋กœ ์—ฐ๊ฒฐ๋˜์–ด์žˆ์„ ํ•„์š”๊ฐ€ ์—†์–ด์„œ
    ์ด๊ฒƒ์ €๊ฒƒ ์ฐพ์•„๋ณด๋‹ˆ SSE๋ฅผ ์•Œ๊ฒŒ๋˜์—ˆ๊ณ , ์‚ฌ์šฉํ•ด๋ณด์•˜๋‹ค!

    SSE๋ž€ ?

    SSE๋Š” ์„œ๋ฒ„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ŠคํŠธ๋ฆฌ๋ฐ ํ•˜๋Š” ๊ธฐ์ˆ ์ด๋‹ค.
    ๋ณ€๊ฒฝ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด ์ง€์†์ ์œผ๋กœ API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋™๊ธฐํ™”ํ•˜๋Š” ์ž‘์—…์„ ์—†์•จ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด๋‹ค!

    - ์›น์†Œ์ผ“์€ WSS ํ”„๋กœํ† ์ฝœ์„ ๋”ฐ๋กœ ์‚ฌ์šฉํ•˜์ง€๋งŒ
    SSE๋Š” HTTP๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋•Œ๋ฌธ์— ๋ณ„๋‹ค๋ฅธ ์„œ๋ฒ„ ์…‹ํŒ…์ด ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.
    - ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๊ฐ€ ์ตœ์ดˆ ํ•œ๋ฒˆ HTTP์—ฐ๊ฒฐ์„ ๋งบ์œผ๋ฉด ๊ทธ ๋’ค๋กœ ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ง€์†์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

    EX )
    ์„œ๋ฒ„์—์„œ ๊ฐ€๋”์”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์•ผํ•˜๋Š”๋ฐ, N์ดˆ๋™์•ˆ ๋ฐ์ดํ„ฐ๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค.
    ์‹ฌ์ง€์–ด N์ดˆ๋„ ๋งค๋ฒˆ ๊ฐ’์ด ๋ฐ”๋€๋‹คํ•˜์ž.

    API ํ˜ธ์ถœ : 1ํšŒ ํ˜ธ์ถœ์— 1ํšŒ ์‘๋‹ต๊ฐ’์„ ์ค„์ˆ˜์žˆ๋‹ค. ์ง€์†์ ์œผ๋กœ ๊ฐ’์˜ ๋ณ€ํ™”๋ฅผ ์•Œ๋ ค๋ฉด ๋งค ์ดˆ๋งˆ๋‹ค api๋ฅผ ํ˜ธ์ถœํ•ด์•ผํ•œ๋‹ค. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฃผ์ฒด๊ฐ€๋œ๋‹ค.

    websocket : ์†Œ์ผ“์„ ์…‹ํŒ…ํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๋ฅผ ์—ฐ๊ฒฐํ•œ๋‹ค. ํด๋ผ์ด์–ธํŠธ/์„œ๋ฒ„ ๋‘˜๋‹ค ์ฃผ์ฒด๊ฐ€ ๋  ์ˆ˜ ์žˆ๋‹ค.

    API์™€ WebSocket ์ค‘๊ฐ„์—์„œ ๊ฐ€๋ณ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด SSE์ด๋‹ค.
    SSE๋Š” ํด๋ผ์ด์–ธํŠธ์—์„œ 1ํšŒ ํ˜ธ์ถœํ•˜๋ฉด ์„œ๋ฒ„์—์„œ ์ง€์ •ํ•œ ์‹œ๊ฐ„๋งŒํผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด์ค„ ์ˆ˜ ์žˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์ฃผ์ฒด๊ฐ€๋œ๋‹ค.



    SSE๋ฅผ ๋…ธํ‹ฐ, ํ‘ธ์‹œ, pub/sub์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์ œ๊ฐ€ ์ข…์ข… ๋ณด๋ฉด์„œ
    ์„œ๋ฒ„์—์„œ ์ด๋ฒคํŠธ(ํ‘ธ์‹œ)๋ฅผ ์—ฌ๋Ÿฌ๋ฒˆ ๋ณด๋‚ด๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•œ๊ฑด๊ฐ€?! ํ•˜๊ณ  ํ˜ผ๋™๋˜์—ˆ๋‹ค.

    setInterval ๋‚ด๋ถ€์—์„œ ๋ถ„๊ธฐ์ฒ˜๋ฆฌํ•ด์„œ ํด๋ผ์ด์–ธํŠธ๋Š” ํ‘ธ์‹œ๋ฅผ ๋ฐ›๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ด์ง€๋งŒ
    ์„œ๋ฒ„์—์„œ๋Š” ๊ณ„์†ํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒดํฌํ•ด์•ผํ•˜๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

    ๋‚ด๊ฐ€ ์›ํ•˜๋Š”๊ฑด ๊ทธ๋ƒฅ send ํ–ˆ์„ ๋•Œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ์ดํ„ฐ๊ฐ€ ์ „์†ก๋˜๋Š” ๊ฒƒ์ด์˜€๋Š”๋ฐ..

    ์ฐพ์•„๋ณผ์ˆ˜๋ก ๋…ธํ‹ฐ, ํ‘ธ์‹œ๋Š” SSE์˜ ๋ชฉ์ ๊ณผ๋Š” ์ข€ ๋‹ค๋ฅด๊ฒŒ ์“ฐ์ด๋Š” ๊ฒƒ ๊ฐ™์€ ๋Š๋‚Œ์„ ๋ฐ›์•˜๋‹ค!





    ๋ฐ์ดํ„ฐ ํฌ๋งท

    id: testN1\n
    event: red\n
    data: {"message" : "hello SSE!", "text" : "blah-blah"}\n\n

    "\n" ๊ฐœํ–‰๋ฌธ์ž๋กœ ๊ฐ ํ•ญ๋ชฉ์„ ๊ตฌ๋ถ„ํ•œ๋‹ค. \n๋ฅผ ๋‘๋ฒˆ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ ์ „์†ก์˜ ๋์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.
    id๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋งˆ์ง€๋ง‰์— ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ๋ฅผ ์ถ”์ ํ•  ์ˆ˜ ์žˆ๋‹ค. (event.lastEventId)
    event๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด pub/sub์˜ ์ฑ„๋„์ฒ˜๋Ÿผ ๊ตฌ๋ถ„ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.


    test.html

    <html>
    <body>
        <script>
    
            //๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ URL์„ ์ž‘์„ฑํ•œ๋‹ค.
            const eventSource = new EventSource("http://localhost:3000/test", {withCredentials:false});
            
    
            //๋ธŒ๋ผ์šฐ์ €๊ฐ€ SSE์ง€์›ํ•˜๋Š”์ง€ ์ฒดํฌ
            if(typeof(EventSource) !== "undefined") {
                console.log("sse์ง€์›");
            } else {
                console.log("sse๋ฏธ์ง€์›");
            }
            
            
            // ์„œ๋ฒ„์™€ ์ปค๋„ฅ์…˜์ด ๋งบ์–ด์งˆ ๋•Œ ๋™์ž‘ํ•œ๋‹ค
            eventSource.addEventListener('open', function(e) {
                console.log(`connection is open`);
            });
            
            
            // ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ผ ๋•Œ event์—†์ด ๋ณด๋‚ด๋ฉด ๋™์ž‘ํ•œ๋‹ค
            eventSource.addEventListener('message', function(e) {
                console.log(event.data);
            });
    
    
            // ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ผ ๋•Œ event๋ฅผ red๋กœ ์„ค์ •ํ•ด์„œ ๋ณด๋‚ผ ๋•Œ ๋™์ž‘ํ•œ๋‹ค
            eventSource.addEventListener('red', event => {
                const data = JSON.parse(event.data);
                console.log(`red : ${data.message}`);
            });
    
    
            // ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ผ ๋•Œ event๋ฅผ blue๋กœ ์„ค์ •ํ•ด์„œ ๋ณด๋‚ผ ๋•Œ ๋™์ž‘ํ•œ๋‹ค
            eventSource.addEventListener('blue', event => {
                const data = JSON.parse(event.data);
                console.log(`blue : ${data.message}`);
            });
    
    
            // ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋™์ž‘ํ•œ๋‹ค.
            eventSource.addEventListener('error', function(e) {    
                if (e.eventPhase == EventSource.CLOSED){
                	eventSource.close()
                }
                if (e.target.readyState == EventSource.CLOSED) {
                    console.log("Disconnected");
                }
                else if (e.target.readyState == EventSource.CONNECTING) {
                    console.log("Connecting...");
                }
            }, false);
    
        </script>
    </body>
    </html>







    Node Express Server

    ํ…Œ์ŠคํŠธ 1

    router.get('/test', (req, res) => {
        res
          .setHeader("Access-Control-Allow-Origin", "*")
          .setHeader("Content-Type", "text/event-stream")
          .setHeader("Connection", "keep-alive")
          .setHeader("Cache-Control", "no-cache")
          .status(200)
          .write(
            	'event: red\n'+
            	'data: {"message" : "hello '+value+'"}\n\n'
          );
    });

    SSE๋Š” ์œ„์˜ ์ฝ”๋“œ๋Š” ๋™์ž‘ํ• ๊นŒ?

    test.html์„ ์—ด๋ฉด ์‚ฌ์ง„์ฒ˜๋Ÿผ ๋‚˜์˜ค๊ณ  ๋”์ด์ƒ ์•„๋ฌด๋Ÿฐ ๋™์ž‘๋„ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
    ์ฒซ๋ฒˆ์งธ ์ปค๋„ฅ์…˜ ์—ฐ๊ฒฐ ์ดํ›„ ์„œ๋ฒ„์—์„œ ๋‚ด๋ ค์ฃผ๋Š” ๊ฐ’์€ ํ•˜๋‚˜๋ฐ–์— ์—†๋Š”๋ฐ ์ข…๋ฃŒ๋Š” ๋˜์ง€์•Š์œผ๋‹ˆ.. ์›€์ง์ด์ง€ ์•Š๋Š”๊ฒŒ ๋‹น์—ฐํ•˜๋‹ค


    (๋‚˜๋Š” ์œ„ ์ฝ”๋“œ์—์„œ /test๋ฅผ ๋‹ค์‹œ ํ˜ธ์ถœํ•˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚ด๋ ค์˜ฌ ์ค„ ์•Œ์•˜๋‹ค.. ํžˆํžˆ)





    ํ…Œ์ŠคํŠธ 2

    let value = 'SSE';
    router.get('/change', (req, res) => {
        let {param} = req.query;
        value = param;
        res.end();
    });
    
    router.get('/test', (req, res) => {
    
        res
          .setHeader("Access-Control-Allow-Origin", "*")
          .setHeader("Content-Type", "text/event-stream")
          .setHeader("Connection", "keep-alive")
          .setHeader("Cache-Control", "no-cache")
    
        setInterval(() => {
            res
                .status(200)
                .write(
                    'event: red\n'+
                    'data: {"message" : "hello '+value+'"}\n\n'
                );
        }, 2000);
    
    });


    ํ…Œ์ŠคํŠธ 2์˜ ์ฝ”๋“œ๋Š” ์„œ๋ฒ„๊ฐ€ 2์ดˆ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ๊ณ„์† ๋ณด๋‚ด์ฃผ๋‹ˆ๊นŒ test.html์„ ์‹คํ–‰ํ•˜์ž๋งˆ์ž ๊ณ„์†ํ•ด์„œ ์„œ๋ฒ„์˜ ๋ฐ์ดํ„ฐ๊ฐ’์„ ๋ฐ›์•„์˜จ๋‹ค.

    ์—ฌ๊ธฐ์„œ /change?param=green์„ ํ˜ธ์ถœํ•˜์—ฌ value๋ฅผ ๋ฐ”๊พผ๋‹ค๋ฉด

    ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ๋ฐ”๋€Œ์–ด์„œ ํ”„๋ก ํŠธ๋กœ ์˜จ๋‹ค!




    ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ์ ์„ ์บ์น˜ํ•ด์„œ setInterval ๋‚ด๋ถ€์—์„œ res.writeํ•ด์ฃผ๋ฉด

    ํด๋ผ์ด์–ธํŠธ๋Š” ์„œ๋ฒ„์— ์งˆ์˜ํ•˜์ง€์•Š๊ณ ๋„ ๋ณ€๊ฒฝ๋œ ๊ฐ’์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ๋œ๋‹ค.

    event๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•ด์„œ ๋ฐ›์„ ์ˆ˜๋„ ์žˆ๋‹ค !
    ๋ฐ˜์‘ํ˜•
    • ๋„ค์ด๋ฒ„ ๋ธ”๋Ÿฌ๊ทธ ๊ณต์œ ํ•˜๊ธฐ
    • ํŽ˜์ด์Šค๋ถ ๊ณต์œ ํ•˜๊ธฐ
    • ํŠธ์œ„ํ„ฐ ๊ณต์œ ํ•˜๊ธฐ
    • ๊ตฌ๊ธ€ ํ”Œ๋Ÿฌ์Šค ๊ณต์œ ํ•˜๊ธฐ
    • ์นด์นด์˜คํ†ก ๊ณต์œ ํ•˜๊ธฐ