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가 전체 스트링으로 불러와지고 있었습니다.
문제
- 데이터가 전체 스트링화
- 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);
}
};
데이터 화면 도출
그 후에는 화면 구성에 맞게 파싱 하여 사용자가 교정본, 개선내용, 예상 질문 맟 답변을 확인할 수 있도록 했습니다.
참고
'Javascript' 카테고리의 다른 글
날짜 순으로 정렬 (0) | 2024.09.04 |
---|---|
현재 페이지 URL 가져오기 (1) | 2024.09.02 |
메모리누수 방지 (0) | 2024.08.29 |
형제, 부모, 자식 노드 접근 (1) | 2024.08.28 |
HTML 태그(DOM) 접근하기 (0) | 2024.08.28 |