본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 09. 11:08

서버리스 환경에서의 AI 응답 스트리밍: 내가 고생하며 배운 것들

요약

서버리스 환경에서 AI API 응답을 기다릴 때 발생하는 긴 지연 시간과 사용자 경험 저하 문제를 해결하는 방법을 다룹니다. Server-Sent Events(SSE)를 활용하여 AI의 토큰 생성 과정을 실시간으로 클라이언트에 스트리밍하는 구현 전략을 소개합니다.

핵심 포인트

  • 서버리스 함수의 타임아웃과 비용 문제를 고려한 설계 필요
  • 전체 응답 대기 방식은 사용자 경험(UX)을 심각하게 저해함
  • SSE(Server-Sent Events)를 통한 실시간 토큰 스트리밍 구현
  • Vercel 등 서버리스 환경에서도 스트리밍 파이프라인 구축 가능

저는 작은 웹 앱을 만드는 것을 좋아합니다. 어떤 느낌인지 아실 겁니다. 단 한 번의 npm create로 시작해서 한 달 동안 모든 주말을 쏟아붓게 되는 그런 사이드 프로젝트 말이죠. 몇 달 전, 저는 사용자의 노트를 바탕으로 요약을 생성하는 AI 모델을 사용하는 간단한 대시보드를 만들기로 했습니다. 거창한 것은 아니었습니다. 노트를 입력하면 불렛 포인트(bullet-point) 형태의 요약을 돌려받는 방식이었죠.

하지만 서버리스 아키텍처 (serverless architectures)의 현실이 저를 덮쳤습니다. AI API의 응답 시간, 그리고 연결 끊김 현상. 그리고 15초 동안 스피너 (spinner)만 바라보고 있는 사용자까지. 그것이 바로 오늘 제가 이야기하고 싶은 문제입니다.

진짜 문제

제 백엔드 (backend)는 단일 Vercel 서버리스 함수 (serverless function, Node.js)였습니다. AI API를 호출하고, 전체 응답이 올 때까지 기다린 다음, 클라이언트 (client)로 전송하는 방식이었죠. 간단해 보이지 않나요? 하지만 AI 모델, 특히 규모가 큰 모델들은 전체 응답을 반환하는 데 10~20초가 걸릴 수 있습니다. 그 시간 동안 서버리스 함수는 실행 밀리초 (millisecond) 단위로 비용이 청구되며, 사용자는 영겁처럼 느껴지는 로딩 스피너를 보게 됩니다.

5문단 분량의 노트를 가지고 처음 테스트했을 때가 기억납니다. 버튼을 클릭하고 물 한 잔을 떠온 뒤 돌아왔는데도 스피너는 여전히 돌고 있었습니다. 용납할 수 없는 경험이었죠.

시도했지만 실패했던 것들

프롬프트 (prompt) 최적화? 더 짧은 프롬프트를 시도해 보았지만, 모델이 생성하는 데 필요한 시간은 여전했습니다.

타임아웃 (timeout) 연장? 서버리스 함수에는 엄격한 제한이 있습니다 (Vercel의 경우 Pro 플랜은 60초, Hobby 플랜은 10초). 이는 사용자 경험 (user experience) 측면에서 아무런 해결책이 되지 못했습니다.

“처리 중...” 표시하기? 그것은 단지 미적인 부분일 뿐입니다. 사용자는 여전히 기다려야 합니다.

청크 (chunks) 단위로 가져오기? 일부 제공업체는 스트리밍 (streaming)을 지원하지만, 이를 서버리스 함수를 통해 어떻게 파이프 (pipe)할지 알아내야 했습니다. 게다가 클라이언트가 스트림 (stream)을 처리해야 했죠. 복잡성만 더해졌습니다.

백그라운드 작업 (queue) 방식? 이는 큐 (queue, 예: Bull, SQS)를 설정하고 프론트엔드 (frontend)에서 폴링 (polling)을 해야 함을 의미합니다. 사이드 프로젝트를 위해서라면? 인프라 (infra) 부담이 너무 컸습니다.

저에게는 절충안이 필요했습니다. 반응성이 느껴지면서도, 전체적인 아키텍처 (architecture)를 재설계할 필요가 없고, 여전히 서버리스 함수 (serverless functions)를 사용할 수 있는 무언가 말이죠 (왜냐하면 저는 Vercel에서 무료로 배포하기 때문입니다).

결국 성공한 방법: 서버리스에서의 스트리밍 (Streaming) + EventSource

저는 AI 제공업체 (provider)로부터 스트리밍을 받아들이고, 이를 Server-Sent Events (SSE)를 사용하여 클라이언트 (client)로 전달하기로 결정했습니다. 핵심 비결은 Vercel(및 유사한 플랫폼)의 서버리스 함수가 연결을 유지하며 데이터가 도착하는 대로 스트리밍할 수 있다는 점입니다. 제가 구현한 방법은 다음과 같습니다.

1단계: 스트리밍을 지원하는 AI 제공업체 선택하기

대부분의 현대적인 제공업체 (OpenAI, Anthropic 및 기타 다수)는 스트리밍 완료 (streaming completions)를 지원합니다. 전체 응답을 기다리는 대신, 토큰 (tokens)이 생성될 때마다 청크 (chunks)를 전송합니다. 저의 경우, stream: true 옵션과 함께 OpenAI API를 사용했습니다.

2단계: 스트리밍하는 서버리스 엔드포인트 (endpoint) 생성하기

저는 Next.js API 라우트 (Node.js 런타임)를 사용했지만, ReadableStream을 반환하는 모든 Node.js 서버리스 함수로도 이를 수행할 수 있습니다. 핵심 코드는 다음과 같습니다:

