tts-generator 회고 1편 | 왜 이 프로젝트를 만들었고, 왜 이 스택을 골랐을까
출퇴근길에 읽지 못한 글을 듣고 싶다는 개인적인 필요에서 시작해, Markdown와 화자 구분 중심의 TTS 워크플로를 설계하게 된 과정을 정리한 글이다.
이 프로젝트를 시작한 계기
평소 출퇴근할 때 팟캐스트를 자주 듣는다. 그런데 공부할 글이나 정리해둔 문서들도 같은 방식으로 들을 수 있으면 좋겠다는 생각이 자주 들었다. 글을 읽을 시간은 부족한데, 이동 시간은 꾸준히 생긴다. 그렇다면 이 시간을 학습 시간으로 바꿀 수 있지 않을까 싶었다.
처음부터 거창한 서비스를 만들고 싶었던 것은 아니다. 내가 정리한 글이나 Markdown 문서를 넣으면, 출퇴근길에 바로 들을 수 있는 오디오 파일로 바꿔주는 작은 도구면 충분했다. 다만 한 가지는 분명했다. 그냥 문장을 기계적으로 읽는 TTS로는 금방 지루해질 것 같았다. 내가 원한 것은 "읽어주는 도구"보다 "작은 팟캐스트처럼 들리는 도구"에 더 가까웠다.
만들기 전에 가장 먼저 고민한 것들
프로젝트를 시작하기 전에 기능 목록보다 먼저 정리한 것은 "무엇이 있어야 내가 실제로 계속 쓰게 될까"였다. 이 기준으로 몇 가지 요구사항이 빠르게 정리됐다.
1. 팟캐스트처럼 화자가 구분되어야 한다
팟캐스트를 계속 듣다 보면, 내용 자체보다도 "누가 말하고 있는지"가 생각보다 중요하다는 걸 느끼게 된다. 설명하는 사람과 질문하는 사람이 다르면 귀가 훨씬 덜 피로하다.
그래서 처음 계획을 세울 때부터 화자를 2명 이상 둘 수 있는 구조를 염두에 뒀다. 단일 보이스 하나로 문서를 전부 읽는 방식은 구현은 간단하지만, 내가 원했던 사용 경험과는 거리가 있었다. 특히 공부 메모나 정리 글은 설명, 예시, 질문, 보충 설명이 섞이는 경우가 많아서 화자 구분이 있으면 훨씬 듣기 편하다.
이 요구사항 때문에 기본 TTS 엔진만 고르는 것으로는 충분하지 않았다. 한국어 문장을 안정적으로 읽는 기본 엔진이 필요했고, 여기에 원하는 목소리 느낌을 덧입힐 수 있어야 했다. 그래서 기본 합성은 MeloTTS를 사용하고, 샘플 보이스 기반 톤 변환은 OpenVoice를 붙이는 방향으로 생각하게 됐다.
2. 공부 내용은 보통 여러 Markdown 파일로 흩어져 있다
실제로 공부한 내용을 돌아보면 하나의 긴 문서보다 여러 개의 Markdown 파일로 쪼개져 있는 경우가 많다. 하루치 메모, 요약 정리, 블로그 초안, 참고 링크 정리처럼 파일이 분산되어 있다.
그래서 만들기 전부터 "결국은 여러 파일을 하나의 듣기 흐름으로 묶는 작업이 필요하겠다"는 생각을 했다. 이번 MVP에서는 단일 Markdown 업로드부터 시작했지만, 내부적으로는 텍스트를 바로 TTS로 보내지 않고 문서 단위로 정규화하는 구조를 먼저 잡았다. 나중에 여러 파일을 하나의 스크립트처럼 묶더라도, 마지막에는 하나의 문서 모델로 수렴하도록 하고 싶었기 때문이다.
즉, 처음부터 멀티 파일 병합 기능을 전부 다 만들지는 않았지만, 이후 확장을 고려해 "문서를 어떻게 표현할 것인가"를 먼저 고민한 셈이다.
3. 긴 작업은 화면이 멈추지 않아야 한다
문서가 길어지면 TTS 생성 시간도 길어진다. 특히 segment가 많아지고, 샘플 보이스 변환까지 붙으면 한 번의 요청으로 바로 응답을 끝내는 방식은 사용자 경험이 좋지 않다.
그래서 일찍부터 비동기 처리 흐름이 필요하다고 봤다. 화면에서 업로드 버튼을 눌렀을 때 브라우저가 멈춘 것처럼 보이면 다시 쓰고 싶지 않을 가능성이 높다. 최소한 작업을 등록하고, 상태를 확인하고, 완료되면 결과를 다운로드하는 흐름은 있어야 했다.
이 판단 때문에 구조도 자연스럽게 "요청 하나 = 결과 하나"가 아니라, "요청은 Job을 만들고 실제 생성은 백그라운드에서 처리"하는 방향으로 정리됐다.
4. Markdown 안에서 보이스를 선택할 수 있어야 한다
단순 업로드만 지원하면 금방 한계를 느낄 것 같았다. 문서 전체에 같은 목소리를 쓰는 것보다, 문서 안에서 화자별로 voice, speed 같은 값을 조절할 수 있어야 내가 원하는 팟캐스트 느낌에 더 가까워진다.
그래서 Markdown 상단에 tts 블록을 두고, 본문에서는 화자: 내용 형식으로 읽게 하는 규칙을 생각했다. 이 방식의 장점은 두 가지였다.
- 별도 편집기를 만들지 않아도 문서 자체에 TTS 설정을 함께 보관할 수 있다.
- 나중에 같은 문서를 다시 음성으로 만들 때도 설정이 문서 안에 남아 있어 재현성이 좋다.
결국 이 프로젝트는 "문서를 음성으로 바꾸는 도구"이면서 동시에 "문서 안에 음성 규칙을 같이 적는 도구"가 되기를 바랐다.
왜 이 기술 조합을 선택했는가
이 프로젝트의 기술 선택 기준은 단순했다. 지금 당장 내가 쓰는 문제를 가장 짧은 경로로 해결할 수 있는가였다. 멋있는 구조보다 실제로 빨리 만들고, 직접 써보고, 다시 고칠 수 있는지가 더 중요했다.
FastAPI를 선택한 이유
백엔드에서 필요한 일은 비교적 분명했다. 파일 업로드, 작업 생성, 상태 조회, 결과 다운로드가 핵심이었다. 즉, 화면 렌더링보다 API 흐름이 중심이고, 오래 걸리는 작업을 등록형으로 다루는 구조가 필요했다.
이런 상황에서는 FastAPI가 잘 맞았다. 라우팅이 단순하고, 요청/응답 스키마를 빠르게 만들 수 있고, 비동기 흐름을 붙이기도 어렵지 않다. 이번 프로젝트처럼 "텍스트 입력 또는 Markdown 업로드 -> Job 생성 -> 상태 조회 -> 다운로드" 구조를 빠르게 조립하기에는 과하지도, 부족하지도 않았다.
반대로 처음부터 복잡한 백그라운드 워커나 메시지 큐를 얹는 것은 이번 단계에서 우선순위가 아니었다. 지금은 개인용 MVP이기 때문에, 외부 인프라보다 API와 생성 파이프라인을 먼저 다지는 편이 맞다고 판단했다.
Next.js와 React를 선택한 이유
프론트엔드는 소개 페이지보다 작업형 화면이 필요했다. 텍스트를 직접 넣을 수도 있어야 하고, Markdown 파일도 업로드할 수 있어야 하며, 생성 상태도 바로 확인해야 했다.
이런 화면은 상호작용이 많아서 React 기반 구성이 편하다. 여기에 Next.js를 선택한 이유는 거창한 SSR보다는, 익숙한 React 기반 개발 흐름 안에서 페이지 구조와 개발 환경을 빠르게 갖추기 쉬웠기 때문이다.
결과적으로 프론트는 "문서를 넣고, 설정을 고르고, 상태를 확인하고, 결과를 받는 작업 공간"으로 정리할 수 있었다. 이 프로젝트에서 프론트엔드의 역할은 예쁜 랜딩 페이지보다 생성 워크스페이스에 더 가깝다.
MeloTTS를 선택한 이유
TTS 프로젝트를 시작할 때 가장 먼저 필요한 것은 "실제로 돌아가는 기본 음성"이다. 이번 프로젝트에서는 한국어 기준으로 먼저 완성하는 것이 중요했고, 그래서 기본 엔진으로 MeloTTS를 선택했다.
처음부터 다양한 엔진을 추상화해서 붙이기보다, 한 엔진으로 전체 파이프라인을 먼저 끝까지 통과시키는 편이 낫다고 봤다. 문서 파싱, segment 분리, 오디오 병합, 다운로드 흐름이 먼저 안정돼야 그 위에 엔진 교체나 확장을 논할 수 있기 때문이다.
즉, MeloTTS는 최고의 선택이라기보다 이번 단계에서 가장 현실적인 시작점이었다.
OpenVoice를 선택한 이유
문제는 기본 TTS 보이스만으로는 내가 원한 "팟캐스트 느낌"이 충분히 나오지 않는다는 점이었다. 화자가 나뉘어 있어도 목소리의 개성이 약하면 듣는 경험이 단조롭게 느껴질 수 있다.
그래서 샘플 보이스를 기반으로 원하는 톤에 더 가깝게 만들 수 있는 OpenVoice를 붙이기로 했다. 이 선택의 핵심은 완전한 음성 복제라기보다, 기본 음성 위에 샘플의 느낌을 덧입히는 데 있었다.
물론 이 선택은 구현 난이도를 높인다. 설치 환경, 체크포인트, 임베딩 캐시 같은 운영 이슈가 바로 따라온다. 그럼에도 불구하고 OpenVoice를 붙인 이유는, 이 프로젝트의 핵심 가치가 "듣기 좋은 결과"에 있었기 때문이다. 내 기준에서는 이 부분이 충분히 감수할 만한 복잡도였다.
ffmpeg를 선택한 이유
문서를 segment 단위로 쪼개어 음성을 만들면, 마지막에는 결국 하나의 파일로 다시 합쳐야 한다. 이때 포맷 정규화와 MP3 변환 같은 일은 ffmpeg가 가장 실용적이었다.
직접 오디오 포맷 변환 로직을 세세하게 구현하기보다, segment는 WAV로 안정적으로 만들고 필요할 때만 MP3로 변환하는 쪽이 처리 흐름이 단순했다. 즉, 내부 파이프라인은 WAV 기준으로 유지하고, 최종 산출물에서만 포맷 요구를 반영하는 방향이다.
DB 대신 파일 저장으로 시작한 이유
이번 프로젝트는 개인용 MVP라서 데이터 모델보다 작업 흐름이 더 중요했다. 그래서 DB를 먼저 붙이지 않고, storage/ 디렉터리 아래에 Job 메타데이터와 결과 파일을 저장하는 방식으로 시작했다.
이 방식의 장점은 구조가 단순하다는 점이다. 지금은 작업 하나가 어떻게 생성되고, 어떤 상태를 거쳐, 어떤 파일이 만들어지는지를 눈으로 추적하기 쉬운 것이 더 중요했다. 나중에 사용량이 늘어나면 DB나 외부 스토리지를 고려할 수 있겠지만, 이번 단계에서는 오히려 파일 기반 저장이 개발 속도를 높여줬다.
이번 MVP에서 일부러 하지 않은 것
프로젝트를 하다 보면 만들 수 있는 것과 지금 만들어야 하는 것을 구분하는 일이 중요하다. 이번에는 일부러 하지 않은 것도 분명히 있었다.
- WebSocket 기반 실시간 스트리밍
- 외부 메시지 큐와 별도 worker 프로세스
- DB 기반 작업 관리
- 완전한 멀티 파일 병합 UI
- 팟캐스트 편집기 수준의 정교한 타임라인 기능
이런 기능들이 불필요해서가 아니라, 지금 단계에서 가장 중요한 것은 "정리한 글을 넣으면 출퇴근길에 들을 수 있는 결과물이 나온다"는 경험 자체였기 때문이다. 개인용 프로젝트에서는 이 우선순위가 꽤 중요하다고 생각한다.
정리
tts-generator는 거대한 서비스 아이디어에서 시작한 프로젝트가 아니다. 출퇴근길에 읽지 못한 글을 듣고 싶다는 아주 개인적인 문제에서 출발했다. 하지만 그 문제를 조금 더 만족스럽게 풀고 싶다 보니, 자연스럽게 화자 구분, 샘플 보이스, 비동기 작업, Markdown 기반 설정 같은 요구가 붙었다.
결국 이번 프로젝트의 기술 선택은 모두 같은 방향을 보고 있었다. 빠르게 만들 수 있어야 하고, 실제로 내가 계속 써볼 수 있어야 하고, 문서가 그냥 읽히는 수준이 아니라 듣기 좋은 흐름으로 바뀌어야 한다는 점이다.
다음 글에서는 이 아이디어를 실제 코드로 어떻게 풀었는지, 긴 문서를 어떤 단위로 쪼개고, 화자별 보이스를 어떻게 연결하고, 구현하면서 무엇이 가장 어려웠는지를 정리해보려고 한다.