우리 비디오가 일주일 동안 조용히 실패했습니다 — 오래된 Env Var가 $60와 12명의 불만 사용자에게 비용을 발생시켰습니다

발행: (2026년 3월 9일 PM 07:01 GMT+9)
12 분 소요
원문: Dev.to

Source: Dev.to

TL;DR

  • 무슨 일인가요? Fal.ai 크레딧이 사용됐지만, Remotion Lambda 함수 이름이 오래되어 비디오가 전달되지 않았습니다.
  • 왜 그런가요? 배포 스크립트의 수동 단계가 Remotion을 업그레이드한 후 REMOTION_LAMBDA_FUNCTION_NAME 환경 변수를 업데이트하지 못했습니다.
  • 어떻게 해결했나요? 대상 재시도 스크립트가 이미 저장된 자산을 다시 렌더링했으며, 배포 스크립트를 자동화하여 환경 변수가 동기화되도록 했습니다.
  • 다음은? 매시간 Inngest 크론이 rendering 상태에 머물러 있는 프로젝트를 모니터링하고, 청구 급증 전에 알림을 보냅니다.

1. The symptom

“You know that sinking feeling when you check your billing dashboard and something doesn’t add up?”

RepoClip – my AI video‑generation SaaS – was burning ≈ $20 / day on Fal.ai credits for three consecutive days (Mar 7‑9).
The first guess was “more users = more videos”, but the videos never reached any user.

2. RepoClip 비디오 파이프라인

GitHub URL → Gemini Analysis → Kling Video Clips (fal.ai) → Remotion Lambda Render → Done
  • 각 “Video Short” → 5개의 AI 클립 (Kling 3.0 Pro) → 내레이션과 함께 스티치 (Remotion on AWS Lambda).

파이프라인은 Inngest에 의해 오케스트레이션되며, 각 단계의 결과를 메모이즈해 재시도 시 이미 완료된 작업을 다시 수행하지 않도록 합니다.

관련 단계 (간략화)

단계설명
1GitHub 코드 가져오기
2Gemini으로 분석
3비디오 클립 생성 (fal.ai) ← 여기서 비용 발생
4Remotion Lambda 렌더 트리거 ← 실패
5렌더 완료 여부 폴링
6프로젝트 상태를 completed 로 업데이트

3. 청구 단서

Fal.ai 대시보드에 $20 / dayMar 7, 8, 9에 표시됨 → 대략 하루에 2–4개의 비디오 생성으로, 자연 트래픽에 대해 그럴듯해 보였다.

4. 데이터베이스 쿼리

SELECT
  created_at::date AS date,
  status,
  COUNT(*) AS count
FROM projects
WHERE created_at >= '2026-03-01'
GROUP BY date, status
ORDER BY date;

결과

날짜상태개수
Mar 1completed3
Mar 1failed5
Mar 1rendering1
Mar 2rendering2
Mar 5rendering1
Mar 6rendering2
Mar 7rendering2
Mar 8rendering4

3월 1일 이후로 completed 상태에 도달한 프로젝트는 없었습니다. 모든 비디오는 rendering 단계에 머물렀으며, 이는 Fal.ai가 클립 생성을 마친 직후 단계입니다.

5. 근본 원인: 오래된 Lambda 함수 이름

환경 변수 (우리가 생각한 대로)

REMOTION_LAMBDA_FUNCTION_NAME=remotion-render-4-0-414-mem2048mb-disk2048mb-600sec

AWS에 실제 존재하는 Lambda 함수들

aws lambda list-functions --query 'Functions[?starts_with(FunctionName, `remotion`)]'

Output:

remotion-render-4-0-429-mem2048mb-disk2048mb-600sec
remotion-render-4-0-429-mem3008mb-disk4096mb-900sec

함수 remotion-render-4-0-414… 더 이상 존재하지 않았습니다.
우리는 Remotion을 v4.0.414 → v4.0.429 로 업그레이드하고 새 Lambda를 배포한 뒤 기존 것을 삭제했지만, Vercel의 환경 변수를 업데이트하는 것을 잊어버렸습니다.

