스트리밍 채팅에는 스피너뿐만 아니라 취소 및 재시도 상태가 필요합니다
요약
스트리밍 AI 채팅 인터페이스 구축 시 발생할 수 있는 UX 실패 사례를 분석하고, 상태 머신을 활용한 견고한 UI 구현 방법을 제안합니다. 로딩, 취소, 실패, 재시도 상태를 체계적으로 관리하여 키보드 접근성과 스크린 리더 지원을 강화하는 가이드를 제공합니다.
핵심 포인트
- 단순 스피너를 넘어 취소, 실패, 재시도를 포함한 상태 머신 설계 필요
- 부분적인 출력 데이터(Partial output)를 유지하여 사용자 경험 보존
- 키보드 조작 및 스크린 리더를 위한 포커스 관리와 안내(Announcement) 필수
- AbortController 등을 활용한 스트리밍 제어 로직 구현
'실시간으로 자신의 UI를 다시 쓰는' 채팅 데모는 보는 재미가 있지만, 이를 중단하려고 하면 문제가 발생합니다. 저는 키보드 경험이 다음과 같은 스트리밍 AI 인터페이스를 계속 테스트하고 있습니다: Enter를 누르고, 포커스를 잃고, 스크린 리더(Screen reader)로부터 아무 소리도 듣지 못하며, 폭주하는 생성 과정을 멈출 수 있는 유일한 방법이 탭을 닫는 것뿐인 상황 말입니다. 스피너(Spinner)는 상태 머신(State machine)이 아닙니다. 이 포스트에서는 로딩, 취소, 실패, 재시도 과정 중에도 키보드로 조작 가능한 최소한의 스트리밍 채팅 패널을 구축하며, GPU나 신용카드 없이도 재현할 수 있도록 무료로 호스팅된 모델을 사용합니다.
구체적인 상호작용 실패 사례
즐겨 사용하는 AI 채팅 UI를 열고 오직 키보드만 사용하여 다음 시퀀스를 시도해 보세요:
- 긴 답변을 생성하는 프롬프트(Prompt)를 제출합니다.
- 스트리밍 중간에
Escape키로 중단하려고 시도합니다. - 요청이 실패하도록 둡니다 (또는 DevTools에서 네트워크 속도를 제한합니다).
- 마지막 프롬프트를 재시도(Retry)하려고 시도합니다.
많은 구현체에서 다음과 같은 현상이 발생합니다: 포커스는 입력창(Composer)에 갇혀 있고, Escape는 아무 동작도 하지 않으며, 스트림은 조용히 죽어버리고, '재시도'는 프롬프트를 다시 타이핑하는 것을 의미합니다. 스크린 리더(Screen reader) 사용자는 상황이 더 나쁩니다. aria-live가 모든 토큰을 하나하나 발표하거나(정보 과부하), 혹은 아예 아무것도 발표하지 않습니다.
먼저 상태 테이블 정의하기
코드를 작성하기 전에 상태(State)의 이름을 정해야 합니다. 상태의 이름이 지정되지 않았다면, 그것은 처리되지 않은 것입니다:
| 상태 | 트리거 | 시각적 요소 | 키보드 | 안내(Announcement) |
|---|---|---|---|---|
idle | 초기 상태 | 입력창 활성화 | Enter로 제출 | 없음 |
| ... |
아래의 모든 것을 결정하는 두 가지 규칙이 있습니다: 부분적인 출력은 쓰레기가 아니라 데이터이다 (취소/에러 시에도 이를 유지할 것), 그리고 사용자가 유발한 모든 전환(Transition)은 안내되어야 하며 포커스 관리가 가능해야 한다입니다.
타입이 지정된 상태 머신 (Typed state machine)
type ChatState =
| { status: 'idle' }
| { status: 'streaming'; controller: AbortController; text: string }
...
핵심은 문법이 아닙니다. cancelled와 error 상태 모두 text와 prompt를 포함하고 있어서, UI가 부분적인 답변을 유지할 수 있고 사용자에게 입력을 다시 요구하지 않고도 실제 재시도 기능을 제공할 수 있다는 점입니다.
실행 가능한 단일 파일 데모
stream-chat.html로 저장한 뒤 브라우저에서 여세요. 이 데모는 EventSource를 사용하지 않고 fetch 스트리밍을 사용하여 모든 OpenAI 호환 엔드포인트(endpoint)에 연결하며, 엔드포인트 URL과 키를 두 개의 입력창에서 읽어오므로 하드코딩된 내용이 없습니다. (설정 없이 바로 실행하려면 아래의 무료 호스팅 접속 관련 노트를 참조하세요.)
<!doctype html>
<html lang="en">
<head>
...
무엇을 대상으로 실행할 것인가 (무료 티어 가능)
OpenAI 호환 스트리밍 엔드포인트가 필요합니다. 저는 MonkeyCode의 무료 서버 옵션에서 제공하는 무료 모델 액세스를 통해 이를 재현했습니다. 로컬 하드웨어나 카드 등록이 필요 없으므로, 바로 이러한 실패 상태(failure-state) 테스트를 위한 편리한 샌드박스(sandbox)가 됩니다. 공개 사항: 이 글은 MonkeyCode의 제품 홍보의 일환으로 작성되었습니다. 두 가지 솔직한 주의 사항이 있습니다: 저는 할당량(quota), 모델 이름, 지연 시간(latency), 또는 무료 티어가 얼마나 지속되는지에 대해 어떠한 주장도 하지 않으며, 공유 무료 서버는 스트리밍 성능을 측정하기에 부적절한 장소라는 점입니다. 성능(throughput)이 아닌 상호작용의 정확성을 검증하는 용도로 사용하세요. 이미 다른 호환 엔드포인트를 가지고 있다면 변경 없이 작동할 것입니다. 이것이 데모를 제공자 중립적(provider-agnostic)으로 유지하는 목적입니다.
왜 알림(announcements)에 스로틀링(throttled)을 적용하는가
유혹적인 방법은 토큰 컨테이너에 aria-live="polite"를 설정하는 것입니다. 하지만 그렇게 하지 마세요. 스크린 리더(screen reader)는 모든 DOM 변형(mutation)을 대기열에 쌓아두었다가 파편화된 쓰레기 스트림을 읽어줄 것입니다. 이 데모는 토큰이 아니라 **상태 전환(transitions)**을 알립니다: 시작됨, 중단됨(부분 텍스트가 유지되었다는 사실 포함), 완료됨(정상 작동 신호로서 단어 수 포함), 그리고 에러(메시지와 재시도 기능 포함)입니다. 전체 텍스트를 원하는 사용자는 로그 영역으로 직접 이동할 수 있습니다. 해당 영역에는 레이블이 있고 위치가 안정적입니다.
직접 구축할 때를 위한 QA 매트릭스
| 시나리오 | 키보드 전용 | 스크린 리더 | 예상되는 전환 (Transition) |
|---|---|---|---|
| 전송 중 Esc 입력 | Esc로 중단, 포커스가 → 입력창(composer)으로 이동 | '중단됨, 일부 내용 유지됨'이라고 들림 | streaming → cancelled |
| ... |
실제 버전으로 테스트하기: 저는 Windows에서 Chrome/Edge + NVDA를 사용하고, macOS에서 Safari + VoiceOver를 사용하며, DevTools에서 'Slow 3G'로 대역폭을 제한(throttle)하여 스트리밍 중간 상태를 실제로 관찰할 수 있도록 합니다.
한계점 및 이 패턴을 그대로 사용해서는 안 되는 경우
- 단일 턴(Single-turn) 전용. 멀티 턴(Multi-turn) 히스토리, 편집, 브랜칭(branching)을 구현하려면 상태 머신(state machine)을 단순히 덧붙이는 것이 아니라 확장해야 합니다.
- 백프레셔(Backpressure) 처리 부재. 매우 빠른 스트림은
textContent업데이트 속도보다 빠를 수 있습니다. 프로덕션 환경에서는requestAnimationFrame을 사용하여 배치(batch) 처리를 해야 합니다. - 재시도(Retry) 시 동일한 프롬프트를 그대로 다시 재생합니다. 백엔드가 멱등성(idempotency)을 보장하지 않는 경우(도구 호출, 부수 효과 등), 재시도 시에는 대화 수준의 멱등성 키(idempotency key)가 필요합니다.
- 무료 호스팅 티어는 샌드박스입니다. 프로덕션 트래픽이나 민감한 데이터를 해당 티어에 연결하지 마세요. 워크플로우를 구축하기 전에 제공업체의 약관과 속도 제한(rate limits)을 확인하십시오.
이 글의 상단에 있는 시퀀스를 사용자의 채팅 UI에서 재현했을 때 전환(transition)이 실패한다면, 사용 중인 브라우저, OS, 보조 공학 기기(assistive technology) 버전과 정확히 어떤 전환에서 오류가 발생했는지 알려주세요. 그러한 버그 리포트가 이 패턴을 개선하는 밑거름이 됩니다.
핵심 요점: '생성 중...'은 단 하나의 상태가 아닙니다. 최소 다섯 가지 상태이며, 사용자는 당신이 건너뛴 모든 상태를 느낄 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기