스트림 참여 및 재참여
스트림 참여 및 재참여 (Join & Rejoin Streams)
에이전트가 긴 작업을 처리하는 동안 사용자가 페이지를 닫아버리면, 그동안의 진행 상황이 통째로 사라질까요? 전통적인 스트리밍 방식이라면 그랬겠지만, join & rejoin 패턴을 쓰면 에이전트는 서버에서 계속 실행되고, 나중에 사용자가 돌아와 정확히 그 지점부터 다시 이어받을 수 있어요. 이 페이지에서는 그 방법을 자세히 설명해 드릴게요.
join & rejoin이 왜 필요할까요?
기존 스트리밍 API는 클라이언트와 서버가 강하게 결합되어 있어서, 클라이언트가 끊기면 스트림도 함께 사라져요. Join & rejoin은 이 결합을 끊어주고, 몇 가지 중요한 패턴을 가능하게 합니다.
- 네트워크 중단 — 이동 중 셀 타워나 Wi-Fi를 오가는 모바일 사용자가 끊김 없이 재개할 수 있어요.
- 페이지 탐색 — 채팅 페이지에서 벗어났다가 나중에 돌아와도 진행 상황을 잃지 않습니다.
- 모바일 백그라운드 — OS가 앱을 일시 중단해도, 다시 포그라운드로 오면 스트림에 재참여할 수 있어요.
- 장시간 실행 작업 — 연구, 코드 생성, 데이터 분석처럼 수 분이 걸리는 작업에서 페이지를 계속 열어둘 필요가 없어져요.
- 여러 기기 전환 — 폰에서 대화를 시작하고, 데스크톱에서 다시 이어받는 식입니다.
이 기능은 LangGraph 에이전트 서버가 필요해요. 로컬에서는 langgraph dev로 에이전트를 실행하고, LangSmith에 배포해서 사용할 수 있습니다.
핵심 개념
join/rejoin 패턴은 세 가지 핵심 메커니즘으로 이루어집니다.
| 메서드 / 옵션 | 용도 |
|---|---|
threadId |
관찰하려는 LangGraph 스레드에 스트림을 바인딩 |
onThreadId |
새로 생성된 스레드 ID를 영속화해서, 다시 마운트할 때 재연결 가능하게 함 |
stream.disconnect() |
에이전트가 서버에서 계속 실행되는 동안 클라이언트 쪽에서 스트림을 떠남 |
같은 threadId로 다시 마운트 |
해당 스레드의 진행 중인 작업에 다시 연결 |
여기서 중요한 함정이 하나 있어요. join/rejoin은 stream.disconnect()를 사용하지, stream.stop()을 쓰지 않습니다. 기본적으로 stream.stop()은 활성 실행(run)을 취소해요 — 클라이언트를 끊고 서버의 실행도 함께 취소하는 거죠. join/rejoin에서는 stop({ cancel: false })의 별칭인 stream.disconnect()를 호출해서, 떠나 있는 동안 에이전트가 계속 처리하게 해야 합니다. 앱 코드에서 명시적으로 실행을 취소하려면 stream.stop()이나 client.runs.cancel을 써요.
useStream 설정하기
가장 중요한 설정 단계는 threadId를 영속화하는 것입니다. 컴포넌트가 같은 스레드 ID로 다시 마운트되면, 스트림이 그 스레드의 현재 상태와 진행 중인 실행에 연결됩니다.
아래 코드는 타입 안전한 스트림 상태를 위해 useStream<typeof myAgent>를 사용합니다. Python 또는 JavaScript 백엔드의 타입 추론에 대해서는 프론트엔드 개요의 해당 섹션을 참고하세요.
import { useStream } from "@langchain/react";
import { useCallback, useState } from "react";
function Chat() {
const [connected, setConnected] = useState(true);
const [mountKey, setMountKey] = useState(0);
const [threadId, setThreadId] = useState<string | null>(
() => sessionStorage.getItem("activeThreadId"),
);
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "join_rejoin",
threadId,
onThreadId(id) {
setThreadId(id);
if (id) sessionStorage.setItem("activeThreadId", id);
},
});
const disconnect = useCallback(() => {
void stream.disconnect();
setConnected(false);
}, [stream]);
const rejoin = useCallback(() => {
setMountKey((key) => key + 1);
setConnected(true);
}, []);
return (
<div key={mountKey}>
<ConnectionStatus connected={connected} />
<MessageList messages={stream.messages} />
<ChatControls
stream={stream}
threadId={threadId}
connected={connected}
onDisconnect={disconnect}
onRejoin={rejoin}
/>
</div>
);
}
onThreadId에서 스레드 ID를 sessionStorage에 저장해 두기 때문에, 나중에 컴포넌트가 다시 만들어져도 같은 ID로 재연결할 수 있어요. Vue, Svelte, Angular에서는 각각의 반응형 패턴(ref, $state, signal)으로 같은 구조를 구현합니다.
메시지 제출
메시지는 평소처럼 제출하면 됩니다. 나중에 다시 마운트했을 때 같은 대화에 재연결되게 해 주는 건 바로 스레드 ID 바인딩이에요.
stream.submit({ messages: [{ type: "human", content: text }] });
스트림에서 나가기
실행을 취소하지 않고 스트림을 떠나려면 stream.disconnect()를 호출하세요. 에이전트는 서버에서 계속 처리합니다.
await stream.disconnect();
// equivalent to: await stream.stop({ cancel: false })
여기서 stream.stop()은 쓰면 안 돼요 — 기본적으로 서버에서 실행을 취소해 버리니까요. disconnect()를 호출한 뒤에는 다음과 같이 동작합니다.
stream.isLoading이false가 됩니다.- 여러분의
connected플래그도false로 바꿔줘야 해요. - 메시지 목록에는 연결 해제 시점까지 받은 메시지가 그대로 남아 있어요.
- 에이전트는 서버에서 계속 실행됩니다.
- 재참여하기 전까지 새 메시지를 받지 않습니다.
스트림 재참여하기
저장해 둔 스레드 ID로 스트림 컨슈머를 다시 마운트하면 재연결됩니다. React 데모에서는 mountKey를 올려서 다시 마운트를 유도하고, 다른 프레임워크에서는 그에 상응하는 재마운트나 조건부 렌더링 패턴을 쓰면 돼요.
setMountKey((key) => key + 1);
setConnected(true);
재참여 후에는 다음이 일어나요.
connected가true가 됩니다.- 떠나 있는 동안 생성된 메시지가 전달됩니다.
- 새 스트리밍 메시지가 실시간으로 다시 이어집니다.
- 에이전트가 아직 실행 중이면
stream.isLoading이true가 되고, 이미 끝났다면 최종 상태를 즉시 받아요.
모범 사례
- join/rejoin에는
disconnect(), 취소에는stop()— 페이지를 떠나거나 앱을 백그라운드로 보낼 때는stream.disconnect()를, 사용자용 "중지(Stop)"·"취소(Cancel)" 버튼에는stream.stop()(또는client.runs.cancel)을 사용하세요. - 항상 스레드 ID를 저장 — 스레드 ID가 없으면 재참여가 불가능해요. 컴포넌트 상태와 영속 저장소를 함께 써서 안전하게 보관하세요.
- 연결 상태를 명확히 표시 — 사용자가 실시간 업데이트를 받고 있는지, 스냅샷을 보고 있는지 항상 알 수 있어야 합니다.
- 가시성 변경 시 자동 재참여 — Page Visibility API를 이용해 사용자가 탭으로 돌아오면 자동으로 재참여하게 하세요.
- 합리적인 타임아웃 설정 — 재참여 시도가 너무 오래 걸리면 스레드 히스토리를 가져오는 방식으로 폴백하세요.
- 오래된 스레드 정리 — 사용자가 새로 시작하거나 백엔드가 스레드를 사용할 수 없다고 알려주면, 영속화된 스레드 ID를 제거하세요.