nestjs

Server-Sent Events(SSE) in nestjs

Rogan_Kim 2023. 10. 15. 16:06
728x90

1. SSE란 무엇인가?

  • SSE는 서버에서 클라이언트로 실시간으로 데이터를 전송하는 단방향 기술입니다. 
    Websocket은 양방향으로 통신한다면 SSE는 서버에서 일방적으로 데이터를 전송할 때 사용합니다

아래의 포스트에서 chat-gpt를 streaming으로 통신할때, 서버에서 openAi를 통해 SSE방식으로 데이터를 받아오고,

클라이언트에 전송할때는 websocket을 활용했었습니다.

https://kimjunho97.tistory.com/41

 

open-ai chat-gpt streaming 통신 기록

nestjs에서 open ai의 gpt-3.5-turbo를 사용하여 streaming 통신 구현 과정을 기록하였습니다. 참고 : https://github.com/openai/openai-node#streaming-responses GitHub - openai/openai-node: The official Node.js / Typescript library for the

kimjunho97.tistory.com

여기서 서버와 클라이언트로 단방향 통신이라고 했는데, 서버에서 서버로 SSE방식으로 데이터를 받아온다고? 할 수 있는데,

서버와 클라이언트를 정의는 아래와 갔습니다.

더보기

서버는 데이터를 포함하거나 네트워크의 다른 컴퓨터에서 액세스 하는 기능을 제공하는 컴퓨터입니다.

클라이언트는 서버로부터 서비스나 데이터를 요청하는 컴퓨터입니다.

보통 프론트엔드, 백엔드 나눠서 통신할때 백엔드 서버, 프론트 클라이언트라고 정의해서 많이 사용해 와서,

웹페이지(프론트) = 클라이언트, 백엔드(서버) = 서버라고 당연하게 생각하고 있었는데, 이번에 확실히 정의를 알 수 있었습니다.

 

 

2.  nestjs에서 SSE controller 구현

구현 방법은 매우 간단합니다.

헤더에 다음의 항목을 추가해 줍니다.

1. Content-Type: text/event-stream

   응답이 Server-Sent Events(SEE) 스트림임을 나타냅니다.

2. Cache-Control: no-cache

   응답을 캐시 하지 않습니다. 캐싱이 일어나면, 신규데이터가 즉시 클라이언트에 전달되지 않고 지연될 수 있기 때문입니다.

3. Connection: keep-alive

   서버와 클라이언트 간의 네트워크 연결이 계속 유지되어야 함을 나타냅니다.

import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express'; // 필수 npm i express > express 설치해야합니다

@Controller('')
export class AppController {
  ...
  
  @Get('see')
  sendEvents(@Res() res: Response) {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    setInterval(() => {
      const data = new Date().toLocaleTimeString();
      res.write(`data: ${JSON.stringify(data)}\n\n`);
    }, 1000);
  }
}

그리고 Resonpse 타입을 express에서 가져와서 사용해야 setHeader와 res.write를 쓸 수 있습니다.

 

브라우저에서 해당 api를 요청하면 아래와 같이 나옵니다.

utf-8 인코딩이 제대로 안된 것을 확인할 수 있습니다.

그러므로 Content-Type에 인코드 utf-8을 명시해 주었습니다.

import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express'; // 필수 npm i express > express 설치해야합니다

@Controller('')
export class AppController {
  ...
  
  @Get('see')
  sendEvents(@Res() res: Response) {
    res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    setInterval(() => {
      const data = new Date().toLocaleTimeString();
      res.write(`data: ${JSON.stringify(data)}\n\n`);
    }, 1000);
  }
}

 

반영 후 아래와 같이 한글이 인코딩이 잘된 것을 확인할 수 있었습니다.

그리고 아래와 같이 코드를 추가하면 5초 후에 통신을 끊을 수 있습니다.

import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express'; // 필수 npm i express > express 설치해야합니다

@Controller('')
export class AppController {
  ...
  
  @Get('see')
  sendEvents(@Res() res: Response) {
    res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    setInterval(() => {
      const data = new Date().toLocaleTimeString();
      res.write(`data: ${JSON.stringify(data)}\n\n`);
    }, 1000);
    // 통신 끊기
    setTimeout(() => {
      res.end();
    }, 5000);
  }
}

 

 

2. React에서 SSE 데이터 처리

그다음으로는 리액트에서 SSE 데이터 처리 방식에 대해 알아보겠습니다.

 

import { useEffect, useState } from 'react';

export default function TestView() {
  const [data, setData] = useState<string[]>([]);
  useEffect(() => {
    const eventSource = new EventSource('http://localhost:4000/v2/see');

    eventSource.onmessage = function (event) {
      setData((prev) => [...prev, JSON.parse(event.data)]);
    };
    // 5초에 종료
    setTimeout(() => {
      eventSource.close();
    }, 5000);
  }, []);
  return (
    <>
      <h1> TestView</h1>
      {data.map((e, i) => (
        <p key={i}>{e}</p>
      ))}
    </>
  );
}

위처럼 코드를 작성하면 1초마다 데이터를 받아 아래와 같은 결과물을 얻을 수 있습니다.

 

 

1. 그 외에 응용 ( 쿼리로 데이터 넘겨주기)

react

import { useEffect, useState } from 'react';

export default function TestView() {
  const [data, setData] = useState<string[]>([]);
  useEffect(() => {
     // 쿼리 see_id = 1
    const eventSource = new EventSource(
      'http://localhost:4000/v2/see?see_id=1'
    );
    console.log(eventSource);
    eventSource.onmessage = function (event) {
      setData((prev) => [...prev, JSON.parse(event.data)]);
    };
    setTimeout(() => {
      eventSource.close();
    }, 5000);
  }, []);
  return (
    <>
      <h1> TestView</h1>
      {data.map((e, i) => (
        <p key={i}>{e}</p>
      ))}
    </>
  );
}

 

nestjs

import { Controller, Get, Res } from '@nestjs/common';
import { Request, Response } from 'express'; // 필수 npm i express > express 설치해야합니다

@Controller('')
export class AppController {
  ...
  
  @Get('see')
  sendEvents(@Req() req: Request, @Res() res: Response) {
    res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    const seeId = req.query['see_id'];
    
    setInterval(() => {
      const data = new Date().toLocaleTimeString();
      res.write(`data seeId => ${seeId}: ${JSON.stringify(data)}\n\n`);
    }, 1000);
    // 통신 끊기
    setTimeout(() => {
      res.end();
    }, 5000);
  }
}

여기서 주의할 점은 express의 Response를 활용한 시점에서@Query pipe로 원하는 쿼리 값을 가져올 수 없다.

그러므로 express의 Request를 활용하였습니다.

 

 

 

 

 

 

참고자료

https://developer.mozilla.org/en-US/docs/Web/API/EventSource

 

EventSource - Web APIs | MDN

The EventSource interface is web content's interface to server-sent events.

developer.mozilla.org

 

728x90