GitHub Actions + Gemini로 콘텐츠 자동화 파이프라인 구축하기
콘텐츠 재활용을 수동으로 하면 어떻게 되냐고?
블로그 포스트 하나를 LinkedIn, Twitter, 뉴스레터용으로 각각 편집하는 데 평균 2-3시간. 주 2회 발행이면 한 달에 24시간 순수 copy-paste 작업. 이걸 아무도 고마워하지 않는다. AI가 하면 되니까.
이 글은 실제로 작동하는 자동화 파이프라인을 만드는 방법이다. GitHub Actions + Google Gemini API를 조합해서, 블로그 포스트를 push하면 자동으로 3가지 채널용 콘텐츠가 생성되는 시스템.
아키텍처 개요
[블로그 MD 파일 push]
↓
[GitHub Actions 트리거]
↓
[Python 스크립트: 원문 파싱]
↓
[Gemini API: 3가지 버전 생성]
├── LinkedIn 포스트 (200-300자)
├── Twitter 스레드 (5-8개 트윗)
└── 뉴스레터 섹션 (500-700자)
↓
[결과물을 PR 코멘트로 첨부]
간단해 보이지만 실제 구현할 때 함정이 몇 개 있다. 하나씩 짚어본다.
Step 1: GitHub Actions 워크플로우 설정
# .github/workflows/content-repurpose.yml
name: Content Repurposer
on:
push:
paths:
- 'blog/**/*.md'
- 'docs/**/*.md'
jobs:
repurpose:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # diff 비교용
- name: Get changed files
id: changed-files
run: |
git diff --name-only HEAD~1 HEAD -- '*.md' > changed.txt
cat changed.txt
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install google-generativeai python-frontmatter
- name: Run repurposer
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python scripts/repurpose.py changed.txt
fetch-depth: 2가 핵심이다. 기본값 1로 설정하면 이전 커밋이 없어서 git diff가 동작하지 않는다. 이걸 놓치면 파이프라인이 빈 파일 리스트로 실행된다.
Step 2: Gemini 프롬프트 설계
단순히 "이걸 LinkedIn 포스트로 바꿔줘"는 안 된다. 결과물이 매번 달라지고, 플랫폼 특성을 무시한다.
실제로 사용하는 프롬프트 구조:
import google.generativeai as genai
import frontmatter
def repurpose_content(md_path: str, platform: str) -> str:
with open(md_path) as f:
post = frontmatter.load(f)
title = post.metadata.get('title', '')
body = post.content[:3000] # 토큰 제한 고려
prompts = {
'linkedin': f"""
당신은 B2B 테크 콘텐츠 전문가입니다.
원문 블로그 포스트:
제목: {title}
내용: {body}
LinkedIn 포스트를 작성하세요:
- 첫 줄이 훅이어야 함 (스크롤 멈추게)
- 200-300자 (한국어 기준)
- 3-5개의 핵심 인사이트를 번호 리스트로
- 끝에 질문형 CTA
- 해시태그 5개
- 이모지 적절히 사용 (과하지 않게)
""",
'twitter': f"""
당신은 테크 트위터 전문가입니다.
원문 블로그 포스트:
제목: {title}
내용: {body}
Twitter 스레드를 작성하세요:
- 1/n 형식으로 5-7개 트윗
- 첫 트윗이 가장 강렬한 주장
- 각 트윗 280자 이하 (영어 기준 — 한국어는 140자 기준)
- 마지막 트윗에 원문 링크 플레이스홀더: [LINK]
- 리트윗 유도하는 마무리
""",
'newsletter': f"""
당신은 뉴스레터 편집자입니다.
원문 블로그 포스트:
제목: {title}
내용: {body}
뉴스레터 섹션을 작성하세요:
- 500-700자 (한국어 기준)
- 독자가 이미 뉴스레터를 구독 중이라는 가정
- 핵심 3가지만 추출
- 원문으로 연결되는 자연스러운 CTA
- 형식: 소제목 + 본문 + CTA
"""
}
model = genai.GenerativeModel('gemini-2.0-flash')
response = model.generate_content(prompts[platform])
return response.text
중요한 설계 결정: 각 플랫폼에 명시적인 글자 수 제약을 준다. "짧게 써줘"는 안 먹힌다. "200-300자"는 먹힌다.
Step 3: 결과물 PR 코멘트 첨부
Actions에서 직접 Slack이나 이메일로 보내는 것보다, GitHub PR 코멘트에 붙이는 게 낫다. 이유:
- 검토 후 수정 가능
- 히스토리 추적됨
- 추가 설정 불필요
import os
import subprocess
from github import Github
def post_to_pr(content: dict, md_path: str):
g = Github(os.environ['GITHUB_TOKEN'])
repo = g.get_repo(os.environ['GITHUB_REPOSITORY'])
# 현재 커밋의 PR 찾기
sha = subprocess.check_output(
['git', 'rev-parse', 'HEAD']
).decode().strip()
pulls = repo.get_pulls(state='open')
for pr in pulls:
if pr.head.sha == sha:
comment = format_comment(content, md_path)
pr.create_issue_comment(comment)
break
def format_comment(content: dict, source: str) -> str:
return f"""## 🔄 Content Repurposer 결과
**원문:** `{source}`
### LinkedIn
{content['linkedin']}
---
### Twitter 스레드
{content['twitter']}
---
### 뉴스레터
{content['newsletter']}
---
*by [Content Repurposer](https://github.com/p4r4d0xb0x/content-repurposer)*
"""
Step 4: 실전에서 겪은 함정들
Gemini API 레이트 리밋
Flash 모델 무료 티어는 분당 15 요청. 파일 3개 × 플랫폼 3개 = 9 요청이면 여유 있지만, 여러 파일이 동시에 push되면 터진다.
해결: time.sleep(4) 를 각 요청 사이에 추가. 우아하진 않지만 작동한다.
import time
for platform in ['linkedin', 'twitter', 'newsletter']:
result[platform] = repurpose_content(md_path, platform)
time.sleep(4) # 레이트 리밋 방지
마크다운 frontmatter 파싱 오류
python-frontmatter 라이브러리가 일부 특수문자 있는 제목에서 파싱 실패한다. 방어 코드 필요:
try:
post = frontmatter.load(f)
title = post.metadata.get('title', '')
except Exception:
# fallback: 첫 줄에서 제목 추출
f.seek(0)
for line in f:
if line.startswith('# '):
title = line[2:].strip()
break
콘텐츠 길이 초과
Gemini가 가끔 요청보다 훨씬 긴 결과를 준다. LinkedIn 포스트인데 800자 나오는 경우.
프롬프트에 명시적 제약 + 결과 후처리 조합:
def trim_to_limit(text: str, limit: int) -> str:
if len(text) <= limit:
return text
# 문장 단위로 자르기
sentences = text.split('.')
result = ''
for s in sentences:
if len(result) + len(s) + 1 <= limit:
result += s + '.'
else:
break
return result.strip()
실제 성능 수치
이 파이프라인을 2주간 운영한 결과:
| 항목 | 수동 | 자동화 |
|---|---|---|
| 처리 시간 | 2-3시간/포스트 | 45초/포스트 |
| 비용 | 인건비 | Gemini Flash $0.002/포스트 |
| 품질 | 일정 | 80% 이상 즉시 사용 가능 |
| 일관성 | 사람에 따라 다름 | 프롬프트 기반 일관성 유지 |
"80% 즉시 사용 가능"이 핵심이다. 100%를 목표로 하면 프롬프트 튜닝에 무한정 시간을 쓴다. 80%에서 사람이 20%를 다듬는 게 현실적이고 빠르다.
이걸 SaaS로 만들면?
위 파이프라인을 직접 구축하면 장점이 있다. 완전한 커스터마이즈, 자신의 인프라, API 키 직접 관리.
단점도 있다. 초기 설정 4-6시간. 에러 디버깅 혼자 해결. 플랫폼별 프롬프트 계속 튜닝.
그래서 Content Repurposer를 만들었다. 위의 모든 복잡함을 CLI 한 줄로 압축:
repurpose post.md --platforms linkedin,twitter,newsletter
설정 없음. API 키 발급도 필요 없음. 3월 8일 오픈소스 + 유료 티어 동시 런칭.
직접 구축하든, 도구를 쓰든 — 콘텐츠 재활용 자동화는 지금 당장 시작해야 한다. 매주 낭비되는 시간을 아무도 돌려주지 않는다.