결과

  • renderMediaOnLambda()ResourceNotFoundException 을 발생시켰습니다.
  • Inngest 가 조용히 재시도했으며, 클라이언트 측 오류도, GA4 “video_generate_complete” 이벤트도, Sentry 알림도 없었습니다.
  • Fal.ai 가 여전히 클립을 생성했기 때문에 청구는 계속되었습니다.

6. 복구 – 타깃 재시도 스크립트

이미 12개의 멈춘 프로젝트 모두 자산이 영구 저장(assets JSONB 컬럼)되어 있었습니다.
우리는 비용이 많이 드는 Fal.ai 클립을 다시 생성하는 대신 저장된 자산을 그대로 재렌더링했습니다.

// 핵심 인사이트: 자산은 이미 저장돼 있으니, 단순히 재렌더링만 하면 된다
const { renderId, bucketName } = await renderMediaOnLambda({
  region: REGION,
  functionName: FUNCTION_NAME, // 이제 올바른 함수로 지정
  serveUrl: SERVE_URL,
  composition: "ProductVideo",
  inputProps, // 저장된 자산을 기반으로 구성
  codec: "h264",
  // …
});

결과: 12/12 비디오 복구 성공 (첫 시도에 9개, 일시적인 네트워크 타임아웃 후 3개).
추가 Fal.ai 비용이 발생하지 않았으며, 사용자에게 완료 이메일이 발송되었습니다.

7. 자동화 – 환경 변수를 다시 업데이트하는 것을 절대 잊지 않기

이전 수동 단계

echo "Set the following environment variables:"
echo "  REMOTION_LAMBDA_FUNCTION_NAME="

새로운 자동화 단계

# Extract function name from deploy output
FUNC_NAME=$(echo "$FUNC_OUTPUT" | grep -oE 'remotion-render-[a-zA-Z0-9-]+' | head -1)

# Verify function exists
aws lambda get-function --function-name "$FUNC_NAME" --region "$REGION"

# Auto‑update Vercel + local env
echo -n "$FUNC_NAME" | npx vercel env rm REMOTION_LAMBDA_FUNCTION_NAME production -y
echo -n "$FUNC_NAME" | npx vercel env add REMOTION_LAMBDA_FUNCTION_NAME production
sed -i '' "s|^REMOTION_LAMBDA_FUNCTION_NAME=.*|REMOTION_LAMBDA_FUNCTION_NAME=$FUNC_NAME|" .env.local

이제 배포 스크립트는 새 Lambda 이름을 추출하고, 이를 검증하며, Vercel과 로컬 .env를 자동으로 업데이트합니다.

8. 지속적인 모니터링 – 멈춘 렌더링을 위한 크론 작업

export const monitorStuckRendersFunction = inngest.createFunction(
  { id: "monitor-stuck-renders" },
  { cron: "0 * * * *" }, // every hour
  async ({ step }) => {
    const stuckProjects = await step.run("check-stuck-projects", async () => {
      const threshold = new Date(Date.now() - 30 * 60 * 1000).toISOString();
      const { data } = await supabase
        .from("projects")
        .select("*")
        .eq("status", "rendering")
        .lt("updated_at", threshold);
      return data;
    });

    if (stuckProjects?.length) {
      // Notify Slack / email / create issue
      await step.run("alert", async () => {
        // …implementation…
      });
    }
  }
);
  • 무엇을 하는가: 매시간 rendering 상태이며 30분 이상 진행 중인 프로젝트를 가져와 팀에 알림을 보냅니다.
  • 왜 필요한가: 조용한 청구 급증을 방지하고 향후 회귀에 대비한 안전망을 제공합니다.

9. 핵심 정리

