AI 스트리밍 UX, 끊기지 않게 설계하는 법

AI 스트리밍 UX, 끊기지 않게 설계하는 법

SSE로 토큰을 흘려보내고, 에이전트 파이프라인이 깨질 때 프론트가 무엇을 보여줄 것인가—두 질문이 만나는 곳에 진짜 AI UX 설계가 있다

Server-Sent Events SSE LLM 스트리밍 MCP 에이전트 에이전트 실패 모드 AI UX 설계 Next.js 스트리밍 UX
광고

LLM이 답변을 생성하는 동안 화면이 멈춰 있다면, 사용자는 기다리는 게 아니라 의심하기 시작한다. "응답이 오고 있는 건가? 아니면 에러인가?" 이 짧은 불확실성이 AI 제품의 신뢰를 갉아먹는다. 토큰을 실시간으로 흘려보내는 스트리밍 UX는 이제 선택이 아니라 AI 프로덕트의 기본 전제가 됐다. 그리고 스트리밍을 구현하고 나면 곧바로 다음 질문이 찾아온다. 에이전트 파이프라인이 중간에 깨졌을 때, 프론트는 사용자에게 무엇을 보여줘야 하는가.

SSE는 왜 LLM 스트리밍의 기본값이 됐나

dev.to에 올라온 SSE 심층 가이드는 이 선택을 명쾌하게 정리한다. 실시간 데이터를 클라이언트에 푸시하는 방법은 세 가지다—폴링, WebSocket, SSE. 폴링은 "새 거 있어?"를 반복해서 묻는 구조라 낭비가 심하다. WebSocket은 양방향 통신이 필요할 때 강력하지만, LLM 응답처럼 서버만 말하면 되는 상황에는 오버스펙이다. SSE는 단일 HTTP 응답을 열어두고 서버가 텍스트 이벤트를 흘려보내는 구조다. 프로토콜 업그레이드 없이 기존 HTTP 인프라를 그대로 쓰고, 브라우저의 EventSource API가 자동 재연결까지 처리해준다.

와이어 포맷은 놀랍도록 단순하다. Content-Type: text/event-stream으로 응답을 열고, data: 필드에 내용을 담아 빈 줄로 구분하면 끝이다. OpenAI의 스트리밍 API가 내부적으로 SSE를 쓰는 것도 이 단순함 때문이다. 토큰 하나하나를 data 이벤트로 내려보내고, 완료 시점에 done 이벤트를 쏘는 패턴이 사실상 표준처럼 굳어졌다.

구현에서 놓치기 쉬운 세 가지

원리는 단순하지만, 프로덕션에서 조용히 발목을 잡는 지점이 있다. 첫째, 프록시 버퍼링이다. Cache-Control: no-cache를 빠뜨리면 중간 프록시가 이벤트를 모아뒀다가 한 번에 쏟아버린다. 사용자 입장에서는 스트리밍이 아니라 긴 로딩 후 전체 응답이 터지는 것처럼 보인다. 둘째, 연결 누수다. req.on('close') 핸들러 없이 구현하면, 사용자가 페이지를 떠난 뒤에도 타이머와 메모리가 서버에 남는다. 트래픽이 늘어날수록 이 누수가 쌓인다. 셋째, 인증이다. 브라우저의 EventSource는 커스텀 헤더를 지원하지 않는다. 토큰 인증이 필요한 API라면 fetch + ReadableStream으로 직접 파싱해야 한다. 이 패턴은 OpenAI 스트리밍 클라이언트가 내부적으로 쓰는 방식과 동일하다.

스트리밍이 흐를 때, 파이프라인이 깨질 때

스트리밍 구현이 끝나면 다음 문제가 기다린다. 에이전트 파이프라인이다. 단순한 LLM 호출이 아니라 여러 에이전트가 툴을 호출하고 결과를 넘기는 MCP 파이프라인에서는, 실패 양상이 훨씬 다양하고 예측하기 어렵다. dev.to에 공개된 오픈소스 프로젝트 'The Gauntlet'은 이 문제를 정면으로 다룬다. Next.js 16과 LangChain으로 구성된 이 데모는 7개의 MCP 서버를 연결하고, 실행 중에 8가지 실패 모드를 실시간으로 토글할 수 있게 설계됐다.

실패 목록을 보면 프로덕션의 현실이 느껴진다. 두 서버가 같은 이름의 툴을 노출할 때 생기는 툴 네임 충돌, 이전 실행의 오염된 데이터가 다음 에이전트에게 넘어가는 컨텍스트 오염, 레이트 리밋에 즉각 재시도가 터져 타임아웃이 연쇄되는 리트라이 스톰, LLM이 존재하지 않는 툴 이름을 호출하는 툴 환각, 50KB 응답이 컨텍스트 윈도우를 날려버리는 컨텍스트 폭탄, 회로 차단기 없이 같은 툴을 무한 호출하는 무한 루프. 이 중 어느 것도 단일 컴포넌트의 버그가 아니다. 서버, 라우팅, LLM 결정의 상호작용에서 창발하는 실패들이다.

프론트가 설계해야 할 실패 가시성

The Gauntlet의 진짜 가치는 실패를 '보이게' 만드는 UI 설계에 있다. 리트라이 패턴을 점 쌓기 그래프로 시각화하고, 컨텍스트 토큰 팽창을 실시간 게이지로 보여주고, 무한 루프를 팩맨 애니메이션으로 표현한다. 개발자 도구가 아니라 사용자에게 보여줄 수 있는 언어로 번역된 실패다. 이것이 중요한 이유는 명확하다. 에이전트가 조용히 실패하면 사용자는 원인을 모른 채 기다리거나 이탈한다. 실패를 감지하고 의미 있는 피드백을 돌려주는 구조가 없으면, 스트리밍으로 속도감을 줘도 신뢰는 무너진다.

두 질문이 만나는 곳

SSE와 에이전트 실패 모드는 표면적으로 다른 주제처럼 보이지만, 사실 같은 질문의 두 면이다. 'AI 응답을 사용자에게 끊기지 않게 전달하려면 무엇을 설계해야 하는가.' SSE는 정상 경로의 답이고, 실패 모드 처리는 비정상 경로의 답이다. 정상일 때 토큰을 흘려보내는 스트리밍 UX와, 파이프라인이 깨졌을 때 의미 있는 상태를 돌려주는 에러 UX—둘 다 없으면 AI 프로덕트의 UX는 완성되지 않는다.

지금 팀이 해야 할 것

프론트엔드 관점에서 당장 챙겨야 할 설계 포인트는 세 가지다. 첫째, SSE 엔드포인트에 req.on('close') 처리와 keep-alive 주석 라인을 기본으로 넣는다. 둘째, 에이전트 파이프라인의 각 단계마다 실패 상태를 명시적으로 정의하고, UI에서 어떻게 보여줄지 미리 설계한다—"처리 중"과 "실패"는 다른 메시지다. 셋째, 툴 호출 로그와 리트라이 상태를 개발 환경에서 가시화하는 디버그 패널을 만들어둔다. The Gauntlet처럼 실패를 재현할 수 있는 환경이 없으면, 프로덕션에서 처음 실패를 마주치게 된다.

스트리밍은 속도의 문제가 아니라 신뢰의 문제다. 토큰이 흐르는 동안 사용자는 시스템이 살아있다는 것을 느끼고, 파이프라인이 깨졌을 때 의미 있는 피드백을 받으면 시스템을 믿는다. AI UX를 설계한다는 것은 결국 이 두 감각을 프론트에서 엔지니어링하는 일이다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요