# AI 답변을 모든 화면에 스트리밍

> 챗봇, 코파일럿, AI 에이전트: 쓰는 대로 나타나고, 모든 탭과 기기에 전달되며, 새로고침 후에도 이어지는 답변.

Source: https://hibira.io/ko/use-cases/ai-streaming

챗봇, 코파일럿, AI 에이전트가 빠르게 느껴지는 건 모델이 쓰는 대로 답변이 화면에 나타날 때입니다. 대부분의 앱은 질문을 보낸 하나의 HTTP 응답으로 스트리밍합니다. 사용자가 페이지를 새로고침하거나, 두 번째 탭을 열거나, 휴대폰으로 옮기거나, 동료가 같은 답변을 보고 싶어 하면 이것만으로는 부족합니다. 긴 답변은 서버리스 함수의 실행 시간 제한을 넘기기도 합니다. 답변을 채널에 발행하면 모델의 작업과 브라우저의 연결을 분리할 수 있습니다. 구독 중인 모든 화면이 같은 텍스트를 보고, 연결이 끊긴 화면은 채널 기록에서 따라잡습니다. 흔히 쓰는 기술 스택에는 묶기, 순서 맞추기, 따라잡기를 맡는 오픈 소스 패키지가 있어서 직접 짜지 않고 몇 줄만 추가하면 됩니다.

## 사용자가 얻는 것

- 전체 답변을 기다리지 않고 단어 단위로 나타나는 답변.
- 모든 탭과 모든 기기, 공유 채팅의 모든 사람에게 같은 답변.
- 새로고침하거나 연결이 끊겨도 처음부터 다시 하지 않고 빠진 부분만 채우며 이어지는 답변.
- '중지' 버튼, 그리고 탭을 닫아도 계속 써지는 답변. 돌아오면 결과가 준비되어 있습니다.