✅ 잘 된 점❌ 잘 안 된 점
렌더링 전에 자산을 지속시켜 → 저비용 복구수동 환경 변수 업데이트 단계가 누락됨
Inngest 메모이제이션으로 중복 Fal.ai 청구 방지무음 재시도로 ResourceNotFoundException이 숨겨짐
청구 이상 현상이 조사 촉발GA4 “complete” 이벤트 없음 → 가시성 부족
자동 배포 스크립트가 이제 환경 변수 동기화 보장정체된 렌더링에 대한 사전 모니터링 부재

핵심 요점: 사소한 수동 단계가 $60 이상의 손실과 열악한 사용자 경험을 초래했습니다. 중간 자산을 지속하고, 환경 업데이트를 자동화하며, 사전 모니터링을 추가함으로써 비용이 많이 드는 장애를 학습 기회로 바꾸었습니다. 🚀

멈춘 프로젝트 모니터링

// Example query to find projects stuck in the “rendering” state
const { data: stuckProjects } = await supabase
  .from("projects")
  .select("id, repo_name, content_mode, updated_at")
  .eq("status", "rendering")
  .lt("updated_at", threshold);

return data ?? [];

// If any projects are stuck, send an alert email with their details
if (stuckProjects.length > 0) {
  // Send alert email with project details
}

일주일 전에도 이런 것이 있었다면, 우리는 7일이 아니라 1시간 안에 알 수 있었을 것입니다.

실시간 상태 이벤트

우리는 사용자의 브라우저가 Supabase Realtime를 통해 상태 변경을 수신할 때 발생하는 두 개의 이벤트를 추가했습니다:

// ProjectStatusListener.tsx
const channel = supabase
  .channel(`project-${projectId}`)
  .on(
    "postgres_changes",
    { /* … */ },
    (payload) => {
      if (payload.new?.status === "completed") {
        gaEvent("video_generate_complete", { project_id: projectId });
      } else if (payload.new?.status === "failed") {
        gaEvent("video_generate_fail", { project_id: projectId });
      }
    }
  )
  .subscribe();

BigQuery의 퍼널 쿼리

-- Start‑to‑complete ratio per day
SELECT
  event_date,
  COUNT(DISTINCT CASE WHEN event_name = 'video_generate_start'
    THEN user_pseudo_id END) AS start_users,
  COUNT(DISTINCT CASE WHEN event_name = 'video_generate_complete'
    THEN user_pseudo_id END) AS complete_users
FROM events_*
GROUP BY event_date;

이제 complete/start 비율이 급격히 떨어지는 것이 명확한 신호로 보입니다.

비용 발견

조사 중에 모든 무료 등급 사용자가 유료 고객과 동일한 “Kling 3.0 Pro” 클립을 받고 있다는 사실을 발견했습니다.

  • 비디오당 비용 (Pro): ≈ $5.60
  • 전환율: ≈ 3 %
  • 결과: 지속 불가능한 고객 획득 비용

해결 방안

계획클립 유형클립 수대략 길이비디오당 비용
FreeKling 3.0 Standard3~15 s$2.52
PaidKling 3.0 Pro5~25 s$5.60
  • “Kling 3.0 Pro 품질”을 실질적인 업그레이드 인센티브로 전환.
  • 무료 등급 비용을 55 % 절감.

교훈

  1. Env vars are a silent single point of failure – 자동으로 수명 주기를 관리하세요.
  2. Background‑job failures are invisible by default – “끝났어야 하는데 끝나지 않은” 작업에 대한 명시적 모니터링을 추가하세요.
  3. Track completion, not just initiationvideo_generate_complete 데이터가 없는 것은 중요한 신호입니다.
  4. Persist intermediate results – 추가 Fal.ai 비용 없이 복구할 수 있게 했습니다.
  5. Billing anomalies are monitoring signals – 예상치 못한 지출 패턴에 대한 알림을 설정하세요.

Try RepoClip

RepoClip은 GitHub 저장소에서 AI 기반 홍보 영상을 생성합니다.
공개 저장소 URL을 붙여넣으면 몇 분 안에 영상을 얻을 수 있습니다 — 무료, 신용카드 필요 없음.

0 조회
Back to Blog

관련 글

더 보기 »