유튜브 쇼츠 자동화 파이프라인을 만들기로 했다.
아이디어는 단순했다. 스크립트 생성 → TTS → 자막 → 영상 렌더 → 업로드. 딱 다섯 단계.
근데 막상 설계하다 보니까, 이게 정말 하나의 레포로 관리될 수 있는 구조인가?
스크립트는 Gemini API, TTS는 별도 서비스, 렌더링은 ffmpeg 물린 워커, 웹 대시보드는 또 따로. 각각이 독립적으로 배포되는 서비스들이다. 이걸 한 레포에 때려박으면 나중에 분명 후회한다.
이미 알고 있었다.
핵심은 타입 공유였다.
Job 타입 하나가 바뀌면 스크립트 워커, 렌더 워커, 웹 앱이 전부 같이 바뀌어야 한다. 이걸 각자 레포로 관리하면? npm publish 하고, 버전 올리고, 각 레포에서 업데이트하고...
생각만 해도 머리가 아팠다.
모노레포를 쓰면 이게 단번에 해결된다. 패키지끼리 로컬 참조로 연결되니까 타입 하나 바꾸면 빌드 시점에 전부 잡힌다. 아, 이거다 싶었다.
툴 선택지는 두 가지였다.
빌드 캐싱이 핵심이었다. 렌더 워커 코드만 바꿨는데 스크립트 워커까지 다시 빌드하면 낭비 아닌가. turborepo로 정했다.
패키지 매니저는 pnpm. workspace 기능이 npm보다 깔끔하고, 디스크 공간도 덜 잡아먹는다.
.changeset/config.json — 버전 관리 전략
.eslintrc.js — 공통 린트 규칙
.gitignore
package.json — 루트 워크스페이스
pnpm-workspace.yaml — apps/*, packages/* 구조 정의
tsconfig.base.json — 공통 TS 설정
turbo.json — 빌드 파이프라인 정의
turbo.json에서 파이프라인을 이렇게 잡았다.
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
^build는 "내 패키지가 의존하는 것들 먼저 빌드해"라는 뜻이다. 의존성 그래프를 자동으로 따라가준다.
pnpm-workspace.yaml은 단순하다.
packages:
- "apps/*"
- "packages/*"
apps/에는 실제 서비스들(web, api, workers), packages/에는 공통 모듈(db 스키마, 타입 정의, 유틸)이 들어갈 예정.
이 시점엔 코드가 하나도 없다. 뼈대만 세운 거다.
근데 이 선택이 나중에 워커 6개 돌릴 때 진짜 빛을 발했다. 타입 하나 바꾸면 전부 에러가 잡혔고, 오히려 안심이 됐으니까.
처음 설계 잘 해두면 이런 게 돌아온다.