LLM 응답 스트리밍의 비밀
LLM 응답 시 지연 시간(Latency) 체감을 줄이기 위해 스트리밍 방식이 도입됨
Server-Sent Events (SSE) 표준을 사용하여 API가 생성하는 토큰을 실시간으로 전송함
SDK는 네트워크 청크(Network Chunk)와 토큰(Token)의 불일치를 추상화하여 개발 편의성을 제공함
유령 스트림(Ghost Stream), 침묵의 잘림(Silent Truncation), 분할 패킷(Split Packet) 등 3가지 주요 버그 처리 방안 제시
스트리밍 응답의 작동 원리: SSE와 청크 처리
LLM 응답 스트리밍은 Server-Sent Events (SSE) 표준을 활용하여 API가 생성하는 토큰을 실시간으로 클라이언트에 푸시하는 방식으로 작동함. 단일 JSON 응답 대신, 지속적인 HTTP 연결을 통해 이벤트가 전송되며, 클라이언트는 이벤트를 수신하여 텍스트를 조합함. 네트워크 청크(Network Chunk)는 토큰이나 단어 단위와 일치하지 않으며, 임의의 지점에서 분할될 수 있으므로 SDK는 이를 **버퍼링(Buffering)하고 개행 문자(
)로 분리**하여 완전한 메시지를 파싱하는 로직을 내부적으로 처리함. `delta.text` 필드에서 실제 생성된 텍스트를 추출하고, `message_delta` 이벤트에서 `stop_reason`을 파악하는 것이 핵심임.
개발자를 위한 스트리밍 파싱 로직
클라이언트 측에서 스트리밍 응답을 처리하는 로직은 ReadableStream을 `for await...of`로 반복하며 바이트를 디코딩하고 버퍼링하는 과정으로 구성됨. SSE 메시지 구분자인 `
`을 기준으로 버퍼를 분할하고, 불완전한 메시지는 다음 청크로 전달하기 위해 버퍼에 유지함. 각 완전한 메시지에서 `data:` 라인을 추출하여 JSON으로 파싱하고, `content_block_delta` 타입의 경우 `delta.text`를 표준 출력에 쓰고, `message_delta` 타입의 경우 `stop_reason`을 표준 에러로 출력함. 이 과정에서 네트워크 경계와 토큰 경계의 불일치를 효과적으로 관리하는 것이 중요함.
스트리밍 환경에서의 주요 버그와 해결 방안
스트리밍 환경에서는 사용자 네비게이션 이탈 시 발생하는 유령 스트림(Ghost Stream), API 과부하 시 발생하는 침묵의 잘림(Silent Truncation), 그리고 SSE 메시지가 여러 TCP 패킷으로 나뉘어 전송될 때 발생하는 분할 패킷(Split Packet) 문제가 발생할 수 있음. 유령 스트림은 `AbortController`를 사용하여 스트림을 명시적으로 중단하고, 침묵의 잘림은 `error` 타입 이벤트를 처리하여 예외를 던지도록 하며, 분할 패킷 문제는 메시지 조각을 버퍼링하여 다음 청크에서 완성하는 방식으로 해결함. 특히 `stop_reason`을 명확히 처리하지 않으면 응답이 중간에 잘렸는지 여부를 알 수 없어 버그로 이어질 수 있음.
스트리밍 vs. 단일 응답: 사용자 경험 비교
스트리밍 방식은 모델의 실제 생성 속도와 관계없이 사용자가 응답을 기다리는 체감 시간을 획기적으로 단축시킴. GIF 예시에서 보듯, 스트리밍을 사용하지 않으면 첫 단어가 나타나기까지 약 4초가 소요되지만, 스트리밍을 사용하면 약 300밀리초(ms) 만에 첫 단어를 볼 수 있음. 전체 응답 완료 시간은 동일하더라도, 초기 응답 속도(Time to First Word)의 차이가 사용자 경험에 큰 영향을 미침. 이는 사용자가 시스템이 '작동 중'임을 인지하게 하여 대기 지루함을 줄여줌.