// pages/api/summarize.js
import OpenAI from 'openai';

...

이렇게 하면 토큰이 도착하는 동안 함수가 활성 상태를 유지합니다. 각 청크는 data: 라인이 포함된 SSE 이벤트로 전송됩니다. 그러면 클라이언트는 이러한 이벤트를 수신하여 UI를 점진적으로 업데이트할 수 있습니다.

3단계: EventSource를 이용한 클라이언트 측 소비

프론트엔드 (frontend)에서는 POST 요청을 보내는 EventSource 래퍼 (wrapper)를 만들었습니다. 잠깐 – 표준 EventSource는 GET 요청만 지원합니다. 그래서 대신 읽기 가능한 스트림 (readable stream)을 사용하는 fetch를 사용했습니다. 깔끔한 클라이언트 측 헬퍼 (helper) 함수는 다음과 같습니다:

async function streamSummary(text, onChunk, onDone, onError) {
  const response = await fetch('/api/summarize', {
    method: 'POST',
...

그 다음 React 컴포넌트에서는 다음과 같이 작성했습니다:

const [summary, setSummary] = useState('');
const [loading, setLoading] = useState(false);

...

짠. 사용자는 클릭 후 보통 1초 이내에 요약 내용이 단어 단위로 나타나는 것을 보게 됩니다. 체감 성능 (perceived performance)이 극적으로 향상되었습니다.

배운 점 및 트레이드오프 (Trade-offs)

  • 서버리스 타임아웃 (Serverless timeout)은 여전히 적용됩니다 – 만약 AI 제공업체가 함수의 타임아웃(예: 취미용 플랜의 10초)보다 오래 걸린다면, 504 에러를 받게 됩니다. 저는 더 빠른 모델을 사용하거나 더 넉넉한 플랜으로 전환하여 이를 완화했습니다. 일부 제공업체는 스트림 (stream)을 위한 "keep-alive" 옵션을 제공하기도 합니다.
  • 에러 핸들링 (Error handling)이 더 까다롭습니다 – 스트림이 중간에 실패할 경우 전략이 필요합니다: 부분적인 결과만 보여줄 것인가? 재시도할 것인가? 저는 지금까지 받은 내용을 보여주고 그것이 불완전하다는 점을 강조하는 방식을 선택했습니다.
  • 콜드 스타트 (Cold starts) – 비활성 상태 이후 첫 요청 시, 함수가 실행되는 데 추가 시간이 소요될 수 있습니다. 스트림은 여전히 작동하지만, 첫 번째 청크 (chunk)가 지연됩니다. 저는 5분마다 간단한 핑 (ping)을 보내 함수를 프리웜 (prewarm)합니다 (임시방편이지만 효과는 있습니다).
  • 보안 (Security) – 비용이 많이 드는 API를 호출하는 엔드포인트 (endpoint)를 노출하게 됩니다. 오남용을 방지하기 위해 속도 제한 (rate limiting)이나 인증 (예: 사용자 토큰)을 추가하세요.
  • 모든 AI 제공업체의 스트리밍 방식이 같지는 않습니다 – 해당 문서들을 확인하세요. 어떤 곳은 가공되지 않은 토큰 (raw tokens)을 보내고, 어떤 곳은 매번 JSON을 보냅니다.

다음에 다시 한다면 다르게 할 점

만약 제가 오늘 이 시스템을 다시 구축한다면, 스트리밍에 대한 오버헤드 (overhead)가 더 낮은 엣지 함수 (edge functions, 예: Cloudflare Workers 또는 Vercel Edge)를 살펴볼 것 같습니다. 엣지 함수는 Node.js의 버퍼링 (buffering) 문제 없이 SSE (Server-Sent Events)를 네이티브로 처리할 수 있습니다. 또한, 양방향 스트리밍 (bidirectional streaming)을 위해 전용 웹소켓 (WebSocket) 엔드포인트를 고려할 수도 있겠지만, 단순한 요약 앱에는 과할 수 있습니다.

이 방식을 사용하지 말아야 할 때

AI 응답이 항상 매우 작다면 (예: 예/아니오 답변), 스트리밍은 이점 없이 복잡성만 더합니다. 또한, 스트리밍 응답을 지원하지 않는 플랫폼(예: 스트리밍 지원 전의 기본 Netlify 함수)을 사용 중이라면 다른 계획이 필요합니다.

도구에 관한 참고 사항

저는 이것을 개인 프로젝트를 위해 만들었지만, 조사하는 과정에서 Interwest AI (ai.interwestinfo.com)라는 서비스를 우연히 발견했습니다. 이 서비스는 이러한 스트리밍 (streaming) 및 서버리스 (serverless)의 복잡성을 일부 추상화해 주는 것으로 보였습니다. 아직 사용해 보지는 않았지만, 더 즉시 사용 가능한 (turnkey) 솔루션을 원하신다면 눈여겨볼 만한 가치가 있습니다. 제가 여기서 보여드린 방식은 완전히 DIY (Do-It-Yourself) 방식이며 어떤 제공업체와도 작동합니다.

이것이 저의 이야기입니다. 서버리스 함수 (serverless functions)에서 AI 응답을 스트리밍하는 것은 제 앱을 "잠시만 기다려 주세요"라는 악몽에서 살아있는 것처럼 느껴지는 무언가로 바꾸어 놓았습니다. 위의 코드는 복사해서 바로 사용할 수 있습니다. 여러분의 API 키를 추가하고 프롬프트 (prompt)를 조정하기만 하면 됩니다.

여러분의 설정은 어떤 모습인가요? AI 응답을 스트리밍하고 계신가요, 아니면 여전히 예전 방식인 전체 응답 대기 (wait-for-all) 방식을 사용하고 계신가요?

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0