TanStack Start와 ByteDance Seedance를 사용하여 AI 반려동물 비디오 생성기를 만든 방법
요약
TanStack Start와 ByteDance Seedance를 활용하여 AI 기반 반려동물 비디오 생성기를 구축한 기술 사례를 소개합니다. 파일 기반 라우팅, 서버 사이드 로더, Nitro 엔진을 통한 효율적인 풀스택 개발 과정을 다룹니다.
핵심 포인트
- TanStack Start의 파일 기반 라우팅과 단순한 컴포넌트 구조 활용
- Loader를 통한 서버 사이드 데이터 페칭 및 타입 안전성 확보
- Nitro 엔진을 이용한 플랫폼 독립적인 서버 엔드포인트 구축
- TanStack Query를 활용한 효율적인 서버 상태 관리
TanStack Start와 ByteDance Seedance를 사용하여 AI 반려동물 비디오 생성기를 만든 방법
몇 달 전, 저는 진정으로 마법 같은 무언가를 만들어보겠다고 결심했습니다. 또 다른 CRUD 대시보드나 생산성 추적기가 아니라, 기술적 지식이 없는 제 친구들도 실제로 사용하고 즐길 수 있는 무언가를 만들고 싶었습니다.
그 결과물이 바로 **PetVideo Generator**입니다. 고양이나 강아지 사진을 업로드하고, 그들이 무엇을 하길 원하는지 설명하면(
마법 없는 파일 기반 라우팅 (File-based routing). 라우트는 src/routes/ 아래에 평면적인 파일과 폴더 형태로 존재합니다. /pricing 페이지는 말 그대로 src/routes/pricing.tsx입니다. 라우트 그룹은 (auth)/sign-in.tsx와 같이 괄호를 사용합니다. 이는 Next.js와 동일한 컨벤션이지만, "use client" 지시어나 RSC (React Server Components) 경계에 대한 혼란이 없습니다. 모든 컴포넌트는 일반적인 React 컴포넌트입니다. 서버에서 실행된다면 loader를 작성하면 되고, 클라이언트에서 실행된다면 컴포넌트를 작성하면 됩니다. 그게 전부입니다.
로더 (Loaders)는 서버 사이드에서 동작하며, 마법이 아닙니다. 페이지를 렌더링하기 전에 데이터를 가져와야 하나요? loader 함수를 내보내기(export) 하세요:
export const Route = createFileRoute('/pricing')({
loader: async () => {
const plans = await getPublicPlans();
...
로더는 SSR (Server-Side Rendering) 중에 서버에서 실행되며 데이터베이스에 직접 접근할 수 있습니다. API 라우트를 거치는 간접적인 방식이나 getServerSideProps 같은 번거로운 절차가 필요 없습니다. 반환된 데이터는 직렬화(serialized)되어 컴포넌트 내에서 Route.useLoaderData()를 통해 완전히 타입이 지정된(fully typed) 상태로 사용할 수 있습니다.
내부에는 Nitro가 탑재되어 있습니다. TanStack Start는 Nitro와 함께 번들링됩니다. 이는 서버 엔드포인트가 src/routes/api/에 위치하며 단순히 내보내진 함수라는 것을 의미합니다. 웹훅(webhook) 핸들러가 필요한가요? src/routes/api/webhook.ts에서 POST 함수를 내보내면 됩니다. Node.js, Cloudflare Workers, 또는 Nitro가 지원하는 어디로든 배포할 수 있어 특정 플랫폼에 종속되지 않습니다.
보일러플레이트 없는 메타데이터 (Metadata). 각 라우트에는 로더 데이터를 전달받는 head export가 있습니다:
export const Route = createFileRoute('/pricing')({
loader: () => ({ title: 'Pricing — PetVideo Generator' }),
head: ({ loaderData }) => ({
...
이 패턴은 앱 내의 35개 이상의 모든 라우트에서 일관되게 유지되었습니다. 한 번 배우고 나면 어디서든 적용할 수 있습니다.
TanStack Query — 서버 상태 (Server State)를 제대로 다루는 법
이전에는 로딩/에러/데이터 상태를 관리하기 위해 useEffect + fetch + useState를 사용하여 서버 상태를 수동으로 관리하곤 했습니다. 그러다 결국 관리가 제대로 되지 않는 캐시 레이어를 추가하게 되곤 했죠. TanStack Query는 그 모든 것을 제거해 줍니다.
관리자 패널의 사용자 목록을 위한 페이지네이션 쿼리(paginated query)는 다음과 같습니다:
const usersQuery = useQuery({
queryKey: ['admin-users', page, search],
queryFn: () => apiGet(`/api/admin/users?page=${page}&search=${search}`),
...
keepPreviousData는 조용히 가장 훌륭한 기능 중 하나로 자리 잡고 있습니다. 사용자가 2페이지를 클릭하면, 2페이지가 로드될 때까지 테이블은 1페이지의 데이터를 계속 보여줍니다. 빈 행이 깜빡거리거나 데이터가 있어야 할 자리에 로딩 스피너(loading spinner)가 나타나지 않습니다. 실제로 빠르기 때문에 빠르다고 느껴집니다.
Mutation (변이)은 앱 전체에서 동일한 패턴을 따릅니다:
const grantCredits = useMutation({
mutationFn: (vars: { userId: string; amount: number }) =>
apiPost('/api/admin/users/credits', vars),
...
성공 시 무효화(Invalidate), 성공 시 토스트(toast) 알림, 에러 시 토스트 알림. 코드베이스의 모든 Mutation은 이 형태를 따르며, 덕분에 앱이 성장하더라도 작업의 예측 가능성을 유지할 수 있었습니다.
TanStack Form + Zod
폼(Form)은 대부분의 React 앱에서 기술 부채(technical debt)가 쌓이는 곳입니다. 처음에는 제어 컴포넌트(controlled inputs)로 시작했다가, 유효성 검사(validation)를 추가하고, 필드 수준의 에러(field-level errors)가 필요해지며, 결국 폼 상태(form state)가 200줄의 useState와 useEffect로 가득 차게 된다는 것을 깨닫게 됩니다.
TanStack Form은 다른 접근 방식을 취합니다. 폼의 형태와 유효성 검사 스키마(validation schema)를 한 번 정의하면, 라이브러리가 모든 상태를 관리합니다:
const form = useForm({
defaultValues: { email: '', password: '' },
validators: {
...
form.Field 상태를 읽어 라벨이 붙은 입력을 렌더링하는 얇은 TextField 래퍼 컴포넌트와 결합하여, 로그인, 회원가입, 설정, 관리자 대화 상자 등 앱의 모든 폼은 동일한 구조를 따릅니다. 폼마다 별도의 상태 관리가 필요 없으며, 클라이언트와 서버 간의 중복된 유효성 검사 로직도 없습니다.
관리자 패널을 위한 TanStack Table
관리자 패널에는 사용자, 구독, 결제, 크레딧, 게시물 등 데이터 집약적인 여러 테이블이 있습니다. 각 테이블에는 서버 측 페이지네이션(server-side pagination), 검색, 정렬 가능한 컬럼(sortable columns)이 필요합니다. TanStack Table은 헤드리스 로직(headless logic)을 처리합니다. 데이터 형태를 제공하고 컬럼을 정의하기만 하면, 렌더링에 필요한 모든 것을 반환해 줍니다.
- 가시적인 행(visible rows)을 위한
table.getRowModel().rows - 정렬 표시기(sort indicators)가 포함된 컬럼 헤더를 위한
table.getHeaderGroups() - 쿼리 파라미터(query parameters)에 연결되는 내장된 페이지네이션 상태(pagination state)
DataTable 컴포넌트(TanStack Table을 래핑함)의 loading 프롭(prop)은 현재 행들을 언마운트(unmount)하지 않고도 은은한 쉬머(shimmer) 오버레이를 보여줍니다. 다시 말해, 갑작스러운 로딩 상태 변화가 없습니다.
TanStack 라이브러리 전반에 걸친 일관성이 진정한 승리입니다. 이들은 동일한 멘탈 모델(mental model)을 공유합니다. 즉, 설정 객체(configuration object)를 정의하면 반응형 상태(reactive state)를 돌려받고, 이를 UI에 연결하는 방식입니다. Query를 배우고 나니, Form과 Table을 익히는 데는 며칠이 아니라 단 몇 시간밖에 걸리지 않았습니다.
ByteDance의 Seedance 2.5 활용하기
이제 재미있는 부분, 즉 실제로 비디오를 만드는 AI에 대해 알아보겠습니다.
Seedance란 무엇인가?
Seedance는 ByteDance의 이미지-투-비디오 (image-to-video) 모델입니다. AI 비디오 분야를 팔로우해 오셨다면 Runway, Pika 또는 Kling에 대해 들어보셨을 것입니다. Seedance는 동일한 카테고리에 속하지만, 이번 프로젝트에 적합했던 몇 가지 특징을 가지고 있습니다.
Seedance 2.0은 이미 탄탄했습니다. 깔끔한 움직임을 생성하고, 반려동물의 털과 해부학적 구조를 상당히 잘 처리하며, 시각적 아티팩트(visual artifacts)를 너무 많이 발생시키지 않았습니다. 올해 초 출시된 Seedance 2.5는 몇 가지 의미 있는 개선 사항을 가져왔습니다:
- 향상된 모션 일관성(motion coherence). 이전의 이미지-비디오 모델들은 때때로 피사체를 왜곡시키곤 했습니다. 예를 들어, 개의 다리가 이상하게 늘어나거나 배경이 깜빡거리는 식입니다. Seedance 2.5는 움직임을 훨씬 더 자연스럽게 처리합니다. 뛰거나 점프하거나 고개를 돌리는 반려동물들이 마치 실제로 그런 행동을 하는 것처럼 보이며, 오류가 난 듯한 근사치처럼 보이지 않습니다.
- 긴 지속 시간에서 높은 시간적 일관성(temporal consistency). 720p 해상도로 최대 15초 분량의 출력을 지원합니다. 10초 이상의 구간에서 프레임 간 일관성이 유지되는 부분이 이전 버전을 능가하는 지점입니다. 변형 아티팩트(morphing artifacts)가 적고 전환이 더 부드럽습니다.
- 프롬프트 준수(Prompt adherence). 이 모델은 카메라 방향 단서—예: '느린 푸시인(slow push-in)', '도리 왼쪽 이동(dolly left)', '버즈 아이 뷰(bird's eye view)'—를 이전보다 더 신뢰성 있게 존중합니다. 이는 영화적인 카메라 움직임이 출력을 단순한 애니메이션 정지 이미지가 아닌 실제 비디오처럼 느끼게 만드는 핵심 요소이기 때문에 중요합니다.
통합 방법 (How I Integrated It)
생성 파이프라인은 세 단계로 구성되어 있으며, Seedance는 세 번째 단계를 처리합니다:
1단계 — 업로드(Upload). 사용자가 이미지를 드롭하거나 붙여넣습니다. 이 이미지는 Cloudflare R2 (S3와 호환되는 오브젝트 스토리지)에 업로드됩니다. 그 어떤 작업보다 먼저, 간단한 비전 모델 검사를 통해 이미지에 실제로 반려동물이 포함되어 있는지 확인합니다. 이는 실패하거나 무의미한 결과물을 생성할 수 있는 시도에 크레딧을 낭비하는 것을 막아줍니다.
2단계 — 프롬프트 최적화(Prompt optimization). 대부분의 사람들은 형편없는 프롬프트를 작성합니다.
3단계 — Seedance를 이용한 비디오 생성 (Video generation). 원본 사진과 최적화된 프롬프트가 Seedance 2.5 엔드포인트(endpoint)로 전송됩니다. 모델은 정지 이미지와 텍스트 설명을 입력받아 보통 480p 또는 720p 해상도의 5~15초 길이 비디오 클립을 생성합니다.
생성 자체는 길이에 따라 30~90초 정도 소요됩니다. 앱은 완료 여부를 폴링(polling)하며, 생성된 비디오 URL을 사용자의 갤러리에 저장합니다. 사용자는 갤러리에서 비디오를 미리 보기, 다운로드 또는 공유할 수 있습니다.
Seedance가 뛰어난 점 (그리고 어려워하는 점)
개발 및 테스트 과정에서 수천 개의 비디오를 생성해 본 결과, 어떤 것이 잘 작동하는지에 대한 감을 잡았습니다.
반려동물의 클로즈업(close-ups)은 매우 뛰어납니다. Seedance는 털의 질감, 귀의 움직임, 얼굴 표정을 잘 처리합니다. 고양이가 천천히 눈을 깜빡이며 고개를 돌리는 클로즈업 장면은 정말 훌륭하게 보이며, 사람들에게 "이게 어떻게 AI야?"라는 말을 듣게 만드는 수준의 결과물을 보여줍니다.
멀리 있는 동작은 약합니다. 프레임 안에서 반려동물이 작게 보일 경우(예: 들판을 가로질러 달리는 강아지), 움직임이 뭉개질 수 있습니다. 모델이 피사체에 할당할 수 있는 픽셀이 적어지기 때문에 품질이 눈에 띄게 떨어집니다. 저만의 경험칙은 이렇습니다: 원본 사진에서 반려동물의 눈이 명확하게 보이지 않는다면, 결과물도 좋지 않을 것입니다.
복잡한 배경보다는 단순한 배경이 더 잘 작동합니다. 잔디 위나 평범한 벽 앞에 있는 반려동물은 복잡한 거실에 있는 반려동물보다 더 깔끔한 움직임을 생성합니다. 모델이 때때로 배경 요소를 피사체와 혼동하기도 합니다. 사용자들에게 "깔끔하고 정돈된 배경의 사진을 사용하세요"라고 권장하면 성공률이 눈에 띄게 높아집니다.
카메라 움직임 지시어(Camera motion cues)는 정말 유용합니다. 프롬프트에 "slow zoom out(천천히 줌 아웃)", "gentle pan right(부드럽게 오른쪽으로 팬)", 또는 "static camera, subject moves toward lens(고정 카메라, 피사체가 렌즈를 향해 이동)" 등을 추가하면 결과물이 달라집니다. 이는 단순한 눈속임이 아니라 근본적인 기능처럼 느껴집니다. 단순히 장면을 묘사하는 것을 넘어 샷(shot)을 연출할 수 있습니다.
왜 다른 모델이 아닌가?
몇 가지 대안을 검토했습니다. Runway Gen-3는 훌륭한 결과물을 만들어내지만, API 가격 책정이 낮은 가격대를 지향하는 소비자 대상 SaaS(Software as a Service)에는 맞지 않았습니다. Kling은 사람의 얼굴 표현에는 강점이 있지만, 동물에 대해서는 일관성이 떨어졌습니다. Pika는 빠르지만 해상도에 제한이 있었습니다.
Seedance는 이번 유스케이스(use case)에 딱 맞는 최적의 지점(sweet spot)을 찾았습니다. 반려동물 콘텐츠(약간의 불완전함이 오히려 거부감보다는 매력으로 다가오는 분야)에 충분한 품질, 합리적인 생성 속도, 그리고 사용자당 손해를 보지 않으면서도 무료 티어(free tier)를 제공할 수 있는 비용 구조를 갖추고 있었습니다.
크레딧 시스템 (The Credit System)
이 부분은 지루하지만 매우 중요한 부분이며, 아무도 블로그에 쓰지 않는 내용입니다. 사용자에게 "크레딧 (credits)"으로 비용을 청구하고, 그 크레딧이 가변 비용이 발생하는 API 호출에 의해 소비될 때는 실제 회계 시스템이 필요합니다.
제가 배운 몇 가지 사항은 다음과 같습니다:
선입선출 (FIFO, First-In-First-Out) 소비가 중요합니다. 사용자는 가입 보너스, 구독 갱신, 크레딧 팩 구매 등 다양한 경로를 통해 크레딧을 축적합니다. 각 배치(batch)는 고유한 만료일을 가집니다. 생성에 22 크레딧이 소모될 때, 시스템은 만료일이 가장 가까운 배치부터 소비해야 합니다. 이를 Drizzle ORM으로 정확하게 구현하려면 단순히 credits -= amount와 같이 차감하는 것이 아니라, 윈도우 함수(window functions)와 원자적 업데이트(atomic update) 쿼리를 작성해야 했습니다.
소비 후 환불이 아니라, 예약 후 소비하십시오. 사용자가 생성을 시작하면 크레딧을 예약(reserve)합니다. 생성이 성공하면 크레딧을 소비(consume)하고, 실패하면 예약된 크레딧을 해제(release)합니다. 만약 먼저 소비한 뒤 실패 시 환불하는 방식을 택하면, 사용자가 동시에 여러 요청을 제출하여 크레딧을 초과 사용(overdraft)할 수 있는 짧은 허점이 생깁니다. 예약 패턴(크레딧 사용을 "대기 중"으로 보여주는 낙관적 UI(optimistic UI)와 함께)을 사용하면 이를 방지할 수 있습니다.
만료는 단순한 비용 통제 메커니즘이 아니라 하나의 기능입니다. 만료되는 크레딧은 정기적인 사용을 유도하며, 이는 사용자가 유료 플랜으로 업그레이드할 확률을 높입니다. 하지만 이에 대해 투명해야 합니다. 대시보드에는 각 크레딧 배치가 정확히 언제 만료되는지 표시되어야 하며, 만료 일주일 전에 안내 이메일이 발송되어야 합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기