최신 트렌드

AI 영상 콘텐츠 완전 무인 자동화 - GitHub Actions + cron으로 매주 월요일 X에 자동 게시

백엔드 개발자 김승원 2026. 4. 30. 10:43

들어가며

지난 세 글에서 영상 AI 시장 정리(1편) → Grok Imagine API 통합(2편) → FFmpeg + Whisper 후처리 자동화(3편)를 다뤘습니다. 이제 마지막 퍼즐 한 조각이 남았습니다 — 이 모든 걸 사람 손 없이 굴리는 방법입니다.

이 글은 "매주 월요일 오전 10시(KST)에 자동으로 영상이 생성되고, 자막이 붙고, BGM이 합성되고, X(트위터)에 게시되는" 완전 무인 콘텐츠 파이프라인을 GitHub Actions로 구축하는 방법을 정리합니다. 별도 서버 없이, 추가 비용 없이, GitHub 무료 플랜만으로도 충분합니다.

핵심은 세 가지입니다.

  • GitHub Actions의 schedule 트리거: cron 표기법으로 시간 예약
  • Repository Secrets: API 키들을 안전하게 주입
  • 실패 알림 + 재시도: 무인 운영의 핵심 - 깨졌을 때 즉시 알아야 함

1. 전체 워크플로우 그림

완성된 파이프라인의 흐름은 다음과 같습니다.

[월요일 10:00 KST]
      ↓
[GitHub Actions 트리거 발동]
      ↓
[1] 토픽/스크립트 생성 (GPT-5.5)
      ↓
[2] Grok Imagine으로 클립 N개 생성
      ↓
[3] FFmpeg + Whisper 후처리 (자막·BGM·워터마크)
      ↓
[4] X API로 영상 업로드 + 트윗
      ↓
[5] 결과를 Slack/Discord에 보고
      ↓
[실패 시 GitHub Issue 자동 생성]

전체 실행 시간은 약 5~8분, 사용 GitHub Actions 분량은 월 30분 정도입니다(주 1회 × 4주). 무료 플랜의 2,000분 한도에 비하면 미미합니다.

2. 디렉터리 구조

먼저 리포지토리 구조를 잡습니다.

video-automation/
├── .github/
│   └── workflows/
│       └── weekly-video.yml      # GitHub Actions 워크플로우
├── src/
│   ├── generate-script.js        # GPT로 스크립트 생성
│   ├── generate-video.js         # Grok Imagine 호출
│   ├── post-process.js           # FFmpeg + Whisper (3편 코드)
│   └── publish-x.js              # X API 게시
├── assets/
│   ├── logo.png
│   ├── bgm/
│   │   ├── upbeat.mp3
│   │   └── chill.mp3
│   └── fonts/
│       └── Pretendard-Bold.ttf
├── prompts/
│   └── topic-pool.json           # 주제 풀 (요일별 로테이션)
├── package.json
└── pipeline.js                   # 모든 단계를 묶는 엔트리포인트

3. GitHub Actions 워크플로우 정의

핵심 파일은 .github/workflows/weekly-video.yml입니다.

name: Weekly AI Video Publish

on:
  schedule:
    # 매주 월요일 01:00 UTC = 한국시간 월요일 10:00
    - cron: '0 1 * * 1'
  workflow_dispatch:  # 수동 실행 버튼 활성화

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install FFmpeg + 폰트
        run: |
          sudo apt-get update
          sudo apt-get install -y ffmpeg fontconfig fonts-noto-cjk
          sudo cp assets/fonts/Pretendard-Bold.ttf /usr/share/fonts/truetype/
          sudo fc-cache -fv

      - name: Install dependencies
        run: npm ci

      - name: Run video pipeline
        env:
          XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          X_API_KEY: ${{ secrets.X_API_KEY }}
          X_API_SECRET: ${{ secrets.X_API_SECRET }}
          X_ACCESS_TOKEN: ${{ secrets.X_ACCESS_TOKEN }}
          X_ACCESS_SECRET: ${{ secrets.X_ACCESS_SECRET }}
        run: node pipeline.js

      - name: Notify Slack on success
        if: success()
        run: |
          curl -X POST -H 'Content-Type: application/json' \
            -d "{\"text\":\":white_check_mark: 영상 게시 성공: ${{ github.run_id }}\"}" \
            ${{ secrets.SLACK_WEBHOOK }}

      - name: Notify Slack on failure
        if: failure()
        run: |
          curl -X POST -H 'Content-Type: application/json' \
            -d "{\"text\":\":x: 영상 게시 실패: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
            ${{ secrets.SLACK_WEBHOOK }}

      - name: Upload artifacts on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: failed-run-${{ github.run_id }}
          path: |
            work/
            logs/
          retention-days: 7

