이 글은 Velog에서 이전한 글입니다. Velog 원문 보기
들어가며
소프트웨어 마에스트로 17기 연수생으로 참여하며, 팀원들과 서비스를 기획했다.
우리 팀이 찾은 핵심 가치는 영어를 완벽하게 말해야 한다는 부담감, 강박증을 없애주기이다.
캐치프라이즈로는 "내 영어를 진짜 외국인이 알아들을 확률은?"를 잡았다.
따라서, 핵심 가치를 기능으로 구현하기 위해서 스피킹 서비스를 만들기로 결정했다.
자연스럽게 따라오는 질문이 있다.
- 사용자의 음성은 어디에서 텍스트로 바꿀까?
- AI가 만든 다음 질문은 어디에서 음성으로 바꿀까?
처음에는 단순해 보였다.사용자가 말하면 음성 파일을 서버로 보내고 -> AI 서버가 STT로 텍스트를 만들고 -> 답변을 분석한 뒤 다음 질문을 만들고 -> 그 질문을 TTS로 음성 파일로 변환해 S3 같은 저장소에 올린다.
프론트는 그 URL을 받아 재생하면 된다.
흐름만 보면 자연스럽다. 하지만 실제 대화 경험을 생각하면 문제가 생긴다.
한 턴이 끝날 때마다 STT, 분석, 다음 질문 생성, TTS 생성, 파일 업로드, URL 반환까지 모두 기다려야 하기 때문이다.
사용자는 말을 끝냈는데 다음 질문이 늦게 나오면 대화가 끊긴 것처럼 느낀다.
이 글은 Saynow의 음성 대화 흐름을 설계하면서 STT와 TTS를 어디에 둘지 고민한 내용을 정리한 글이다. Speak 같은 스피킹 서비스의 구조도 참고했지만, 결론은 지금 단계에서 가장 단순하고 빠르게 검증할 수 있는 MVP 구조를 선택하는 쪽으로 기울었다.
첫 번째 생각: 서버에서 TTS까지 모두 처리하기
가장 먼저 떠올린 구조는 서버 중심 구조였다.
사용자 음성
-> 백엔드
-> AI 서버 STT
-> 답변 분석
-> 다음 질문 생성
-> TTS 생성
-> S3 업로드
-> 프론트에서 ttsUrl 재생
시나리오를 시작할 때도 같은 방식이다. 첫 질문의 TTS 파일을 미리 만들어 S3에 저장해 두고, 백엔드는 첫 질문 텍스트와 ttsUrl을 프론트에 내려준다. 프론트는 그 URL로 음성 파일을 가져와 재생한다.
턴이 진행될 때는 사용자가 말한 음성 파일을 백엔드로 업로드한다. 백엔드는 세션 정보, 시나리오 정보, 현재 턴 정보를 붙여 AI 서버로 보낸다. AI 서버는 음성을 텍스트로 바꾸고, 사용자의 답변을 분석하고, 다음 질문을 만든다. 이후 그 질문에 대한 TTS 파일을 생성해 S3에 올리고, 최종적으로 nextQuestion과 ttsUrl을 백엔드에 반환한다.
그림으로 보면 이렇다.

장점도 있다. 음성 품질을 서버에서 통제할 수 있고, 모든 클라이언트에서 같은 목소리를 들려줄 수 있다. 나중에 캐릭터 음성이나 특정 발음 품질이 중요해질 때도 유리하다.
하지만 MVP에는 부담이 크다. 특히 한 턴의 응답이 아래 과정에 모두 묶인다.
STT -> 답변 분석 -> 다음 질문 생성 -> TTS 생성 -> S3 업로드 -> ttsUrl 반환
질문 텍스트는 이미 만들어졌는데 TTS 생성이나 업로드 때문에 프론트 응답이 늦어질 수 있다. 스피킹 서비스에서 이 지연은 꽤 치명적이다. 사용자는 텍스트가 조금 늦는 것보다 대화 리듬이 끊기는 것을 더 크게 느낄 수 있다.
두 번째 생각: TTS를 비동기로 분리하기
서버 TTS를 유지하면서 지연을 줄이는 방법도 있다. TTS 생성을 턴 응답의 필수 조건에서 빼는 것이다.
1. STT와 분석을 먼저 끝낸다.
2. 다음 질문 텍스트를 먼저 프론트에 반환한다.
3. TTS는 뒤에서 생성한다.
4. 준비되면 ttsUrl만 따로 갱신한다.
응답은 이런 형태가 된다.
{
"nextQuestion": "What size would you like?",
"understoodScore": 85,
"ttsStatus": "PENDING",
"ttsUrl": null
}
프론트는 질문 텍스트를 먼저 보여주고, TTS가 준비되면 polling, callback, websocket 같은 방식으로 ttsUrl을 받아 음성을 재생한다.

이 방식은 서버 TTS의 장점을 유지하면서도 텍스트 응답은 빨리 줄 수 있다. 다만 구현 복잡도가 올라간다. ttsStatus를 관리해야 하고, TTS URL을 나중에 갱신하는 API나 이벤트 흐름도 필요하다.
MVP에서 이 정도 복잡도를 감수할 만한지 고민이 됐다. 아직 검증해야 할 핵심은 “사용자가 말하면 AI가 잘 이해하고 적절한 다음 질문을 주는가”이지, “서버가 자연스러운 음성 파일을 만들어 주는가”가 아니기 때문이다.
스픽(Speak)는 어떻게 접근하고 있을까?
스피킹 서비스를 기획하며 유사 서비스인 '스픽(Speak)'을 많이 분석했다.
Speak의 공개 자료를 보면, 대략적인 구조는 다음과 같다.
모바일 앱
-> WebRTC / LiveKit
-> Voice Agent Server
-> ASR / LLM / TTS provider 또는 Speech-to-Speech provider
-> 모바일 앱으로 음성 스트리밍
조금 더 풀면 이렇다.

