최근 SSE(Server-Sent Events) 기반 스트리밍 응답을 처리하는 과정에서 꽤 어려운 문제를 마주했다.
바로 브라우저의 StreamReader 구조 때문에 SSE 이벤트의 일부가 done === true 이후에도 처리되는 현상 때문이었다.
이 문제는 단순한 버그처럼 보였지만 깊이 파보면 스트리밍 구조 자체에 대한 이해가 필요했고,
React 상태 관리 및 useRef 동작까지 얽혀 있어 꽤 난이도가 있었다.
이번 글에서는 문제가 어떻게 발생했는지 → 왜 발생했는지 → 어떻게 해결했는지
내가 적용한 전략을 정리해본다.
SSE 이벤트 흐름은 보통 다음 순서를 따른다:
message_start
content_block_start
content_block_delta (여러 번)
content_block_stop
message_stop
reader.read() → done === true
나는 이 중 message_stop 이벤트가 왔을 때 스트림이 끝났다고 판단하고
botMessageIdRef.current를 초기화하고 있었다.
그런데 문제는…
즉 순서가 이렇게 흐를 수 있다:
done === true → botMessageIdRef 초기화
↓
question_delta 이벤트 처리 시도
→ botMessageIdRef 는 '' 이므로 메시지 업데이트 실패
결국 정확히 어떤 bot 메시지를 업데이트해야 하는지 알 수 없게 되는 오류가 발생한다.
브라우저의 StreamReader는 다음과 같은 구조를 가진다:
reader.read() → { value: Uint8Array, done: false }
reader.read() → { value: Uint8Array, done: false }
reader.read() → { value: undefined, done: true } ← 스트림 종료 시점
하지만 마지막 value(마지막 chunk) 안에 여러 SSE 이벤트가 포함될 수 있다.
문제는 done === true 라고 해서 이전의 chunk 가 완전히 처리된 것이 아니라는 점이다.
즉…
그리고 나는 바로 이 차이를 간과하고 있었다.
콘솔 로그를 찍어보면 이렇게 나온다:
done true
msg.id user-text-1765538817472
botMessageIdRef.current ""
즉 done이 먼저 찍히고, 그 직후 question_delta가 들어온다.
이제 botMessageIdRef.current는 이미 초기화된 상태.
그래서 메시지 업데이트가 불가능해진다.
결론은 단순하지만 매우 효과적이었다.
🔥 botMessageIdRef는 message_stop(종료)이 아니라 message_start(시작)에서 초기화해야 한다.
왜냐하면…
따라서 아래와 같이 로직을 수정했다:
botMessageIdRef.current = '' // 이전 id 값이 남아 있으면, 시작 시점에 초기화
if (data.type === 'message_start') {
botMessageIdRef.current = data.message.id; // 새로운 id 할당
}
그리고 message_stop이나 done === true 시점에는 botMessageIdRef를 즉시 초기화하지 않는다.
대신 스트림 전체가 끝나고 모든 delta 이벤트 처리가 끝난 후에 한 번만 초기화한다.
이 방식으로 개선한 후 다음 문제가 모두 해결되었다:
이번 문제는 개발자가 흔히 갖는 오해에서 비롯된다.
“message_stop → done → 이후에는 더 이상 이벤트가 없을 것이다.”
하지만 SSE는 텍스트 기반 스트리밍이기 때문에 chunk 단위로 움직이고,
chunk 안의 이벤트 처리와 stream done 시점은 타이밍이 일치하지 않는다.
따라서 스트림 종료 처리 코드는 다음 순서로 작성해야 한다:
이러한 구조를 유지하면 어떤 모델(GPT, Claude, Gemini)이든
모든 SSE 이벤트를 안정적으로 처리할 수 있다.
이번 경험은 단순 버그 해결을 넘어서,
스트림 처리 모델을 깊이 이해하는 계기가 되었다.
SSE는 “간단해 보이지만 실제로는 섬세한 처리 흐름을 요구”한다.
앞으로 다른 SSE 기반 다른 웹앱 개발을 할 때도 잘 활용할 수 있을 것 같다.