핵심 포인트를 짚어봅니다.

  • cron 표기법: '0 1 * * 1' — 분(0), 시(1), 일(*), 월(*), 요일(1=월요일). UTC 기준이라 KST로는 +9시간입니다.
  • workflow_dispatch: GitHub UI에서 "Run workflow" 버튼이 생깁니다. 디버깅·즉시 실행에 필수.
  • timeout-minutes: 30: 무한 루프로 분량을 다 태우는 사고를 방지합니다.
  • fc-cache: 한글 폰트 적용에 필수. 빠뜨리면 자막이 □□□로 나옵니다.
  • artifacts on failure: 실패 시 작업 디렉터리를 7일간 보관합니다. 로컬에서 재현 불가능한 버그 디버깅에 필수.

4. cron 표기법 - 자주 헷갈리는 부분

cron은 5칸으로 구성됩니다: 분 시 일 월 요일. GitHub Actions는 UTC만 지원합니다. KST 기준으로 환산이 필수입니다.

원하는 시간 (KST) cron 표기 (UTC) 의미
매일 09:00 0 0 * * * 매일 자정 UTC = KST 09:00
매주 월요일 10:00 0 1 * * 1 월 01:00 UTC = 월 10:00 KST
매월 1일 00:00 0 15 28-31 * * + 조건분기 월말 처리는 까다로움
평일 18:00 0 9 * * 1-5 UTC 09:00 = KST 18:00
30분마다 */30 * * * * 0분, 30분

주의: GitHub Actions의 schedule은 최대 ±15분 지연이 발생할 수 있습니다(트래픽 많을 때). 정확한 시간 보장이 필요하면 외부 스케줄러(예: AWS EventBridge → workflow_dispatch trigger)를 써야 합니다.

로컬에서 cron 검증

crontab.guru를 쓰면 표현식의 의미와 다음 실행 시간을 확인할 수 있습니다. 또는 Node에서 node-cron으로 시뮬레이션할 수 있습니다.

npm install --save-dev cron-parser

// validate-cron.js
import parser from 'cron-parser';

const expr = '0 1 * * 1';
const interval = parser.parseExpression(expr, { tz: 'UTC' });
for (let i = 0; i < 5; i++) {
  const next = interval.next().toDate();
  console.log('UTC:', next.toISOString());
  console.log('KST:', new Intl.DateTimeFormat('ko-KR', {
    timeZone: 'Asia/Seoul', dateStyle: 'medium', timeStyle: 'short'
  }).format(next));
}

5. Repository Secrets 관리

API 키는 절대 코드/yml에 하드코딩하지 마세요. GitHub Settings → Secrets and variables → Actions에서 등록합니다.

등록할 시크릿 목록

이름 용도 발급처
XAI_API_KEY Grok Imagine 영상 생성 console.x.ai
OPENAI_API_KEY GPT 스크립트 + Whisper 자막 platform.openai.com
X_API_KEY X(트위터) Consumer Key developer.twitter.com
X_API_SECRET X Consumer Secret 위와 동일
X_ACCESS_TOKEN X 사용자 액세스 토큰 위와 동일
X_ACCESS_SECRET X 사용자 액세스 시크릿 위와 동일
SLACK_WEBHOOK 실패/성공 알림 api.slack.com/messaging/webhooks

로컬 개발용 .env

GitHub Actions에서는 secrets로 주입되지만, 로컬에서는 .env가 필요합니다. .gitignore에 반드시 추가하고, .env.example은 빈 값으로 커밋합니다.

# .env.example (커밋용)
XAI_API_KEY=
OPENAI_API_KEY=
X_API_KEY=
X_API_SECRET=
X_ACCESS_TOKEN=
X_ACCESS_SECRET=
SLACK_WEBHOOK=

# .env (로컬, .gitignore에 등록)
XAI_API_KEY=xai-실제키
...

시크릿 누출 방지 - 추가 안전장치

실수로 secret이 로그에 찍힐 수 있습니다. ::add-mask:: 명령어로 임시 변수도 마스킹할 수 있습니다.

- name: Generate session token
  run: |
    TOKEN=$(curl -s ...)
    echo "::add-mask::$TOKEN"
    echo "SESSION_TOKEN=$TOKEN" >> $GITHUB_ENV

6. 메인 파이프라인 코드

pipeline.js는 모든 단계를 묶는 엔트리포인트입니다.