![데모 어시스턴트: 같은 답변이 노트북과 휴대폰에 동시에 스트리밍되고, 휴대폰은 다시 연결된 뒤 따라잡습니다.](https://hibira.io/portal/articles/demo-ai-streaming-ko.webp)

## 사용하는 기능

- **비공개 채널**: 대화에 참여할 수 있는 사람만 답변을 읽을 수 있습니다. 확인은 여러분의 인증 엔드포인트에서 합니다.
- **채널 기록**(`pusher:rewind`, Hobby 이상 요금제, 앱마다 설정에서 켬): 다시 연결된 화면이 놓친 이벤트를 받습니다.
- **Hibira 패키지**: Vercel AI SDK, React, Vue, Nuxt, Laravel용. 토큰을 Free 한도 4 KB보다 작은 이벤트로 묶고, 번호를 붙이고, 순서대로 다시 맞춥니다.
- **여러분의 모델과 API 키**: 텍스트를 스트리밍하는 모델이면 무엇이든 됩니다. Hibira는 텍스트를 사용자에게 전달할 뿐입니다.

```text
sequence: 서버는 답변을 한 번 쓰고, Hibira가 열려 있는 모든 화면에 전달합니다
브라우저 | 여러분의 API | *Hibira
1 > 3: private-ai-chat-7 구독
1 > 2: POST /api/chat
2 >> 1: 202, 답변은 백그라운드에서 계속
2 > 3: 번호가 붙은 묶음 이벤트 | 50–100 ms마다 하나
3 > 1: 모든 화면에 텍스트 표시
1 > 3: 다시 연결된 뒤: pusher:rewind
3 >> 1: 놓친 이벤트
```

## 기술 스택에 맞는 패키지 고르기

| 사용 중인 것 | 패키지 | 브라우저 |
| --- | --- | --- |
| Vercel AI SDK `useChat` | [@hibira-io/ai-sdk](https://hibira.io/ko/guides/ai-sdk) | `HibiraChatTransport` |
| Anthropic, OpenAI, Gemini SDK와 React 또는 Vue | [@hibira-io/react](https://hibira.io/ko/guides/react), [@hibira-io/vue](https://hibira.io/ko/guides/vue) | `useStream()` |
| Laravel AI SDK 에이전트 | [hibira-io/laravel](https://hibira.io/ko/guides/laravel) | `useAgentStream()` |
| 그 밖의 언어나 프레임워크 | 아무 Pusher 서버 라이브러리 | pusher-js |

## Vercel AI SDK: useChat용 트랜스포트

`useChat()`은 그대로 씁니다. 답변이 HTTP 응답 대신 채널로 올 뿐입니다. 브라우저에서는:

```tsx
import { useChat } from '@ai-sdk/react';
import { HibiraChatTransport } from '@hibira-io/ai-sdk';

const transport = new HibiraChatTransport({ pusher, api: '/api/chat' });

export function Chat({ id, initialMessages }) {
  const { messages, sendMessage, status } = useChat({ id, messages: initialMessages, transport, resume: true });
  // render messages as usual
}
```

API 라우트는 바로 응답하고, 모델의 스트림은 백그라운드에서 발행합니다:

```ts
import { after } from 'next/server';
import { streamText, convertToModelMessages } from 'ai';
import { publishUIMessageStream } from '@hibira-io/ai-sdk/server';

export async function POST(req: Request) {
  const { id, messages, runId } = await req.json();
  const result = streamText({ model, messages: await convertToModelMessages(messages) });
  after(() =>
    publishUIMessageStream(pusher, {
      chatId: id,
      runId,
      stream: result.toUIMessageStream({ originalMessages: messages, onFinish: ({ messages }) => saveChat(id, messages) }),
    }),
  );
  return Response.json({ runId }, { status: 202 });
}
```

트랜스포트는 `private-ai-chat-{chatId}`를 구독하므로, 인증 엔드포인트에서 채팅 소유자에게만 이 채널을 허용하세요. `resume: true`를 주면 새로고침한 페이지가 아직 쓰는 중인 답변을 이어받습니다.

## React 또는 Vue와 아무 모델 SDK

`streamToChannel()`은 Anthropic, OpenAI, Gemini SDK가 돌려주는 스트림을 그대로 받습니다:

```js
import { streamToChannel } from '@hibira-io/react/server';

export async function POST(req) {
  const { conversationId, question } = await req.json();
  const stream = anthropic.messages.stream({ model, max_tokens: 1024, messages: [{ role: 'user', content: question }] });
  const id = crypto.randomUUID();
  streamToChannel(pusher, `private-conversation-${conversationId}`, stream, { id }).catch(console.error);
  return Response.json({ id });
}
```

```jsx
import { useStream } from '@hibira-io/react';

function Answer({ conversationId }) {
  const { text, done, error } = useStream(`private-conversation-${conversationId}`, { rewind: true });
  return <p aria-busy={!done}>{text}{error && <em> ({error})</em>}</p>;
}
```

`@hibira-io/vue`에도 같은 `useStream()`이 있고, Nuxt에서는 자동으로 가져옵니다. 중지 버튼을 넣으려면 `AbortController`의 `signal`을 `streamToChannel()`에 넘기고 중지 라우트에서 abort하세요.

## Laravel AI SDK 에이전트

에이전트를 큐에 넣고 그 이벤트를 사용자의 비공개 채널로 브로드캐스트합니다. 텍스트, 추론, 도구 호출, 승인 요청이 순서대로 도착하고, 요금제의 이벤트 크기보다 큰 것은 나뉘어 옵니다:

```php
use Hibira\Laravel\Ai\AgentChannel;

(new SalesCoach)->broadcastOnQueue($request->input('prompt'), AgentChannel::user($request->user()));
```

```jsx
const { text, status, toolCalls, approvals } = useAgentStream(`private-hibira-ai.user.${user.id}`, { rewind: true });
```

`status`는 `streaming`, `awaiting_approval`, `done`, `error` 중 하나이므로 에이전트가 무엇을 하는지 페이지에 보여 줄 수 있습니다.

## 에이전트 단계와 승인

에이전트 작업의 각 단계를 보여 주거나, 위험한 도구를 실행하기 전에 사람에게 묻거나, MCP 클라이언트 안의 에이전트가 진행 상황을 알리게 하려면 [AI 에이전트: 실시간 진행 단계, 승인, 알림](https://hibira.io/ko/use-cases/ai-agents)을 보세요. 같은 채널을 쓰므로 스트리밍 중인 답변과 에이전트의 단계를 한 페이지에 함께 보여 줄 수 있습니다.

## 패키지 없이

아무 Pusher 서버 라이브러리에서든 모델이 쓴 내용을 100 ms마다 보내고 묶음마다 번호를 붙이면, 브라우저가 순서대로 맞추고 빠진 묶음을 알아챌 수 있습니다:

```python
seq, buffer, last = 0, "", time.monotonic()
for delta in model_stream:
    buffer += delta
    if time.monotonic() - last >= 0.1 or len(buffer) >= 1000:
        pusher_client.trigger(f"private-chat-{chat_id}", "ai-delta", {"id": answer_id, "seq": seq, "text": buffer})
        seq, buffer, last = seq + 1, "", time.monotonic()
pusher_client.trigger(f"private-chat-{chat_id}", "ai-done", {"id": answer_id, "seq": seq, "text": buffer})
```

완성된 답변은 데이터베이스에 저장하고, 페이지를 열거나 다시 연결될 때 한 번 불러오세요. 큐 워커로 같은 패턴을 쓰는 방법은 [오래 걸리는 작업의 진행률](https://hibira.io/ko/use-cases/job-progress)을 참고하세요.

## 팁

- 몇 개의 화면이 받든 한 번의 발행은 메시지 1건입니다. 초당 50토큰이라면 20초짜리 답변은 토큰마다 보내면 약 1,000건, 100 ms 묶음이면 약 200건입니다.
- 각 이벤트는 요금제의 이벤트 크기 한도보다 작게 유지하세요(Free는 4 KB, [요금](https://hibira.io/ko/pricing) 참고). 패키지는 3,500바이트 이하로 맞추고 더 큰 것은 나눕니다.
- 채널 기록은 Hobby 이상 요금제에서 쓸 수 있습니다. 꺼져 있어도 패키지는 스트리밍하며, 답변 일부를 놓친 화면은 깨진 내용 대신 '불완전'으로 표시합니다.
- 새로고침하지 않는 탭 하나뿐이라면 일반 HTTP 스트리밍으로 충분합니다. 답변이 새로고침을 견뎌야 하거나, 여러 기기나 여러 사람에게 가야 하거나, 백그라운드 작업에서 나올 때 채널을 쓰세요.
- 긴 답변이나 에이전트 작업이 끝나기 전에 사용자가 앱을 닫았다면 [푸시 알림 가이드](https://hibira.io/ko/guides/push-notifications)로 푸시를 보내세요.