여기서 눈여겨볼 점은 Speak이 모든 기능을 하나의 방식으로 처리하지 않는다는 것이다.
- 의미 중심의 롤플레이: ASR, LLM, TTS를 조합하는 Cascade 구조
- 발음이나 억양처럼 음성 자체의 정보가 중요한 기능:Speech-to-Speech 방식
을 선택할 수 있다.
OpenAI Realtime API 같은 기술도 이런 흐름과 맞닿아 있다.
기존에는 외부에서 STT, LLM, TTS를 각각 조립해야 했다면, Realtime API는 하나의 실시간 세션에서 음성 입력과 음성 출력을 처리할 수 있게 해 준다.
기존 Cascade:
음성 -> STT -> LLM -> TTS -> 음성
Realtime:
음성 입력 -> 실시간 모델 세션 -> 음성 출력
다만 이 구조를 MVP에 바로 적용하기에는 무겁다.
WebRTC, LiveKit, Voice Agent Server, 실시간 세션 관리까지 들어가면 제품 검증보다 인프라 구현에 더 많은 시간을 쓰게 될 수 있기 때문이다.
그래서 MVP에서는 클라이언트 TTS를 선택한다
그래서 SayNow의 현재 MVP에서는 TTS를 서버에서 처리하지 않기로 했다.
MVP의 핵심 흐름은 이렇게 잡을 수 있다.
사용자 음성 파일 업로드
-> AI 서버 STT
-> 답변 분석
-> 다음 질문 텍스트 생성
-> 백엔드 저장/응답
-> 프론트 클라이언트 TTS 재생

역할도 단순해진다.

이 구조에서는 ttsUrl이 필수값이 아니다. 서버는 질문 텍스트를 내려주고, 프론트는 그 텍스트를 클라이언트 TTS로 읽어 준다.
예를 들면 응답은 이렇게 단순해질 수 있다.
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"turnId": 1,
"transcript": "I want an iced americano.",
"understoodScore": 85,
"status": "IN_PROGRESS",
"babsaeText": "What size would you like?",
"babsaeTtsUrl": null,
"followUpCount": 1,
"maxFollowUpCount": 5
}
babsaeText는 필수다. babsaeTtsUrl은 제거하거나 nullable로 둔다. MVP에서는 프론트가 babsaeText를 클라이언트 TTS로 재생하면 된다.
클라이언트 TTS의 단점은 없을까?
당연히 있다. 클라이언트 TTS는 기기와 브라우저에 따라 품질이 다르다.
iOS, Android, Chrome, Safari에서 목소리와 발음이 달라질 수 있고, 영어 억양도 기대만큼 자연스럽지 않을 수 있다.
그래도 MVP에서는 이 단점이 치명적이지 않다고 봤다. SayNow의 초기 검증 포인트는 고품질 음성 합성이 아니라, 사용자가 말한 내용을 AI가 잘 이해하고 다음 질문으로 대화를 이어 갈 수 있는지다. 질문 텍스트를 화면에 함께 보여준다면, TTS가 조금 어색해도 사용자는 흐름을 이해할 수 있다.
반대로 서버 TTS를 처음부터 넣으면 응답 지연과 구현 복잡도가 먼저 커진다. 품질 문제를 해결하려다 제품의 핵심 흐름 검증이 늦어질 수 있다.
나중에 어떤 방향으로 고도화할까?
MVP 이후에는 실제 사용 데이터를 보고 판단하면 된다.

사용자가 “음성이 너무 어색하다”고 느끼면 서버 TTS나 외부 TTS provider를 붙이면 된다. 사용자가 “대화가 너무 느리다”고 느끼면 WebRTC와 실시간 Voice Agent 구조를 검토할 수 있다. 발음, 억양, 톤까지 평가해야 한다면 Speech-to-Speech나 음성 특성 분석이 필요해진다.
결국 처음부터 최종 구조를 만들려고 하지 않는 것이 중요하다. Speak 같은 서비스의 현재 구조는 오랜 시간 제품을 운영하며 도달한 결과에 가깝다. 우리는 먼저 가볍게 검증하고, 실제 문제가 확인되는 지점부터 고도화하는 편이 낫다.
결론
SayNow의 MVP에서는 다음 구조가 가장 현실적이다.
Frontend
- 음성 녹음
- 질문 텍스트 표시
- 클라이언트 TTS 재생
Backend
- 세션 상태 관리
- 턴 저장
- AI 서버 호출
- 응답 정규화
AI Server
- STT
- 답변 분석
- 슬롯/목표 달성 판단
- 다음 질문 텍스트 생성
한 턴의 응답을 TTS 생성과 S3 업로드 완료에 묶지 않는다. 먼저 텍스트 기반 대화 흐름을 빠르게 만들고, TTS 품질이나 실시간성이 실제 문제로 드러났을 때 서버 TTS나 Realtime 구조로 확장한다.
지금 필요한 건 완성형 음성 인프라가 아니라, 사용자가 말하고 AI가 이어서 질문하는 핵심 루프를 빠르게 검증하는 것이다.
참고 링크
'Project > Saynow' 카테고리의 다른 글
| [Saynow] LLM Workflow 개선기 - availableOptions를 버리고 RAG로 간 이유 (1) | 2026.08.05 |
|---|---|
| [Saynow] 프롬프트 개선기 - Prompt 1에서 9까지 (0) | 2026.08.05 |