// pipeline.js
import { generateScript } from './src/generate-script.js';
import { generateClips } from './src/generate-video.js';
import { ShortFormBuilder } from './src/post-process.js';
import { publishToX } from './src/publish-x.js';
import { readFile, writeFile } from 'node:fs/promises';
import { setTimeout as sleep } from 'node:timers/promises';

async function main() {
  const startedAt = Date.now();
  console.log('[1/4] 스크립트 생성...');
  const topics = JSON.parse(await readFile('prompts/topic-pool.json', 'utf-8'));
  const today = new Date().getDay(); // 0=일,1=월
  const topic = topics[today % topics.length];
  const script = await generateScript(topic);
  await writeFile('work/script.json', JSON.stringify(script, null, 2));

  console.log('[2/4] 영상 클립 생성...');
  const clips = await generateClips(script.shots);

  console.log('[3/4] 후처리...');
  const builder = new ShortFormBuilder('./work');
  const finalVideo = await builder.build({
    clips,
    bgm: 'assets/bgm/upbeat.mp3',
    logo: 'assets/logo.png',
    outputPath: 'work/final.mp4',
    transition: 'fade'
  });

  console.log('[4/4] X 게시...');
  const tweetId = await publishToX({
    videoPath: finalVideo,
    text: script.tweet  // 280자 트윗 본문
  });

  const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
  console.log(`완료! ${elapsed}초 소요. 트윗 ID: ${tweetId}`);
}

main().catch(err => {
  console.error('파이프라인 실패:', err);
  process.exit(1);  // GitHub Actions가 실패로 인식하도록
});

스크립트 생성 (GPT-5.5)

// src/generate-script.js
import OpenAI from 'openai';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export async function generateScript(topic) {
  const response = await openai.chat.completions.create({
    model: 'gpt-5.5',
    response_format: { type: 'json_object' },
    messages: [
      {
                role: 'system',
                content: '당신은 30초 숏폼 영상 작가입니다. JSON으로 응답하세요.'
      },
      {
        role: 'user',
        content: `주제: ${topic}

3개 컷(각 8~10초)으로 구성된 스토리보드를 만들어주세요.
응답 형식:
{
  "shots": [
    { "prompt": "Grok Imagine용 영어 프롬프트", "voiceover": "한국어 보이스오버" },
    ...
  ],
  "tweet": "트윗 본문 (280자 이내, 해시태그 2~3개 포함)"
}`
      }
    ]
  });

  return JSON.parse(response.choices[0].message.content);
}

7. X(트위터) API 게시

영상이 완성됐으면 X에 올립니다. X API v2는 "chunked upload"를 요구합니다 — 영상을 여러 조각으로 나눠 업로드해야 합니다.

// src/publish-x.js
import { TwitterApi } from 'twitter-api-v2';
import { readFile, stat } from 'node:fs/promises';

export async function publishToX({ videoPath, text }) {
  const client = new TwitterApi({
    appKey: process.env.X_API_KEY,
    appSecret: process.env.X_API_SECRET,
    accessToken: process.env.X_ACCESS_TOKEN,
    accessSecret: process.env.X_ACCESS_SECRET,
  });

  // 1) 영상 chunked 업로드
  const stats = await stat(videoPath);
  const buffer = await readFile(videoPath);
  console.log(`업로드 중... ${(stats.size / 1024 / 1024).toFixed(1)}MB`);

  const mediaId = await client.v1.uploadMedia(buffer, {
    mimeType: 'video/mp4',
    target: 'tweet',
    longVideo: stats.size > 5 * 1024 * 1024  // 5MB 초과 시 chunked
  });

  // 2) 트윗 게시
  const { data } = await client.v2.tweet({
    text,
    media: { media_ids: [mediaId] }
  });

  return data.id;
}

주의사항:

  • X API v2 영상 업로드는 v1.1 미디어 엔드포인트와 v2 트윗 엔드포인트의 혼합입니다(2026-04 기준). twitter-api-v2 라이브러리가 자동으로 처리합니다.
  • X 무료 플랜은 월 1,500 트윗 게시 한도가 있습니다. 자동화에는 충분하지만, 댓글 자동 응답까지 한다면 Basic($100/월) 플랜이 필요합니다.
  • 영상 길이 제한: 2분 20초. 30초 숏폼은 안전한 범위입니다.

8. 실패 알림과 자동 재시도

무인 운영의 핵심은 "깨졌을 때 즉시 알아채는 것"입니다. 단순 Slack 메시지를 넘어, 실패 패턴별로 다른 대응이 필요합니다.

