How to Create Real-Time AI Chat Streaming in Minutes with Convex Components
Quick Summary
Convex의 Persistent Text Streaming 컴포넌트를 쓰면 WebSocket이나 SSE를 직접 다루지 않고도 React 채팅 앱에 ChatGPT식 실시간 응답 스트리밍을 빠르게 붙일 수 있다는 튜토리얼입니다.
🖼️ 인포그래픽
🖼️ 4컷 인포그래픽
💡 한 줄 요약
Convex의 Persistent Text Streaming 컴포넌트를 쓰면 WebSocket이나 SSE를 직접 다루지 않고도 React 채팅 앱에 ChatGPT식 실시간 응답 스트리밍을 빠르게 붙일 수 있다는 튜토리얼입니다.
📌 핵심 요약
- 글은 Convex Components가 프론트엔드 컴포넌트처럼 백엔드 로직까지 선언적이고 재사용 가능한 단위로 제공한다는 점에서 출발하며, 이를 통해 실시간 기능 구현의 복잡성을 줄일 수 있다고 설명합니다.
- @convex-dev/persistent-text-streaming 컴포넌트는 전체 응답을 한 번에 반환하는 전통적 API와 달리 텍스트를 부분적으로 전송해 사용자가 답변 생성을 즉시 확인할 수 있게 합니다.
- 예제는 Convex Chef로 생성한 React AI 채팅 앱에 스트리밍을 추가하는 흐름을 따르며, 처음에는 익명 로그인 후 일반적인 요청-응답 방식으로 동작하는 채팅 인터페이스를 확인합니다.
- 백엔드에서는 패키지 설치, convex.config에서 컴포넌트 등록, /chat-stream POST HTTP 라우트 설정, PersistentTextStreaming.stream() 호출을 통해 스트리밍 응답을 구성합니다.
- 프론트엔드에서는 @convex-dev/persistent-text-streaming/react의 useStream 훅을 사용해 streamId에 연결된 텍스트를 구독하고, 스트리밍 중에는 부분 텍스트와 활성 표시를 보여주며 완료 후에는 저장된 메시지 내용을 표시합니다.
🧩 주요 포인트
- 글은 Convex Components가 프론트엔드 컴포넌트처럼 백엔드 로직까지 선언적이고 재사용 가능한 단위로 제공한다는 점에서 출발하며, 이를 통해 실시간 기능 구현의 복잡성을 줄일 수 있다고 설명합니다.
- @convex-dev/persistent-text-streaming 컴포넌트는 전체 응답을 한 번에 반환하는 전통적 API와 달리 텍스트를 부분적으로 전송해 사용자가 답변 생성을 즉시 확인할 수 있게 합니다.
- 예제는 Convex Chef로 생성한 React AI 채팅 앱에 스트리밍을 추가하는 흐름을 따르며, 처음에는 익명 로그인 후 일반적인 요청-응답 방식으로 동작하는 채팅 인터페이스를 확인합니다.
- 백엔드에서는 패키지 설치, convex.config에서 컴포넌트 등록, /chat-stream POST HTTP 라우트 설정, PersistentTextStreaming.stream() 호출을 통해 스트리밍 응답을 구성합니다.
- 프론트엔드에서는 @convex-dev/persistent-text-streaming/react의 useStream 훅을 사용해 streamId에 연결된 텍스트를 구독하고, 스트리밍 중에는 부분 텍스트와 활성 표시를 보여주며 완료 후에는 저장된 메시지 내용을 표시합니다.
🧠 상세 정리
1. Convex Components가 해결하려는 문제
원문은 ChatGPT처럼 답변이 점진적으로 나타나는 채팅 경험을 만들고 싶지만 WebSocket이나 SSE 같은 저수준 통신 프로토콜을 직접 다루고 싶지 않은 상황에서 출발합니다. Convex Components는 React, Vue, Angular의 컴포넌트 구조나 Spring, NestJS의 모듈식 단위와 비슷한 개념으로 설명됩니다. 다만 Convex는 단순한 서버 프레임워크를 넘어 클라이언트-서버 구조 전체를 재사용 가능한 모듈로 추상화한다는 점이 강조됩니다. 특히 백엔드 로직까지 선언적이고 재사용 가능한 컴포넌트로 제공된다는 점이 Convex의 강력한 특징으로 제시됩니다.
2. Persistent Text Streaming 컴포넌트의 역할
튜토리얼의 중심 컴포넌트는 @convex-dev/persistent-text-streaming입니다. 이 컴포넌트는 일반적인 API처럼 완성된 응답 전체를 한 번에 돌려주는 대신, ChatGPT 응답처럼 텍스트를 실시간으로 조금씩 출력할 수 있게 해줍니다. 사용자는 전체 메시지가 완성될 때까지 기다리지 않고 부분 응답을 바로 볼 수 있으므로 더 빠르고 상호작용적인 경험을 얻게 됩니다. 원문은 이 기능을 직접 구현하려면 보통 SSE나 WebSocket 같은 프로토콜을 다루어야 하지만, Convex의 내장 컴포넌트를 사용하면 그런 저수준 구현을 건너뛸 수 있다고 설명합니다.
3. Convex Chef로 만든 기본 채팅 앱
실습은 Convex Chef를 사용해 AI 채팅 기능이 포함된 React 애플리케이션을 빠르게 생성하는 방식으로 진행됩니다. 사용자가 프롬프트를 입력하고 잠시 기다리면 채팅 인터페이스를 가진 애플리케이션이 자동 생성되며, 완성된 예제 코드는 GitHub 저장소로도 제공된다고 원문은 언급합니다. 생성된 앱에서는 'Sign in anonymously'를 클릭해 익명 로그인 후 AI와 대화를 시작할 수 있습니다. 그러나 이 초기 상태의 인터페이스는 아직 스트리밍이 적용되지 않아, 응답이 점진적으로 나타나는 것이 아니라 완성된 형태로 한 번에 전달되는 전통적인 요청-응답 패턴을 보여줍니다.
4. 패키지 설치와 Convex 앱 등록
스트리밍 기능을 붙이는 첫 단계는 npm install @convex-dev/persistent-text-streaming 명령으로 패키지를 설치하는 것입니다. 이어서 convex.config 파일에서 defineApp을 가져오고, @convex-dev/persistent-text-streaming/convex.config에서 persistentTextStreaming을 불러온 뒤 app.use(persistentTextStreaming)으로 컴포넌트를 등록합니다. 원문은 defineApp()이 Convex 앱에 재사용 가능한 컴포넌트를 붙이는 베타 API라고 설명합니다. 이 방식은 인증, 결제, 채팅 같은 기능을 defineSchema와 defineFunctions를 직접 구성하지 않고 모듈식으로 추가할 수 있게 하지만, 실험적 API이므로 프로덕션 환경에서는 주의해서 사용해야 한다고 덧붙입니다.
5. /chat-stream HTTP 라우트 구성
컴포넌트를 등록한 뒤에는 Convex의 httpRouter를 사용해 HTTP 엔드포인트를 설정합니다. 예제에서는 ./chat에서 streamChat 핸들러를 가져오고, /chat-stream 경로에 POST 메서드로 라우트를 등록합니다. 이 streamChat 핸들러는 클라이언트가 보낸 요청을 받아 실시간 텍스트 스트리밍을 수행하는 역할을 합니다. 원문은 이 엔드포인트가 AI 응답을 클라이언트로 스트리밍하는 통로가 된다고 설명하며, 이후 프론트엔드의 useStream 훅이 이 URL을 사용해 스트림을 구독하는 구조로 이어집니다.
6. 기본 백엔드 스트리밍 패턴
백엔드 예제에서는 PersistentTextStreaming 클래스를 생성하고, components.persistentTextStreaming을 넘겨 스트리밍 컴포넌트를 초기화합니다. streamChat은 httpAction으로 정의되며 요청 본문에서 streamId, conversationId, userMessage를 읽습니다. 내부의 generateChat 함수는 chunkAppender를 받아 문자열을 한 글자씩 추가하고, 각 글자 사이에 짧은 지연을 넣어 실제로 텍스트가 흐르는 듯한 효과를 냅니다. 마지막에는 persistentTextStreaming.stream(ctx, request, streamId, generateChat)을 호출해 HTTP 스트리밍 응답을 처리하고, CORS 관련 헤더인 Access-Control-Allow-Origin과 Vary도 설정합니다.
7. 대화 기록을 반영한 완성형 백엔드 흐름
원문은 더 완성된 구현으로 streamId에 해당하는 메시지를 조회하고, 해당 메시지가 속한 conversationId를 기준으로 전체 대화 기록을 가져오는 방식을 제시합니다. 그 뒤 사용자 역할의 메시지만 필터링해 가장 최근 사용자 메시지를 찾고, 메시지가 없으면 기본 인사말을 스트리밍합니다. 사용자 메시지가 있으면 그 내용을 바탕으로 응답 문자열을 만들고, 앞선 예제처럼 글자 단위로 chunkAppender에 전달합니다. 스트리밍이 끝나면 markStreamComplete mutation을 호출해 메시지를 완료 상태로 표시하고 최종 내용을 저장하며, 오류가 발생하면 콘솔에 기록한 뒤 오류 안내 문구를 스트림에 추가합니다.
8. React useStream 훅으로 스트림 표시하기
프론트엔드에서는 @convex-dev/persistent-text-streaming/react 패키지의 useStream 훅을 사용해 스트리밍 텍스트를 UI에 반영합니다. 예제의 StreamingMessage 컴포넌트는 VITE_CONVEX_URL을 기반으로 Convex API URL의 .convex.cloud 도메인을 .convex.site로 바꿔 HTTP 액션 URL을 만듭니다. useStream에는 스트림 본문을 가져오는 api.chat.getStreamBody, /chat-stream URL, 자동 실행 여부를 나타내는 isDriven, 그리고 message.streamId가 전달됩니다. 스트리밍 중에는 훅이 반환한 text를 표시하고, 그렇지 않을 때는 저장된 message.content를 보여주며, 활성 상태일 때는 깜박이는 커서 모양의 시각적 표시를 추가합니다.
9. 자동 시작과 수동 시작 옵션
원문은 useStream의 세 번째 인자인 isDriven이 스트리밍 동작을 제어한다고 설명합니다. message.isStreaming이 true인 새 메시지에서는 isDriven을 true로 두어 데이터가 준비되는 즉시 스트리밍을 시작할 수 있습니다. 반대로 isDriven을 false로 설정하면 스트리밍은 자동으로 시작되지 않고, 애플리케이션의 별도 조건이나 버튼 클릭 같은 로직에 따라 시작되도록 만들 수 있습니다. 이 구분은 같은 스트리밍 인프라를 사용하더라도 어떤 메시지가 즉시 재생되어야 하는지, 어떤 메시지는 사용자의 명시적 동작을 기다려야 하는지를 UI 흐름에 맞게 조정할 수 있게 합니다.
10. streamId로 생성과 소비를 연결하는 구조
마지막 흐름에서는 PersistentTextStreaming을 제대로 활용하려면 백엔드가 메시지를 만들 때 streamId를 발급하고, 프론트엔드가 그 ID를 사용해 실시간 응답을 구독해야 한다고 설명합니다. 사용자가 메시지를 보내면 백엔드는 사용자 메시지를 저장하고, 고유한 streamId를 가진 빈 assistant 메시지를 생성하는 방식으로 스트리밍 대상 메시지를 준비합니다. 이후 /chat-stream 엔드포인트는 해당 streamId를 기준으로 실제 텍스트를 흘려보내고, React 컴포넌트는 같은 streamId를 useStream에 넘겨 화면을 갱신합니다. 제공된 원문은 이 연결 구조를 설명한 뒤 sendMessage mutation 예제로 들어가는 부분에서 끝나므로, 그 이후 구현 세부는 본문 범위 안에서 확인되지 않습니다.
🧾 핵심 주장 / 시사점
- 이 글의 핵심은 스트리밍 자체보다, 백엔드 기능을 설치 가능한 컴포넌트로 다루어 애플리케이션 코드가 메시지 생성·상태 표시·스트림 구독 같은 상위 흐름에 집중하게 만든다는 점입니다.
- streamId를 중심으로 백엔드의 빈 assistant 메시지 생성과 프론트엔드의 useStream 구독을 연결하는 구조가 중요하며, 이 ID가 스트리밍 응답의 생성 지점과 표시 지점을 묶어주는 역할을 합니다.
- 예제는 실제 AI 모델 호출보다 스트리밍 배관을 보여주는 데 초점을 둡니다. 따라서 원문에서 제시된 응답 생성 부분은 데모 문자열 기반이며, 모델 연동은 별도로 붙일 수 있는 지점으로만 언급됩니다.
✅ 액션 아이템
- Convex Components를 프론트엔드·백엔드 공용 단위로 해석해 채팅 앱의 실시간 응답 플로우를 선언적으로 재설계한다.
- 백엔드에서 패키지 설치, convex.config 등록, /chat-stream POST 라우트, PersistentTextStreaming.stream() 순으로 적용해 청크 기반 응답 체인을 구성한다.
- 클라이언트는 streamId 기준으로 useStream을 구독해 스트리밍 중 부분 텍스트와 활성 표시를 동기화하고 완료 후 저장 메시지 표시를 일관되게 처리한다.
❓ 열린 질문
- 부분 전송 스트리밍으로 전환하면 기존 단건 응답 대비 체감 지연은 어떤 시점에서 가장 크게 줄어드는가?
- 익명 로그인 기반 초기 인터페이스에서 streamId의 소유권과 사용자별 메시지 연결 규칙은 무엇으로 정의하는 것이 적절한가?
- WebSocket/SSE 구현 없이 PersistentTextStreaming.stream()를 쓴 뒤 연결 안정성과 전달 완결성을 운영 환경별로 어떻게 검증할 것인가?