AI 채팅창을 직접 만들어보니, 진짜 중요한 건 스트리밍이었다
GPT나 Claude처럼 답변이 실시간으로 올라오는 AI 채팅창은 어떻게 만들까? 직접 구현하면서 알게 된 LLM 스트리밍 구조와 SSE, WebSocket의 차이, 그리고 SSE를 선택한 이유를 정리했습니다.
이번에 AI를 붙여서 간단한 채팅 기능을 만들어볼 일이 있었습니다.
처음에는 별로 어렵게 생각하지 않았습니다.
채팅창 만들고, 백엔드에서 LLM 호출하고, 받은 답변 보여주면 끝 아닌가?
그런데 직접 만들어보니 GPT나 Claude에서 너무 당연하게 쓰던 채팅창이 생각보다 단순하지 않았습니다.
특히 AI가 답변을 만드는 동시에 화면에 계속 보여주는 부분.
이번에 직접 구현하면서 알게 된 내용을 기록해봅니다.
먼저 구현한 내용을 프리뷰로 간단히 보여드릴게요.
1. 채팅창은 assistant-ui로 시작했습니다
채팅 UI를 처음부터 만들지는 않았습니다.
assistant-ui를 사용했습니다.
AI에게 assistant-ui 기반으로 원하는 채팅창을 만들어달라고 하면 기본적인 형태는 비교적 쉽게 만들 수 있습니다.
- 사용자 메시지
- AI 메시지
- 메시지 입력창
- 대화 목록
- 로딩 상태
- 스트리밍 응답 표시
처음 구조는 단순했습니다.
사용자
↓
assistant-ui
↓
Backend
↓
LLM
UI까지 만들었을 때는 LLM만 붙이면 거의 끝날 줄 알았습니다.
2. LLM은 OpenLLMs로 연결했습니다
LLM 호출은 제가 만든 OpenLLMs를 사용했습니다.
OpenLLMs는 제가 결제해서 사용하고 있는 Claude나 Codex 등의 구독을 OpenAI 호환 방식으로 호출해서 사용할 수 있도록 만든 프로젝트입니다.
이번 채팅에서도 별도의 LLM API를 직접 붙이는 대신 백엔드에서 OpenLLMs를 호출하도록 구성했습니다.
assistant-ui
↓
Backend
↓
OpenLLMs
↓
LLM
프론트에서 LLM을 직접 호출하지 않고 백엔드를 한 번 거치도록 했습니다.
여기까지 연결해서 질문을 보내고 답변을 받는 것까지는 어렵지 않았습니다.
문제는 그다음이었습니다.
3. 답변을 한 번에 받으니 AI 채팅 같지 않았습니다
가장 단순하게 만들면 이런 식입니다.
사용자 질문
↓
백엔드
↓
LLM 호출
↓
답변 생성 완료까지 대기
↓
완성된 답변 반환
↓
채팅창에 표시
기능적으로는 문제가 없습니다.
질문하면 답변도 잘 옵니다.
그런데 긴 답변을 생성하면 몇 초 동안 화면에 아무 변화가 없습니다.
그러다가 생성이 끝나는 순간 긴 답변 전체가 한꺼번에 나타납니다.
우리가 GPT나 Claude에서 익숙하게 보던 방식과는 꽤 다릅니다.
4. 여기서 필요한 게 스트리밍입니다
GPT나 Claude는 답변 전체를 만든 다음 한꺼번에 보내는 방식이 아닙니다.
LLM에서 생성되는 데이터를 작은 조각으로 받아서 화면에 계속 반영합니다.
대략 이런 느낌입니다.
안녕
↓
안녕하세요.
↓
안녕하세요. 무엇을
↓
안녕하세요. 무엇을 도와드릴까요?
실제로 데이터가 꼭 단어나 글자 단위로 오는 것은 아닙니다.
LLM에서 전달되는 작은 데이터 조각을 보통 chunk라고 부릅니다.
프론트에서는 이 chunk가 들어올 때마다 현재 표시하고 있는 AI 메시지 뒤에 계속 붙입니다.
LLM
↓
chunk
↓
chunk
↓
chunk
↓
chunk
↓
완료
사용자 입장에서는 AI가 실시간으로 글을 작성하고 있는 것처럼 보입니다.
여기까지 이해하고 나니 다음 문제가 생겼습니다.
이 데이터를 서버에서 브라우저까지 어떤 방식으로 계속 전달할 것인가?
여기서 SSE와 WebSocket을 만나게 됩니다.
SSE와 WebSocket
처음에는 둘 다 실시간 통신인데 무슨 차이가 있나 싶었습니다.
직접 구현하면서 가장 크게 느낀 차이는 통신 방향과 연결을 사용하는 방식이었습니다.
5. SSE는 서버에서 계속 내려보내는 방식
SSE는 Server-Sent Events의 약자입니다.
이름 그대로 서버가 클라이언트에게 이벤트를 계속 전달하는 데 적합합니다.
AI 채팅에 대입하면 이해하기 쉽습니다.
사용자
│
│ 질문
▼
서버
│
│ LLM 호출
▼
LLM
│
│ chunk
▼
서버
│
│ SSE
├────────→ chunk 1
├────────→ chunk 2
├────────→ chunk 3
├────────→ chunk 4
└────────→ 완료
│
▼
브라우저
사용자가 질문을 보내면 서버에서 LLM을 호출합니다.
그리고 LLM에서 응답이 들어올 때마다 서버가 그 내용을 클라이언트로 바로 전달합니다.
응답 전체가 완성될 때까지 기다릴 필요가 없습니다.
SSE의 특징
- HTTP 기반으로 동작합니다.
- 서버에서 클라이언트 방향의 이벤트 전달에 적합합니다.
- 텍스트 기반 스트리밍을 구현하기 편합니다.
- 일반적인 LLM 응답 스트리밍과 잘 맞습니다.
- WebSocket에 비해 구조를 단순하게 가져갈 수 있습니다.
다만 SSE 자체는 WebSocket처럼 하나의 연결에서 자유로운 양방향 메시징을 하기 위한 프로토콜은 아닙니다.
클라이언트에서 서버로 무언가 보내야 한다면 일반 HTTP 요청 등을 함께 사용할 수 있습니다.
그래서 구조가 보통 이런 느낌이 됩니다.
질문
Client ──────────→ Server
HTTP 요청
답변
Client ←────────── Server
SSE Stream
6. WebSocket은 양쪽이 계속 대화할 수 있습니다
WebSocket은 성격이 조금 다릅니다.
연결을 맺은 뒤 클라이언트와 서버가 같은 연결을 통해 양방향으로 메시지를 주고받을 수 있습니다.
┌────────────┐ ┌────────────┐
│ Client │ ←─────────────→ │ Server │
└────────────┘ WebSocket └────────────┘
단순히 LLM의 답변만 내려주는 것보다 복잡한 에이전트를 생각하면 차이가 좀 더 명확해집니다.
예를 들어 에이전트가 여러 작업을 수행한다고 해보겠습니다.
Agent → 검색을 시작합니다.
Agent → 검색 중...
Agent → 파일을 찾았습니다.
Agent → 파일을 분석하고 있습니다.
Agent → 실행하려면 승인이 필요합니다.
User → 승인
Agent → 실행합니다.
User → 중단
Agent → 작업을 중단했습니다.
이런 구조에서는 서버만 계속 데이터를 보내는 게 아닙니다.
작업이 진행되는 도중 사용자도 계속 서버에 명령을 보낼 수 있습니다.
- 승인
- 거절
- 중단
- 재실행
- 추가 명령
- 상태 변경
이런 상호작용이 많아질수록 WebSocket의 양방향 연결이 편해집니다.
7. SSE와 WebSocket 비교
제가 이번에 이해한 기준으로 정리하면 이렇습니다.
| 구분 | SSE | WebSocket |
|---|---|---|
| 기본 통신 | 서버 → 클라이언트 중심 | 클라이언트 ↔ 서버 |
| 기반 | HTTP | WebSocket 프로토콜 |
| 연결 유지 | 가능 | 가능 |
| 양방향 통신 | 별도 HTTP 요청 등과 조합 | 같은 연결에서 가능 |
| LLM 텍스트 스트리밍 | 잘 맞음 | 가능 |
| 구현 복잡도 | 비교적 단순 | 상대적으로 높음 |
| 사용자 중간 개입 | 단순한 경우 충분 | 복잡한 경우 유리 |
| 승인, 중단 등 실시간 제어 | 별도 요청과 조합 | 잘 맞음 |
| 단순 AI 채팅 | 적합 | 경우에 따라 과할 수 있음 |
| 복잡한 AI 에이전트 | 요구사항에 따라 가능 | 적합한 경우가 많음 |
8. 무조건 AI 에이전트는 WebSocket이라는 뜻은 아닙니다
이 부분은 구분할 필요가 있었습니다.
단순 채팅이면 SSE, 에이전트면 무조건 WebSocket이라고 나눌 수 있는 건 아닙니다.
에이전트가 여러 Tool을 실행하더라도 서버에서 상태를 계속 보여주기만 하면 SSE로 충분할 수 있습니다.
Agent 시작
↓
검색 중
↓
검색 완료
↓
Tool 실행
↓
Tool 완료
↓
답변 생성
↓
완료
이런 이벤트들을 SSE로 계속 내려주면 됩니다.
반대로 작업 중에 사용자가 자주 개입해야 한다면 이야기가 달라집니다.
Agent 실행
↓
승인 요청
↓
사용자 승인
↓
Agent 계속 실행
↓
사용자 중단
↓
Agent 중단
↓
사용자 수정 요청
↓
Agent 다시 실행
이런 식의 실시간 양방향 상호작용이 많다면 WebSocket을 고려할 이유가 커집니다.
결국 이름이 채팅인지 에이전트인지보다 어떤 통신이 필요한지를 보고 선택하는 게 맞았습니다.
저는 SSE를 선택했습니다
이번에 제가 만들던 기능은 비교적 단순했습니다.
사용자가 질문을 보냅니다.
LLM이 답변을 생성합니다.
생성되는 답변을 실시간으로 보여줍니다.
끝입니다.
작업 중간에 사용자가 승인하거나 거절할 것도 없고, 실행 중인 에이전트에게 새로운 명령을 계속 전달할 필요도 없었습니다.
그래서 WebSocket까지 사용할 이유가 크지 않았습니다.
SSE로 구현했습니다.
9. 실제 동작 구조
전체를 연결하면 이런 구조가 됐습니다.
┌───────────────┐
│ 사용자 │
└───────┬───────┘
│
│ 메시지 입력
▼
┌───────────────┐
│ assistant-ui │
└───────┬───────┘
│
│ 요청
▼
┌───────────────┐
│ Backend │
└───────┬───────┘
│
│ LLM 요청
▼
┌───────────────┐
│ OpenLLMs │
└───────┬───────┘
│
▼
┌───────────────┐
│ LLM │
└───────┬───────┘
│
│ 응답 생성
│
▼
chunk
│
chunk
│
chunk
│
▼
┌───────────────┐
│ Backend │
└───────┬───────┘
│
│ SSE Streaming
▼
┌───────────────┐
│ assistant-ui │
└───────┬───────┘
│
│ 들어오는 내용을
│ 계속 화면에 추가
▼
┌───────────────┐
│ 사용자 │
└───────────────┘
10. 순서대로 보면 더 단순합니다
- 사용자가 메시지를 입력합니다.
- 프론트에서 백엔드로 요청을 보냅니다.
- 백엔드에서 OpenLLMs를 통해 LLM을 호출합니다.
- LLM이 답변 생성을 시작합니다.
- 백엔드는 전체 답변이 완성될 때까지 기다리지 않습니다.
- 생성되는 데이터를 받는 즉시 스트림으로 전달합니다.
- 프론트에서는 들어오는 데이터를 계속 현재 AI 메시지에 추가합니다.
- LLM 응답이 끝나면 스트림도 종료합니다.
직접 만들어보고 나서
처음에는 AI 채팅을 굉장히 단순하게 생각했습니다.
채팅 UI + LLM API = AI 채팅
물론 크게 보면 맞습니다.
그런데 직접 만들어보니 그 사이에서 데이터가 어떻게 오가는지도 꽤 중요했습니다.
특히 평소 GPT나 Claude에서 아무 생각 없이 보고 있던 실시간 답변이 그냥 만들어지는 게 아니었습니다.
이번에 구현하면서 제가 잡은 기준은 이 정도입니다.
질문
↓
LLM 답변
↓
실시간으로 화면에 출력
이 정도라면
→ SSE
반대로,
Agent 실행
↓
상태 변경
↓
사용자 승인
↓
다시 실행
↓
사용자 중단
↓
다른 작업 진행
이런 실시간 양방향 상호작용이 많다면
→ WebSocket 고려
SSE가 더 좋은 것도 아니고 WebSocket이 더 좋은 것도 아니었습니다.
필요한 통신 방식에 맞는 걸 선택하면 됩니다.
이번에는 단순한 채팅이었기 때문에 SSE면 충분했습니다.
막상 직접 한번 만들어보고 나니 SSE와 WebSocket의 차이도 그제야 조금 명확해졌습니다.