(1) 자동 재시도 로직

API 호출 실패는 일시적인 경우가 많습니다. exponential backoff로 재시도합니다.

// src/utils/retry.js
export async function withRetry(fn, { maxAttempts = 3, baseMs = 1000 } = {}) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxAttempts) throw err;
      // 5xx 또는 rate limit만 재시도, 4xx는 즉시 throw
      const status = err.status || err.response?.status;
      if (status && status < 500 && status !== 429) throw err;

      const delay = baseMs * Math.pow(2, attempt - 1);
      console.log(`재시도 ${attempt}/${maxAttempts} (${delay}ms 대기)`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

(2) GitHub Actions의 retry-on-failure

워크플로우 자체에서 step 단위로 재시도할 수도 있습니다.

- name: Run video pipeline (with retry)
  uses: nick-invision/retry@v3
  with:
    timeout_minutes: 25
    max_attempts: 2
    command: node pipeline.js

(3) 실패 패턴별 대응

실패 원인 탐지 방법 대응
API 키 만료 401 응답 Slack에 "키 갱신 필요" 알림 + 사람 호출
Rate limit 429 응답 지수 백오프, 최대 3회
비용 한도 초과 402/403 중단 + Slack 긴급 알림
FFmpeg 크래시 exit code != 0 artifacts 보존, GitHub Issue 자동 생성
X API 일시 장애 503 1시간 후 재실행 (workflow_dispatch)

(4) GitHub Issue 자동 생성

- name: Create issue on failure
  if: failure()
  uses: dacbd/create-issue-action@v2
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
    title: '영상 자동화 실패: ${{ github.run_id }}'
    body: |
      Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
      Time: ${{ github.event.schedule || 'manual' }}
      
      Artifacts (7일 보존)에서 work/, logs/ 디렉터리를 확인하세요.
    labels: bug,automation

9. 비용 모니터링

무인 운영은 "잠자는 동안 콘텐츠가 만들어지는" 장점이 있지만, "잠자는 동안 비용이 폭주하는" 위험도 있습니다. 매 실행마다 비용을 추적해야 합니다.

실행당 예상 비용 (2026-04 기준)

단계 API 예상 비용
스크립트 생성 GPT-5.5 (~2K 토큰) $0.02
영상 생성 (3 클립 × 10초) Grok Imagine ($4.20/분) $2.10
자막 생성 (~30초) gpt-4o-mini-transcribe $0.0015
X 게시 X API v2 무료 $0
GitHub Actions (10분) 무료 플랜 한도 내 $0
총합   약 $2.12

월 4회 실행 기준 $8.5, 연 $102. 이 정도면 부담 없이 굴릴 수 있는 수준입니다. 단, Grok Imagine은 가격 변동이 잦으니 주기적으로 확인하세요.

비용 한도 가드

OpenAI는 대시보드에서 "하드 한도"를 설정할 수 있습니다. xAI도 동일한 기능을 제공합니다. 자동화 계정은 전용 결제 키 + 월 $20 한도로 분리 운영을 강력히 권장합니다.

10. 운영 모니터링 - GitHub Actions Insights

몇 주 운영하면 패턴이 보입니다.

  • 성공률 추이: Actions → Insights → Workflow runs에서 그래프로 확인
  • 실행 시간 분포: 평균 5~8분에서 갑자기 30분으로 튀면 어딘가 멈춰있는 것
  • 실패 사유 분류: 매주 깨지는 원인이 다르면 환경 문제, 같은 원인이 반복되면 코드 버그

3주 이상 안정적으로 굴러가면 "동일 워크플로우의 변형"을 만드세요. 화요일은 다른 톤, 목요일은 다른 BGM 같은 식으로 다양화하면 콘텐츠가 단조롭지 않습니다.

11. 흔한 함정과 해결법

(1) "schedule 트리거가 안 발동돼요"

GitHub Actions의 schedule은 기본 브랜치(main/master)에 있어야만 발동됩니다. 또한 60일간 리포에 푸시가 없으면 자동 비활성화됩니다(public repo는 예외). 워크플로우 안에 매주 작은 더미 커밋을 만드는 step을 넣거나, 정기적으로 리포에 푸시하는 게 안전합니다.

(2) "timeout-minutes 안에 끝났는데 'cancelled'로 표시돼요"

Job timeout 외에도 GitHub Actions는 전체 워크플로우 6시간 한도가 있습니다. 또 같은 cron이 짧은 간격으로 두 번 트리거되면 새 실행이 이전 실행을 취소합니다. concurrency 설정으로 명시적 제어가 가능합니다.

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: false  # 새 실행이 이전을 취소하지 못하게

(3) "로컬에선 되는데 Actions에선 깨져요"

대부분 환경 차이입니다.

  • 한글 폰트 → fc-cache
  • FFmpeg 버전 → ubuntu-latest는 4.4.x, 로컬은 6.0+ 가능. setup-ffmpeg 액션으로 버전 고정
  • 타임존 → 컨테이너는 UTC. process.env.TZ = 'Asia/Seoul' 명시
  • 임시 디렉터리 권한 → /tmp가 아닌 ./work를 사용

(4) "X 게시까진 됐는데 영상이 안 보여요"

X는 영상 업로드 후 비동기 transcode를 합니다. 업로드 직후 트윗하면 "이 영상은 처리 중입니다" 상태로 보일 수 있습니다. STATUS 명령으로 transcode 완료를 확인하고 게시하세요.

// 업로드 후 처리 완료 대기
await client.v1.mediaInfo(mediaId);  // STATUS 폴링
// twitter-api-v2 라이브러리는 longVideo 옵션이 자동 대기 처리

(5) "같은 영상이 매주 비슷해요"

주제 풀에 다양성이 부족한 경우입니다. topic-pool.json최소 30개 이상 채우고, 매주 GPT가 그중 하나를 골라 변주하도록 합니다. 또는 X의 트렌딩 토픽을 끌어와 동적으로 주제를 정하는 방식이 가장 자연스럽습니다.

12. 다음 단계 - 멀티 플랫폼 확장

X 게시까지 자동화했으면, 같은 영상을 다른 플랫폼에도 뿌릴 수 있습니다. 다만 플랫폼마다 사양이 다르니 최종 인코딩 단계에서 분기해야 합니다.

# Instagram Reels용 (1080×1920, 모노 → 스테레오 변환)
ffmpeg -i base.mp4 -vf "scale=1080:1920:flags=lanczos" \
  -af "pan=stereo|c0=c0|c1=c0" \
  -c:v libx264 -preset fast -crf 23 \
  -c:a aac -b:a 192k instagram.mp4

# YouTube Shorts용 (1080×1920, 60초 이하 보장)
ffmpeg -i base.mp4 -t 60 -vf "scale=1080:1920" \
  -c:v libx264 -preset slow -crf 20 \
  shorts.mp4

# X용 (720p 그대로, 압축률 우선)
ffmpeg -i base.mp4 -c:v libx264 -preset fast -crf 25 x.mp4

각 플랫폼 API에 맞춘 publish-instagram.js, publish-youtube.js를 추가하면 됩니다. 단, Instagram Graph API는 비즈니스 계정이 필요하고, YouTube Data API는 일일 quota 제한이 있으니 사전 확인 필수입니다.

마치며

4편 시리즈로 "AI 영상 콘텐츠 자동화"의 전 영역을 다뤘습니다.

  • 1편: 영상 AI 시장 정리 (Grok·Veo·Kling·Runway 4파전)
  • 2편: Grok Imagine API 단독 통합 (xAI API + Node/Spring)
  • 3편: FFmpeg + Whisper 후처리 (자막·BGM·트랜지션)
  • 4편 (이 글): GitHub Actions로 무인 운영 (스케줄·시크릿·실패 대응)

완성된 파이프라인의 운영 비용은 주 $2, 월 $8 수준입니다. 사람이 매주 영상 한 편을 만들면 최소 2시간이 들어갑니다. 시급 $20으로 환산해도 주 $40, 월 $160. 자동화 시 월 $150 이상의 시간 비용을 회수한다는 의미입니다.

중요한 건 "100% 자동화"가 아닙니다. 품질 검수는 사람이, 반복 작업은 기계가 하는 것이 핵심입니다. 매주 월요일 오전에 자동 게시된 영상을 30초 살펴보고 마음에 들면 그대로, 별로면 다음 주 프롬프트를 살짝 수정하세요. 이 정도 개입이면 콘텐츠 큐레이터 역할은 유지하면서 노가다는 없앤 "진짜 레버리지"가 만들어집니다.

다음 시리즈에서는 이 영상 파이프라인에서 나온 데이터(조회수·좋아요·댓글)를 다시 GPT에 피드백해서 자동으로 다음 주 콘텐츠를 개선하는 자기 학습 루프를 다룰 예정입니다. "콘텐츠 → 지표 수집 → 분석 → 다음 콘텐츠 개선"이 한 사이클로 도는 진짜 무인 운영의 모습을 코드로 보여드리겠습니다.