- 공유 링크 만들기
- X
- 이메일
- 기타 앱
Featured Post
작성자:
Iros
- 공유 링크 만들기
- X
- 이메일
- 기타 앱

며칠 전에 후배 개발자랑 커피 마시다가 이런 얘기를 들었어요. “요즘 AI 바이브 코딩이 좋다는데, 실제 서비스에 붙이려면 뭐부터 봐야 해요?” 하고요. 그 말을 듣는데 괜히 웃음이 나더라고요. 저도 개발을 20년쯤 하다 보니 새로운 기술을 볼 때 설레는 마음보다 “이거 운영에서 안 터지나?”부터 보게 되거든요. 그런데 Vercel AI SDK는 좀 달랐어요. 특히 스트리밍은 실제 사용자 경험을 바꾸는 힘이 있더라고요. 오늘은 제가 최근에 만든 여행 추천 챗봇 프로젝트에서 Vercel AI SDK로 스트리밍 구현하면서 겪었던 일들, 삽질했던 부분, 그리고 써보니까 알게 된 작은 팁들을 편하게 풀어볼게요.
내가 스트리밍에 꽂힌 이유, 사용자는 기다리는 방식에 민감하더라
처음부터 거창하게 시작한 건 아니었어요. 그냥 Next.js로 간단한 여행 추천 챗봇을 만들고 있었고, 사용자가 “부산 2박 3일 가족 여행 코스 짜줘”라고 물어보면 AI가 답을 만들어주는 정도였죠. 처음에는 평범하게 fetch로 요청 보내고, 응답이 다 오면 화면에 한 번에 보여주는 방식으로 만들었어요.
기능은 됐어요. 문제는 분위기였어요. 사용자가 질문을 던지고 나면 화면에는 “로딩 중...”만 덩그러니 떠 있는 거예요. 3초는 괜찮아요. 5초도 뭐, 참을 만해요. 그런데 여행 일정처럼 답변이 길어지면 10초 가까이 걸릴 때도 있더라고요. 그때부터는 사용자가 불안해합니다. “이거 멈춘 거 아냐?” 하는 느낌이죠.
그래서 Vercel AI SDK 스트리밍을 붙여봤어요. 사실 붙이기 전에는 조금 귀찮을 줄 알았는데, 생각보다 코드가 단순해서 살짝 허무할 정도였습니다.
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages,
temperature: 0.7,
});
return result.toDataStreamResponse();
}
이 정도만 해도 프론트에서는 응답이 청크 단위로 흘러들어오고, 사용자는 글자가 하나씩 쌓이는 걸 보게 됩니다. 별거 아닌 것 같죠? 그런데 이게 생각보다 큽니다. 사람은 기다리는 시간 자체보다 “아무 일도 안 일어나는 것 같은 시간”을 더 싫어하거든요.
비스트리밍일 때는 사용자가 화면을 멍하니 보고 있다가 새로고침을 누르거나, 다시 질문을 보내는 경우가 꽤 있었어요. 그런데 스트리밍으로 바꾸고 나서는 반응이 달라졌습니다. 답변이 아직 끝나지도 않았는데 사용자가 이미 읽기 시작해요. “아, 지금 뭔가 만들어지고 있구나” 하고 받아들이는 거죠. 이건 숫자로도 느껴졌고, 그냥 옆에서 테스트하는 사람들 표정만 봐도 알겠더라고요.
| 비교 항목 | 일반 응답 방식 | Vercel AI SDK 스트리밍 |
|---|---|---|
| 첫 화면 반응 | 응답이 끝날 때까지 로딩만 표시 | 첫 토큰이 오면 바로 문장이 쌓이기 시작 |
| 사용자 체감 | 멈춘 것처럼 느껴질 수 있음 | AI가 실시간으로 답하는 느낌이 남 |
| 구현 난이도 | 단순하지만 답답함 | SDK 도움을 받으면 생각보다 간단함 |
| 운영 시 신경 쓸 부분 | 응답 실패 처리 정도 | 중간 끊김, 취소, 타임아웃 처리가 중요함 |
실제로 운영처럼 써보니 에러는 꼭 이상한 타이밍에 터지더라
개발할 때는 늘 그렇잖아요. 내 컴퓨터에서는 잘 됩니다. 테스트 몇 번 해보면 “오, 이 정도면 됐네” 싶어요. 그런데 다른 사람이 쓰기 시작하면 이상하게 문제가 하나씩 나옵니다. AI 스트리밍 구현도 마찬가지였어요.
처음에는 스트리밍이 중간에 뚝 끊기는 일이 있었습니다. 문장이 “제주도 여행은 첫날 공항에 도착한 뒤...” 여기서 멈춰요. 처음 봤을 때는 프론트 문제인 줄 알았어요. 상태 관리가 꼬였나, 네트워크가 불안정한가, 별생각을 다 했죠. 그런데 서버 로그를 보니 답은 꽤 단순했습니다. rate limit이었어요.
rate limit은 조용히 오지 않는다, 꼭 애매하게 끊긴다
OpenAI API를 쓰다 보면 사용량이나 호출 빈도에 따라 429 에러를 만나게 됩니다. 일반 응답 방식이면 그냥 에러 메시지가 떨어지니까 알아차리기 쉬워요. 그런데 스트리밍에서는 조금 더 얄밉습니다. 앞부분은 나오다가 중간에 멈춘 것처럼 보일 수 있거든요.
제가 봤던 로그는 대충 이런 느낌이었어요.
스트림 에러 발생: RateLimitError: 429 요청 한도를 초과했습니다
요청 경로: /api/chat
모델: gpt-4o
사용자 메시지 길이: 1842자
이걸 보고 나서야 “아, 프론트 문제가 아니었구나” 싶었습니다. 그래서 서버 쪽에서 에러를 조금 더 명확하게 잡고, 사용자에게도 너무 차가운 메시지가 아니라 상황을 알려주는 식으로 바꿨어요.
const controller = new AbortController();
const result = streamText({
model: openai('gpt-4o'),
messages,
abortSignal: controller.signal,
onError({ error }) {
console.error('스트림 에러 발생:', error);
},
});
return result.toDataStreamResponse();
여기서 중요한 건 단순히 로그만 찍는 게 아니에요. 운영에서는 “왜 끊겼는지”를 나중에 추적할 수 있어야 합니다. 사용자 질문 길이, 모델 이름, 요청 시간, 사용자 식별자 정도는 남겨두는 게 좋아요. 물론 개인정보는 조심해야 하고요. 저도 처음에는 로그를 너무 대충 남겼다가, 나중에 원인 찾느라 시간을 꽤 날렸습니다.
사용자가 중간에 취소하면 진짜로 멈춰야 한다
챗봇을 만들다 보면 사용자가 답변을 기다리다가 “아, 질문 잘못했다” 싶어서 다시 입력하는 경우가 많아요. 특히 여행 추천은 그래요. “오사카 3박 4일”이라고 썼다가 갑자기 “아니, 부모님 모시고 가는 일정으로 다시” 이렇게 바꾸고 싶어 하거든요.
처음에는 사용자가 취소 버튼을 누르면 화면에서만 로딩을 없앴어요. 그러면 겉으로 보기엔 멈춘 것처럼 보이죠. 그런데 서버에서는 여전히 AI 응답이 생성되고 있었습니다. 이건 비용도 비용이고, 서버 리소스도 아깝습니다. 뭐랄까, 수도꼭지는 계속 틀어놓고 컵만 치운 느낌이에요.
그래서 abortSignal을 제대로 연결했습니다. 사용자가 “중단”을 누르면 controller.abort()로 요청 자체를 끊어줘야 해요.
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 30000);
const result = streamText({
model: openai('gpt-4o'),
messages,
abortSignal: controller.signal,
});
이렇게 해두면 30초가 지나도 응답이 끝나지 않을 때 자동으로 끊을 수도 있고, 사용자가 직접 취소했을 때도 깔끔하게 멈출 수 있습니다. 솔직히 이 부분은 꼭 넣는 걸 추천해요. 데모에서는 없어도 되지만, 실제 서비스 느낌으로 가려면 취소 처리는 거의 필수에 가깝습니다.
타임아웃은 친절함의 문제이기도 하다
AI 모델이 가끔 깊은 생각에 빠질 때가 있어요. 물론 진짜 생각은 아니지만, 체감상 그래요. 특히 “도쿄 4박 5일 일정인데, 아이 둘 데리고 가고, 비 오는 날 대체 코스도 넣고, 지하철 이동 적게 해줘” 같은 요청은 답변이 길어질 수밖에 없습니다.
그런데 사용자는 40초, 50초를 잘 기다리지 않아요. 저라도 안 기다립니다. 그래서 일정 시간이 넘어가면 “답변이 길어지고 있어요. 질문을 조금 나눠서 다시 시도해볼까요?” 같은 안내를 주는 편이 훨씬 낫더라고요.
- 제가 스트리밍 붙일 때 실제로 확인하는 체크리스트입니다
- □ 429 에러가 났을 때 사용자에게 자연스러운 안내를 보여주는가
- □ 사용자가 중단 버튼을 눌렀을 때 API 요청도 같이 끊기는가
- □ 30초 이상 길어지는 요청에 대한 타임아웃 기준이 있는가
- □ 서버 로그에서 모델명, 요청 시간, 에러 원인을 확인할 수 있는가
- □ 프론트에서 스트림 완료 상태와 중단 상태를 구분하는가
- □ 긴 답변이 나올 때 화면 스크롤이 자연스럽게 따라가는가
프론트 UI는 기술보다 기분이 중요할 때가 있다
스트리밍이 기술적으로 잘 돌아간다고 해서 사용자 경험이 바로 좋아지는 건 아니더라고요. 화면에 글자가 찍히긴 하는데 뭔가 뚝뚝 끊겨 보이거나, 갑자기 긴 문장이 한 번에 확 붙으면 묘하게 어색합니다. 이런 건 성능 문제가 아니라 감각의 문제에 가까워요.
너무 빠른 출력은 오히려 정신없다
useChat을 쓰면 기본적으로 들어오는 응답을 알아서 화면에 반영해줍니다. 편하죠. 그런데 토큰이 빠르게 들어올 때는 글자가 우르르 쏟아지는 느낌이 날 때가 있어요. 처음에는 “빠르면 좋은 거 아닌가?” 했는데, 옆자리 동료가 한마디 하더라고요. “이거 읽기도 전에 지나가는 느낌인데요?”
그 말 듣고 조금 뜨끔했습니다. 그래서 표시 쪽에 살짝 완충을 줬어요. 실제 서비스에서는 상황에 따라 다르지만, 아주 짧은 지연이나 스크롤 제어만 넣어도 훨씬 부드럽게 느껴집니다.
const { messages, append, isLoading } = useChat({
onFinish: () => {
setLoading(false);
},
onError: () => {
setErrorMessage('아이쿠, 연결이 잠깐 불안정했어요. 다시 한번 물어봐 주세요.');
},
});
코드만 보면 별거 없죠. 그런데 저는 이런 작은 문구가 꽤 중요하다고 봐요. “오류가 발생했습니다”보다 “아이쿠, 연결이 잠깐 불안정했어요”가 훨씬 덜 차갑습니다. 특히 AI 챗봇은 대화형 서비스라서, 에러 메시지도 대화처럼 보여야 어색하지 않더라고요.
에러 메시지도 사람 말처럼 보여주는 게 좋다
스트림이 중간에 끊기면 사용자 입장에서는 되게 찝찝합니다. 답변이 완성된 건지, 끊긴 건지 알 수 없거든요. 그래서 저는 중간 실패가 발생하면 기존 답변 아래에 짧게 안내를 붙이는 방식으로 처리했습니다.
죄송해요. 답변을 만드는 중에 연결이 잠깐 끊겼어요.
방금 질문을 조금 짧게 나눠서 다시 보내주시면 더 안정적으로 답할 수 있어요.
이런 문구는 기술적으로 대단한 건 아닌데, 사용자 불만을 꽤 줄여줍니다. 개발자는 로그와 상태 코드로 상황을 보지만, 사용자는 화면에 남은 문장만 보니까요. 저도 예전에는 에러 처리를 기능의 끝부분이라고 생각했는데, 요즘은 오히려 제품의 말투를 보여주는 부분이라고 생각합니다.
진행률 대신 살아 있다는 신호를 주면 충분하다
가끔 “AI 답변 생성률을 30%, 60%처럼 보여줄 수 있나요?”라는 질문을 받습니다. 마음은 이해해요. 그런데 실제로는 쉽지 않습니다. 전체 답변 길이를 미리 정확히 아는 게 아니니까요. 토큰 수를 추정할 수는 있지만, 그걸 진행률처럼 보여주면 오히려 거짓말에 가까워질 때가 있습니다.
저는 그래서 진행률 대신 깜빡이는 커서나 “답변을 정리하고 있어요” 같은 짧은 상태 문구를 선호합니다. 사용자는 정확한 퍼센트보다 “아직 작동 중이구나”라는 신호를 원할 때가 많거든요.
여행 추천 챗봇에 붙여보니 프롬프트도 같이 손봐야 했다
여기서 하나 더 이야기하고 싶은 게 있어요. Vercel AI SDK로 스트리밍을 붙이면 화면은 좋아지는데, 답변 구조가 엉망이면 스트리밍 효과가 반감됩니다. 긴 문단이 한참 이어지면 사용자가 읽기 힘들어요. 그래서 저는 프롬프트도 같이 고쳤습니다.
예를 들어 처음에는 이런 식으로 요청했어요.
사용자에게 여행 일정을 추천해줘.
너무 넓죠. 그러면 답변도 제멋대로 나옵니다. 어떤 때는 장소 설명만 길고, 어떤 때는 시간표가 빠지고, 어떤 때는 맛집 추천이 너무 많아져요. 스트리밍으로 보면 더 산만하게 느껴지고요.
그래서 프롬프트를 조금 더 운영 친화적으로 바꿨습니다.
너는 40대 직장인을 위한 여행 일정 도우미야.
답변은 다음 순서로 작성해줘.
1. 전체 여행 분위기 한 줄
2. 날짜별 추천 일정
3. 이동 동선에서 조심할 점
4. 부모님이나 아이와 함께 갈 때의 대체 코스
5. 너무 광고처럼 보이는 표현은 피하고, 실제로 다녀온 사람처럼 자연스럽게 말해줘
이렇게 해두면 스트리밍으로 출력될 때도 사용자가 “아, 이제 일정이 나오겠구나”, “이제 주의사항이구나” 하고 따라가기 쉽습니다. 스트리밍은 단순히 빨리 보여주는 기술이 아니라, 읽히는 순서를 설계하는 일이기도 하더라고요.
| 프롬프트 방식 | 스트리밍 화면에서의 느낌 | 제가 느낀 문제 |
|---|---|---|
| 자유롭게 답변 요청 | 문장이 길게 이어지고 구조가 들쭉날쭉함 | 사용자가 중간에 흐름을 놓치기 쉬움 |
| 섹션을 정해 답변 요청 | 제목과 항목이 순서대로 나타남 | 읽는 리듬이 좋아지고 수정도 쉬움 |
| 대상 사용자를 지정 | 말투와 추천 기준이 일정해짐 | 서비스 성격이 더 또렷해짐 |
써보면서 느낀 Vercel AI SDK 스트리밍의 현실적인 장단점
솔직히 말하면 Vercel AI SDK는 Next.js를 쓰는 사람에게 꽤 좋은 선택지입니다. 특히 빠르게 AI 기능을 붙여보고 싶은 팀이라면 시간을 많이 아낄 수 있어요. API 라우트에서 streamText() 쓰고, 프론트에서 useChat으로 받으면 기본 흐름은 금방 나옵니다.
다만 운영까지 생각하면 “우와, 한 줄이면 끝!” 같은 느낌으로 접근하면 조금 위험합니다. 중간에 끊기는 상황, 사용자가 취소하는 상황, 모델 응답이 너무 길어지는 상황, 비용이 예상보다 커지는 상황을 같이 봐야 해요. AI 기능은 데모가 쉬운 만큼 운영이 묘하게 까다롭습니다. 이건 제가 꽤 여러 번 당해보고 나서 생긴 생각이에요.
- 좋았던 점
- Next.js와 붙이는 흐름이 자연스럽다
- 스트리밍 기본 구현이 정말 빠르다
- 챗봇 프로토타입을 만들 때 속도가 잘 나온다
- 프론트와 서버 코드가 과하게 복잡해지지 않는다
- 조심할 점
- 에러 처리를 안 해두면 중간 끊김이 사용자의 불만으로 바로 이어진다
- abort 처리를 빼먹으면 비용이 새어나갈 수 있다
- 긴 답변에서는 프롬프트 구조를 잘 잡아야 읽기 편하다
- 개발 환경에서는 괜찮아 보여도 실제 사용량이 늘면 rate limit을 만나기 쉽다
이 글이 도움이 될 사람들
Vercel AI SDK 스트리밍 구현은 공식 예제만 봐도 시작할 수 있습니다. 그건 맞아요. 그런데 실제로 사용자에게 열어놓고 써보면 문서에 없는 사소한 문제들이 더 크게 느껴집니다. 답변이 중간에 멈췄을 때 어떻게 말할지, 사용자가 취소했을 때 비용을 어떻게 막을지, 긴 답변을 어떻게 읽기 좋게 흘려보낼지 같은 것들이요.
이 글은 Next.js로 AI 챗봇을 만들고 있는 개발자, 기존 챗봇에 실시간 응답 느낌을 넣고 싶은 분, 그리고 AI 바이브 코딩으로 빠르게 뭔가 만들어봤는데 이제 조금 더 서비스답게 다듬고 싶은 분에게 잘 맞을 것 같아요. 특히 저처럼 여행 추천, 상담형 검색, 문서 요약 같은 긴 답변 서비스를 만들고 있다면 스트리밍은 거의 한 번쯤 꼭 써볼 만합니다.
개인적으로는 스트리밍을 붙인 뒤에야 챗봇이 “도구”에서 “대화 상대”처럼 느껴졌어요. 그 차이가 꽤 큽니다. 개발자로 오래 일하다 보면 기술 자체보다 사용자가 느끼는 온도에 더 눈이 가게 되는데요. Vercel AI SDK는 그 온도를 올리는 데 꽤 괜찮은 도구였습니다. 물론 에러 처리와 비용 관리는 꼭 챙기고요. 그거 안 챙기면, 나중에 커피 한 잔 마시다가도 서버 로그 생각납니다.
댓글
댓글 쓰기