최근 AI 기술의 발전으로 사용자의 데이터를 쉽게 활용할 수 있는 AI 에이전트 플랫폼들이 주목받고 있습니다. 저는 ChatGPT와 같은 대규모 언어 모델(LLM)을 활용해 사용자가 올린 파일을 학습하고, 이를 기반으로 지능적인 질의응답 서비스를 제공하는 플랫폼을 개발하고 있습니다.
이러한 AI 채팅 서비스에서 가장 중요한 것은 사용자와의 자연스러운 대화 경험입니다. 특히 AI가 답변을 생성하는 과정을 실시간으로 보여주는 것이 핵심 요구사항이었는데요. 지금부터 실시간 스트리밍 구현 과정에서 마주친 기술적 과제들과 그 해결 방법에 대해 3탄에 걸쳐 이야기해보고자 합니다.
일반적인 HTTP 통신에서는 클라이언트의 요청이 있을 때마다 서버가 한 번의 응답을 보내고 연결이 종료됩니다. 예를 들어 "오늘 날씨 어때?"라는 질문에 AI가 오늘은 맑고 화창한 날씨입니다. 라고 응답한다고 해봅시다.
기존 HTTP 방식에서는 실제 대화와는 거리가 먼 경험을 제공합니다.
이런실제 대화처럼 자연스러운 경험을 제공하려면 AI가 응답을 생성하는 과정을 실시간으로 보여줄 필요가 있습니다.
AI가 생성하는 응답을 순차적으로 UI에 표시하기 위해 두 가지 스트리밍 응답 통신 방식을 검토했습니다.
WebSocket
SSE (Server-Sent Events)
일반적인 메신저와 같은 양방향 채팅 서비스와 달리, AI 채팅은 다음과 같은 특징이 있습니다.
이러한 특성을 고려할 때, 양방향 통신의 복잡성을 감수할 필요 없이 단방향 통신만을 지원하는 SSE로 충분하다고 판단했습니다.
SSE는 기존 HTTP 프로토콜을 기반으로 하지만, 일반적인 HTTP 요청/응답 모델과는 근본적으로 다른 통신 패턴을 제공합니다.
기존 HTTP 통신 방식
SSE 통신 방식
클라이언트 요청
GET /stream HTTP/1.1
Host: example.com
Accept: text/event-stream
클라이언트는 Accept: text/event-stream
헤더를 포함하여 SSE 연결을 요청합니다.
서버 응답
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
서버는 Content-Type: text/event-stream
으로 응답하며, 캐싱을 방지하기 위한 헤더들을 포함합니다.
SSE는 W3C와 WHATWG에서 공동으로 관리하는 HTML Living Standard에 포함된 웹 표준 기술입니다. 표준 문서의 Server-Sent Events 섹션에서는 EventSource 인터페이스와 이벤트 스트림의 구문을 상세히 정의하고 있습니다.
SSE 표준은 크게 다음과 같은 핵심 영역을 다룹니다.
이러한 표준화 덕분에 Chrome, Firefox, Safari 등 모든 주요 브라우저에서 일관된 방식으로 SSE가 구현되어 있어, 별도의 호환성 처리 없이도 안정적으로 실시간 데이터 스트리밍을 구현할 수 있습니다.
SSE는 서버에서 클라이언트로 전송되는 이벤트 스트림의 형식을 명확하게 정의하고 있습니다. 각 이벤트는 여러 필드로 구성되며, 특정 규칙에 따라 파싱됩니다.
AI 채팅에서 사용되는 이벤트 스트림의 예:
event: message
data: {"text": "안녕하세요! "}
id: 1
event: message
data: {"text": "안녕하세요! 오늘 날씨에 대해 "}
id: 2
event: message
data: {"text": "안녕하세요! 오늘 날씨에 대해 말씀드리겠습니다."}
id: 3
event: close
data: {"reason": "completion"}
id: 4
각 청크는 개행 문자(\n\n
)로 구분되며, 이는 브라우저가 개별 이벤트를 인식하는 기준이 됩니다.
브라우저는 SSE 통신을 위한 표준 인터페이스로 EventSource
를 제공합니다. 이는 WHATWG 명세를 기반으로 각 브라우저 벤더(Chrome, Firefox, Safari 등)가 구현한 것입니다.
readyState
속성을 통해 연결 상태 확인 가능 (0: CONNECTING
, 1: OPEN
, 2: CLOSED
)EventSource
생성자의 두 번째 인자로 { withCredentials: true }
옵션을 주면 쿠키 및 인증 정보 포함 가능message
이벤트를 자동으로 수신하고 핸들러 실행event:
필드를 사용하여 특정 이벤트 타입 지정 가능 (addEventListener('customEvent', handler)
)JSON.parse(event.data)
로 변환하여 사용 가능error
이벤트 발생Access-Control-Allow-Origin
헤더 필요).close()
메서드를 호출하여 수동으로 연결 종료 가능event.lastEventId
활용)const eventSource = new EventSource('/stream');
eventSource.addEventListener("notice", (e) => {
console.log(e.data);
});
하지만 브라우저의 EventSource는 몇 가지 중요한 제약사항이 있습니다.
EventSource는 기본적으로 GET 요청만 지원하고, 요청 헤더도 제한적으로만 설정할 수 있습니다. 특히 우리 서비스에서는 음성 메모나 PDF와 같은 대용량 파일을 요청 본문에 포함해야 했는데 EventSource로는 구현이 불가능했습니다.
이런 대량의 데이터를 GET 요청의 쿼리 파라미터로 전송하는 것은 적절하지 않았고 POST 요청으로 긴 요청 본문(body)을 전송할 수 있는 새로운 해결책이 필요했습니다.
지금까지 AI 채팅의 실시간 응답 구현을 위한 기술 선택 과정과 SSE의 기본 개념을 살펴봤습니다. 단방향 통신이 주요 패턴인 AI 채팅에서는 SSE가 구현 복잡도와 성능 측면에서 적절한 선택이었습니다. 표준 HTTP 프로토콜 위에서 동작하고 브라우저 기본 지원을 활용할 수 있어 개발 리소스도 절약할 수 있었습니다.
하지만 앞서 언급한 것처럼 브라우저의 EventSource는 POST 요청을 지원하지 않는 등의 한계가 있었습니다. 다음 글에서는 이런 한계를 극복하기 위한 커스텀 SSE 클라이언트 구현에 대해 다루겠습니다
이 글이 도움이 되었길 바랍니다. 질문이나 의견이 있으시면 댓글로 남겨주세요!