Javascript

SSE (text/event-stream) 적용

minsun309 2024. 9. 1. 21:41

407 포텐데이X클로바 스튜디오 해커톤에 참여해  HyperCLOVA X 를 활용한 AI 발표 준비 도우미, 또랑또랑을 출시했다.

또랑또랑 : https://www.ttorang.site/

프로젝트를 진행하면서 마주친 문제 중 하나인 text/event-stream 에 대해 정리해 보았다

배경

서비스 특성상 사용자 요청이 2~3천 자 이상이면 요청시간이 너무 길어져 Caused by: io.netty.handler.timeout.ReadTimeoutException: null 에러가 발생했습니다. 동기식 처리 방식으로 인해 모든 응답이 완료될 때까지 시간이 너무 길어져 에러가 발생하고 있다고 판단했습니다.

이 문제를 해결하기 위해 백엔드에서 적은 리소스로 더 많은 요청을 효율적으로 처리할 수 있는 방식인 Flux를 도입했습니다.

이제 대량의 텍스트를 넣어도 포스트맨, 스웨거에서 timeout 에러가 발생하지 않게 되어 클라이언트에 적용하려고 보니 위 사진 같이 조금 생소한 구조의 데이터가 들어왔습니다.

그동안 익숙했던 Content-Type: application/json 이 아닌 Content-Type: 'text/event-stream'인 데이터를 받아오는 것을 확인했습니다.

SSE(Server Sent Event)란?

Server-Sent Events(SSE) 란, 실시간으로 서버에서 클라이언트로 데이터를 보내는 단방향 통신을 의미합니다. SSE는 클라이언트가 서버에 연결 상태를 유지하면, 서버가 클라이언트에 지속적으로 데이터를 전송할 수 있습니다.

기본 REST API

  • 기본적으로 REST API는 클라이언트에서 서버로 요청을 보내고, 서버가 응답을 반환하는 단방향 요청-응답 방식입니다.
  • 클라이언트는 서버에게 요청을 보내며, 서버는 요청에 대한 응답을 제공합니다. 이 과정은 클라이언트의 요청이 있을 때만 이루어지며, 서버는 클라이언트에게 자동으로 새로운 정보를 전송하지 않습니다.

Server-Sent Events (SSE)

  • Server-Sent Events는 클라이언트가 서버에 연결 요청을 보내고, 서버가 클라이언트에게 실시간으로 데이터를 푸시하는 단방향 스트리밍 방식입니다.
  • 클라이언트는 서버와의 연결을 설정한 후, 서버가 지속적으로 데이터를 전송합니다. 이 방식은 실시간 데이터 업데이트가 필요한 경우 유용합니다. 클라이언트는 여전히 서버에 요청을 보낼 수 있지만, 데이터 전송은 서버에서 클라이언트로만 이루어집니다.

 

데이터 확인

받아온 데이터를 Response에서 확인해 보면 아래와 같이 나오는데 이해가 안 가 콘솔에서 확인해 보았더니 data가 전체 스트링으로 불러와지고 있었습니다.

Response / console

문제

  • 데이터가 전체 스트링화
  • data : 가 매번 붙어서 나옵니다.

 

해결 포인트

text/event-stream 형식은 서버에서 클라이언트로 이벤트를 전송하는 스트리밍 프로토콜입니다. 이 포맷에서 이벤트는 다음과 같은 형식을 가집니다:

  • 각 이벤트는 특정 필드들로 구성될 수 있습니다 (data, event, id, retry 등).
  • 이벤트는 두 개의 줄 바꿈 문자(\n\n)로 구분됩니다.(상단 왼쪽 이미지 처럼) 즉, 각 이벤트는 빈 줄로 구분된 블록 형태로 전송됩니다.

 

해결

  • 전달된 데이터는 'data:'로 시작하는 부분이 포함될 수 있습니다. 이 부분을 제거하여 데이터만 남깁니다.
  • 데이터는 이벤트 단위로 나누어져 있으며, 각 이벤트는 두 개의 줄바꿈 문자('\n\n')로 구분됩니다. 이 기준으로 문자열을 나누어 각 이벤트를 배열에 저장합니다.
  • 각 이벤트를 반복하면서
    • 이벤트가 비어 있지 않으면 (event.trim() 사용 - 공백만 있는지 아닌지를 확인),
    • 이벤트를 JSON으로 파싱 합니다 (JSON.parse(event)).
    • 파싱 된 JSON 객체에서 message.content를 추출합니다. content가 있으면 이를 newContentQueue 배열에 추가합니다.
    • JSON 파싱 중 오류가 발생하면 이를 콘솔에 로그로 남깁니다.
  • newContentQueue 배열에 저장된 모든 메시지를 하나의 문자열로 결합합니다. 이 문자열은 모든 content 메시지를 연속적으로 포함하게 됩니다.
  const modifyScript = async () => {
    setScriptLoading(true);
    try {
      const data = {//.. };
      //...
      
      //data
      const response = await fetchAnnounceData(data);
      const redData = response.data.replace(/data:/g, '');
      const events = redData.split('\n\n'); // 이벤트 분리
      const newContentQueue = [];
      events.forEach((event) => {
        if (event.trim()) {
          try {
            const jsonData = JSON.parse(event);
            const content = jsonData.message?.content || '';

            if (content) {
              // 상태를 업데이트하여 새 content 값을 배열에 추가
              newContentQueue.push(content);
            }
          } catch (error) {
            console.error('Failed to parse JSON:', error);
          }
        }
      });

      const finaldata = newContentQueue.join('');
      
      //.. 

    } catch (error) {
      console.error('Error fetching modified script:', error);
    }
  };

 

 

데이터 화면 도출

 

그 후에는 화면 구성에 맞게 파싱 하여 사용자가 교정본, 개선내용, 예상 질문 맟 답변을 확인할 수 있도록 했습니다.

 

 

참고

 

openAI chat API SSE (text/event-stream) 적용해보기

목차 chatGPT 웹에서 대화시 텍스트가 따다다닥 박히는데, SSE 방식이라고 파트장님이 알려주셔서 한번 간단하게 적용해보았다. 느낀점 1. 재밌고 신선하다. 어떻게보면 파일 송수신이나 같이 특별

vapor3965.tistory.com