중단과 재개 가능한 스트림
중단과 재개 가능한 스트림 (Abort and resumable streams)
useChat에서 resume: true로 스트림 재개를 쓸 때 중단(abort)과 연결 해제(disconnect)가 어떻게 구분되는지, 그리고 올바르게 처리하는 방법을 알려드려요.
출처: 문서
본문
문제 (Issue)
useChat에 resume: true를 사용해 스트림 재개를 쓸 때, 클라이언트 측 중단은 연결 해제로 취급돼요. 탭을 닫거나, 페이지를 새로고침하거나, 다른 곳으로 이동하거나, stop()을 호출하면 현재 HTTP 연결이 닫히지만, 그것이 기본(underlying) 생성 작업을 취소해서는 안 돼요.
애플리케이션이 요청 abort 신호를 모델 호출로 전달한다면, 연결 해제가 스트림 재개가 계속 실행되길 기대하는 작업을 취소할 수 있어요. stop 버튼이 stop()만 호출한다면, 서버 쪽 생성은 계속될 수 있고 클라이언트는 같은 활성 스트림에 다시 연결할 수 있어요.
const { messages, stop } = useChat({
id: chatId,
resume: true, // 스트림 재개 활성화
});
// stop()은 현재 클라이언트 요청만 중단해요.
// 서버 측 취소 요청이 아니에요.
배경 (Background)
스트림 재개는 원래 연결이 닫힌 후 클라이언트가 활성 스트림에 다시 연결할 수 있게 해줘요. 이를 지원하려면 서버가 현재 어떤 클라이언트도 연결되지 않았을 때도 스트림 프로듀서를 계속 실행해야 해요.
즉, 라우트 정리, 페이지 언로드, 네트워크 연결 해제는 재개 가능한 연결 해제로 처리되어야 해요. 명시적인 사용자 취소는 활성 프로듀서를 취소하고 저장된 활성 스트림 참조를 지우는 별도의 서버 측 신호가 필요해요.
해결 방법 (Solution)
연결 해제 후 재연결에는 resume: true를 사용하고, 명시적 사용자 취소에는 전용 stop 엔드포인트를 추가하세요.
stop 엔드포인트는 해야 할 일:
- 채팅을 로드하고 활성 스트림 ID를 읽는다
- 클라이언트가 부분 어시스턴트 메시지를 보내면 최신 부분 메시지를 영속화한다
- 스트림을 만드는 작업을 취소한다
- 활성 스트림 참조가 여전히 같은 스트림을 가리킬 때만 그 참조를 지운다
클라이언트에서는 로컬 채팅 스트림을 멈추기 전에 stop 엔드포인트를 호출하세요:
const chat = useChat({
id: chatId,
resume: true,
});
async function stopStream() {
await fetch(`/api/chat/${chatId}/stop`, { method: 'POST' });
chat.stop();
}
내비게이션을 stop 동작과 분리하세요. 라우트 정리 코드, 페이지 언로드 핸들러, 컴포넌트 unmount 정리에서 stop 엔드포인트를 호출하지 마세요. 그런 이벤트는 재개 가능해야 하는 연결 해제예요.
스트림 재개가 필요 없고 클라이언트 측 취소만 원한다면 resume을 비활성화하고 stop()을 직접 사용하세요:
const { messages, sendMessage, stop } = useChat({
id: chatId,
resume: false, // 스트림 재개 비활성화 (기본 동작)
});
관련 항목
더 알아보기 (Learn more)
- onEnd not called when stream is aborted — 중단 시 onEnd 처리
- useChat — 채팅 훅