Server-Sent Events를 사용하여 Laravel에서 AI 응답 스트리밍하기
요약
Laravel 환경에서 Server-Sent Events(SSE)를 활용해 AI 챗봇의 응답을 스트리밍하는 방법을 설명합니다. WebSockets와 달리 추가 인프라 없이 서버에서 브라우저로 단방향 스트리밍을 구현하여 사용자 경험(UX)을 개선할 수 있습니다.
핵심 포인트
- SSE를 사용하면 LLM 응답을 토큰 단위로 실시간 스트리밍하여 UX를 개선할 수 있습니다.
- Laravel 11의 response()->eventStream()을 통해 별도 인프라 없이 구현 가능합니다.
- POST 요청 스트리밍을 위해 브라우저의 fetch API를 활용합니다.
- Nginx나 PHP-FPM의 버퍼링 설정이 스트리밍 동작을 방해할 수 있으니 주의해야 합니다.
이 시리즈의 첫 번째 게시물(https://dev.to/adityakdevin/adding-an-ai-chatbot-to-your-laravel-app-with-the-openai-api-177f)에서는 Laravel에 작동하는 AI 챗봇을 구축했습니다. 이 챗봇은 모든 사용자가 즉시 알아차리는 문제가 있었습니다. 메시지를 보내면 응답 전체가 생성되는 동안 스피너를 보며 5초 동안 기다려야 하는 것입니다.
LLM(대규모 언어 모델)은 텍스트를 토큰 단위로 생성합니다. 전체 완료를 기다린 후에 아무것도 보여주지 않으면, 가장 큰 UX 개선점인 스트리밍을 놓치는 것입니다. 5초가 걸리는 것과 동일한 답변이 스트리밍되면 약 300ms 만에 나타나기 시작합니다. 모델 자체가 빨라진 것은 아니지만, 사용자에게는 10배 더 빠르게 느껴집니다.
본 게시물에서는 **Server-Sent Events (SSE)**를 사용하여 챗봇을 업그레이드하여 응답 스트리밍 기능을 구현할 것입니다. WebSockets도, Pusher도, 추가 인프라스트럭처도 필요 없습니다. 오직 Laravel만 있으면 됩니다.
SSE를 사용하는 이유와 WebSockets가 아닌 이유?
WebSockets는 양방향 통신(bidirectional)이며 장시간 실행되는 서버(Reverb, Soketi) 또는 유료 서비스가 필요합니다. 채팅 완료의 경우 하나의 방향만 필요합니다: 요청 수명 주기 동안 서버 → 브라우저입니다. 이것이 바로 SSE의 목적이며, Laravel 11부터는 response()->eventStream()으로 프레임워크에 내장되어 있습니다.
일반적인 규칙: 존재 여부(presence), 타이핑 표시기(typing indicators), 다중 사용자 환경(multiplayer) → WebSockets. 하나의 AI 답변 스트리밍 → SSE.
Route::post('/chat/stream', ChatStreamController::class)
->middleware(['auth', 'throttle:20,1']);
3. 브라우저에서 스트림 읽기 (Reading the stream in the browser)
네이티브 EventSource API는 GET 요청만 지원하며, 우리는 메시지 본문(message body)을 POST해야 하므로 대신 fetch를 사용하여 스트림을 읽습니다:
<script>
async function streamChat(message, botEl) {
const res = await fetch('/chat/stream', {
...
</stream>은 Laravel의 기본 스트림 종료 마커입니다. 이를 필터링하거나 (endStreamWith 인수를 사용하여 사용자 정의할 수 있습니다).
이것으로 끝입니다. 메시지를 보내면 답변이 타이핑되는 것을 지켜보세요.
프로덕션 환경에서 스트리밍이 깨지는 경우 (Where streaming breaks in production)
대부분의 튜토리얼이 건너뛰는 부분입니다. 스트리밍은 php artisan serve에서는 즉시 작동하지만, 실제 서버에서는 신비롭게 모든 내용이 한 번에 도착합니다. 원인은 항상 PHP와 브라우저 사이에 있는 어딘가의 버퍼링(buffering) 때문입니다:
- nginx: 기본적으로 프록시된 응답을 버퍼링합니다. 위의
X-Accel-Buffering: no헤더는 응답별로 이를 비활성화하며; 또는 라우트에서proxy_buffering off;를 설정하세요. - PHP-FPM + 출력 버퍼링 (output buffering):
php.ini의output_buffering을 확인하세요. Laravel의eventStream은 매번yield후에 플러시(flush)되지만, 외부 버퍼가 여전히 이를 삼킬 수 있습니다. - Cloudflare / 로드 밸런서 (load balancers): 대부분 SSE 콘텐츠 타입을 존중하지만, 코드를 비난하기 전에 프로덕션 환경에서
curl -N을 사용하여 검증하세요.
디버그 팁: curl -N -X POST https://yourapp.test/chat/stream ...는 브라우저의 마법 없이 정확히 무엇이 언제 도착하는지 보여줍니다.
프로덕션 체크리스트 (Production checklist)
프로덕션 체크리스트 (Production checklist)
- 세션 잠금 해제(Release the session lock): 스트리밍 전에 반드시 잠금을 해제해야 합니다 (위에서 완료됨) — 이는 Laravel에서 SSE를 사용할 때 발생하는 가장 흔한 '내 앱이 멈춤' 버그입니다.
X-Accel-Buffering: no헤더를 nginx에 설정합니다 (위에서 완료됨).- 타임아웃(Timeouts): 스트림은
max_execution_time과 웹 서버의 전송 타임아웃보다 오래 지속될 수 있습니다. 둘 다 최악의 경우 생성 시간보다 길게 설정해야 합니다 (최대 토큰 수를 500으로 제한할 경우, 60초가 적절한 상한선입니다). - 스트림 중 오류(Mid-stream errors): 제너레이터 루프를 try/catch로 감싸고 친근한
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기