본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 04. 10:49

GitHub Actions와 Claude를 사용하여 매일 블로그 게시물을 자동으로 발행하는 방법

요약

GitHub Actions와 Claude API를 활용하여 매일 블로그 게시물을 자동으로 생성하고 Shopify에 발행하는 워크플로를 소개합니다. 주제 큐 관리, 중복 확인, API 연동을 통해 수동 작업 없이 콘텐츠를 자동화하는 방법을 다룹니다.

핵심 포인트

  • GitHub Actions cron을 이용한 완전 자동화 워크플로 구축
  • Anthropic API를 활용한 고품질 블로그 초안 생성
  • Shopify Admin API를 통한 무인 게시 프로세스 구현
  • 텍스트 기반 주제 큐와 중복 방지 로직 적용
  • 매일 블로그 자동화는 UTC 06:00에 GitHub Actions cron을 통해 무료로 실행됩니다.
  • 주제 큐(Topic queue) 파일과 라이브 블로그 인덱스(live blog index)를 통한 중복 확인(dedup check)을 수행합니다.
  • 단 한 번의 Anthropic API 호출로 1,800단어 분량의 초안 전체를 생성합니다.
  • Shopify Admin API를 통해 수동 작업 없이 게시물을 발행합니다.

저는 매일 블로그 게시물을 하나씩 발행하지만, 이를 위해 텍스트 에디터를 한 번도 열지 않습니다. GitHub Actions cron 작업이 UTC 06:00에 깨어나서, 큐(queue) 파일에서 다음 주제를 선택하고, 중복을 피하기 위해 라이브 블로그 인덱스와 대조한 뒤, Claude를 호출하여 초안을 작성하고, Shopify Admin API를 통해 제 스토어에 바로 게시합니다. 바로 복사해서 사용할 수 있는 전체 워크플로(workflow)를 소개합니다.

큐 파일(Queue File)과 Cron 트리거

모든 것은 일반 텍스트 파일에서 시작됩니다. 저는 리포지토리(repo) 루트에 topics.txt라는 파일을 유지합니다. 한 줄에 주제 하나씩 작성하며, 각 줄은 파이프(|) 기호로 구분된 제목과 짧은 관점(angle)으로 구성됩니다. 다음과 같은 형태입니다:


How I Auto-Publish a Blog Article Per Day|workflow walkthrough, queue file, dedup, API
Why I Switched My Image Pipeline to [Magnific](https://referral.magnific.com/mQMIvsh)|upscaling tests, before/after counts
...

이 큐(queue)가 단일 진실 공급원(single source of truth)입니다. 아이디어가 떠오르면 한 줄을 추가합니다. 현재 60줄이 쌓여 있는데, 이는 제가 아무것도 건드리지 않아도 이미 두 달 치의 기사가 예약되어 있음을 의미합니다.

트리거는 GitHub Actions 워크플로(workflow)입니다. cron 구문은 사람들이 자주 틀리는 부분이니, 정확한 블록을 여기에 적어둡니다:


on:
  schedule:
...

workflow_dispatch 라인은 보기보다 중요합니다. 이 라인은 Actions 탭에 수동 "Run workflow" 버튼을 추가해 주므로, 뜨거운 주제(hot topic)가 생겼을 때 UTC 06:00를 기다리는 대신 즉시 실행할 수 있습니다. cron은 UTC 기준 매일 오전 6시에 실행되며, 이는 계절에 따라 베를린 시간으로 오전 7시 또는 8시입니다. 이 타이밍 덕분에 저는 첫 커피를 마시기 전에 기사가 라이브 상태가 됩니다.

GitHub cron에 대해 한 가지 주의할 점은 정시성이 보장되지 않는다는 것입니다. 인프라가 바쁜 시기에는 작업이 최대 15분까지 늦게 시작되는 것을 목격했습니다. 블로그 운영에는 전혀 문제가 되지 않는 수준입니다. 만약 정확한 타이밍이 필요하다면 셀프 호스팅 러너 (self-hosted runner)로 전환해야 하겠지만, 매일 발행하는 콘텐츠의 경우 이러한 오차는 무의미합니다.

무료 티어에서는 프라이빗 저장소 (private repos)에 대해 매달 2,000분의 Actions 분량을 제공하며, 퍼블릭 저장소 (public ones)에서는 무제한으로 제공됩니다. 체크아웃 (checkout)부터 발행 (publish)까지 제 전체 작업은 약 90초 내에 완료됩니다. 따라서 한 달에 30번을 실행해도 제 할당량 중 약 45분 정도만 소모됩니다. 제한 수치에는 한참 못 미치는 수준입니다. 제가 실제로 비용을 지불하는 유일한 부분은 Anthropic API 호출이며, 1,800단어 분량의 초안 하나를 작성하는 데 몇 센트 정도가 소요됩니다. 제가 이와 같은 에이전트 (agents)를 프로덕션 (production) 환경에서 어떻게 실행하는지에 대한 전체 아키텍처 (architecture)가 궁금하시다면, Claude Blueprint에서 전체 스택 (stack)을 확인하실 수 있습니다.

라이브 블로그 인덱스(Live Blog Index)를 통한 중복 제거

자동 발행에서 가장 무서운 실패 모드 (failure mode)는 같은 내용을 반복하는 것입니다. 거의 동일한 두 개의 기사가 나란히 놓여 있는 것만큼 신뢰를 빠르게 떨어뜨리는 일은 없습니다. 그래서 저는 무언가를 생성하기 전에, 작업이 제 라이브 블로그 인덱스를 가져와 선택된 주제를 인덱스와 대조하여 확인합니다.

로직은 간단합니다. Shopify Admin API에서 기존 기사의 핸들 (handles)과 제목 목록을 가져온 다음, 다음 대기열 (queue) 항목을 이들과 비교합니다. 저는 정확한 일치 (exact match)가 아닌 퍼지 매칭 (fuzzy match)을 사용하는데, 이는 "How I Auto-Publish a Blog"와 "Auto-Publishing My Blog Daily"가 단어만 다를 뿐 같은 기사이기 때문입니다.

다음은 Node에서 구현된 확인 로직의 형태입니다:

const existing = await fetchAllArticleTitles();
const candidate = queue[0].split("|")[0];
...

중복이 발견되어도 작업을 중단시키지 않습니다. 대신 대기열에서 해당 항목을 밀어내고(shift), 파일을 저장한 뒤 깔끔하게 종료합니다. 그러면 다음 날 다음 주제를 가져오게 됩니다. 이런 방식을 통해 잘못된 대기열 항목이 전체 파이프라인 (pipeline)을 차단하는 일을 방지할 수 있습니다.

또한 프롬프트 (prompt) 내부의 본문 수준에서도 중복을 제거합니다. Claude에게 마지막 10개의 기사 제목을 전달하고, 이들과 겹치지 않도록 명시적으로 지시합니다. 이러한 이중 안전 장치 (belt-and-suspenders approach) 덕분에 200개 이상의 게시물을 발행하는 동안 인덱스 (index)를 깨끗하게 유지할 수 있었습니다. 생성 (generation) 프로세스가 실행되기 전, 제목 확인 단계에서 두 번의 아슬아슬한 상황을 잡아낸 것을 제외하고는 완벽했습니다.

이러한 방식의 가드레일 (guardrail)을 안전하게 실행하는 것에 대한 더 깊은 맥락을 원하신다면, 저는 인덱스 가져오기 (index fetch)를 읽기 전용 (read-only)으로 처리하고 실행 기간 동안 캐시 (cache)하여 반복적인 호출로 API를 계속해서 두드리는 일을 방지합니다.

Anthropic API 호출

이 부분이 모두가 궁금해하는 지점입니다. 생성 과정은 구조화된 시스템 프롬프트 (system prompt)를 사용하여 Claude에 단 한 번의 호출을 보내는 것으로 이루어집니다. 에이전트 루프 (agent loop), 도구 사용 (tool use), 체이닝 (chaining)은 없습니다. 단 한 번의 요청으로 하나의 초안 (draft)을 만듭니다.

시스템 프롬프트는 깁니다. 여기에는 정확한 구조 (TLDR div, 4개의 H2 섹션, Bottom Line), 목표 단어 수, 말투 규칙, 금지어, 그리고 내부 링크 요구 사항이 정의되어 있습니다. 사용자 메시지 (user message)는 짧습니다. 대기열 (queue)에서 가져온 주제 라인과 관점 (angle)만 포함됩니다.

const res = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
...

API 키는 레포지토리 (repo)가 아닌 GitHub Secrets에 저장됩니다. 워크플로 (workflow) 내의 secrets 저장소에서 이를 참조하며, 런타임 (runtime) 시 환경 변수 (environment)에 로드됩니다. 키를 교체하는 작업은 레포지토리 설정에서 2분이면 끝나는 간단한 일입니다.

응답이 돌아오면, 어떤 데이터도 Shopify에 닿기 전에 일반 Node.js 환경에서 검증 단계 (validation pass)를 실행합니다. 단어 수를 세어봅니다. 만약 초안이 설정한 최소 기준 미만이라면 발행하지 않습니다. 실패를 로그 (log)로 남기고 종료하며, 해당 주제는 내일을 위해 대기열에 그대로 남겨둡니다. 또한 모든 엠 대시 (em dashes)를 제거하고, 통화 형식을 정규화하며, 금지된 문구가 있는지 스캔합니다. 이러한 후처리 (post-processing) 레이어는 모델이 지침에도 불구하고 가끔 실수로 포함시키는 사소한 부분들을 잡아냅니다.

제 생성물 중 대략 4%는 첫 번째 시도에서 검증 (validation)에 실패하며, 이는 거의 항상 단어 수 때문입니다. 루프 (loop)를 돌며 재시도하는 대신 (이는 토큰을 낭비하고 통제 불능 상태로 빠질 수 있습니다), 저는 그냥 다음 날이 처리하도록 둡니다. 대기열 (queue)이 충분히 길기 때문에 하루를 건너뛴다고 해서 콘텐츠가 비게 되는 일은 없습니다. 저는 두 가지 접근 방식을 모두 테스트했을 때 단일 호출 (single-call) 대 에이전트 루프 (agent loops)의 신뢰성 트레이드오프 (tradeoffs)를 더 심도 있게 다루었으며, 콘텐츠 생성의 경우 단일 호출이 비용과 예측 가능성 측면에서 매번 승리합니다.

모델은 제가 전달하는 목록에서 내부 링크를 선택하므로, 모든 기사에는 관련 글에 대한 최소 3개의 외부 링크가 포함되어 발행됩니다. 이것 하나만으로도 다른 어떤 단일 변경 사항보다 제 사이트 체류 시간 (time-on-site)을 높이는 데 큰 역할을 했습니다.

Shopify Admin API를 통한 발행

초안이 검증을 통과하면, 마지막 단계는 이를 라이브로 게시하는 것입니다. 저는 Shopify Admin REST API를 사용하여 특정 블로그 내에 기사를 생성합니다. 세 가지가 필요합니다: 스토어 도메인, 블로그 ID, 그리고 write_content 권한 (scope)을 가진 Admin API 액세스 토큰입니다.

블로그 ID를 찾는 과정에서 사람들이 실수를 하곤 합니다. /admin/api/2024-01/blogs.json을 한 번 쿼리하여 원하는 블로그를 찾은 다음, 해당 ID를 하드코딩 (hardcode)하세요. 제 경우에는 ID가 절대 바뀌지 않으므로 매 실행마다 조회할 이유가 없습니다.

생성 호출 (create call)은 다음과 같습니다:

await fetch(`https://${SHOP}/admin/api/2024-01/blogs/${BLOG_ID}/articles.json`, {
  method: "POST",
...

published: true 플래그가 이 과정을 완전히 자동화 (hands-off)로 만들어 줍니다. 초안을 먼저 검토하고 싶다면 false로 설정하세요. 저는 처음 3주 동안 초안 모드로 실행하며 모든 결과물을 수동으로 검토했고, 검증 레이어 (validation layer)를 신뢰할 수 있게 된 후에야 true로 전환했습니다. 이러한 신뢰 구축 기간을 갖는 것은 가치가 있습니다. 그것이 바로 프롬프트 (prompt)가 놓친 엣지 케이스 (edge cases)를 잡아내는 방법입니다.

Shopify는 마크다운 (markdown)이 아닌 body_html을 저장하기 때문에, 전송하기 전에 마크다운을 HTML로 변환합니다. 작은 마크다운 라이브러리를 사용하면 두 줄로 이를 처리할 수 있습니다. 또한 상단에 TLDR div를 원시 HTML (raw HTML)로 삽입하며, 제 테마는 이를 요약 박스 형태로 스타일링합니다.

성공적으로 발행이 완료되면, 사용된 주제를 대기열(queue)에서 제거하고 업데이트된 topics.txt를 저장소(repo)에 다시 커밋(commit)합니다. 이 커밋이 실행 간에 시스템의 상태를 유지(stateful)하게 만드는 핵심입니다. GitHub Actions 토큰은 contents: write 권한을 가지고 있으므로, 이 한 줄의 변경 사항을 푸시(push)할 수 있습니다.

게시물을 확산시키고 싶을 때는 새로운 URL을 Buffer에 넣습니다. 그러면 제가 어떤 앱도 열지 않고도 제 소셜 계정들에 맞춰 예약 게시가 진행됩니다. 크론 틱(cron tick)부터 라이브 기사, 그리고 예약된 소셜 게시물에 이르기까지 전체 체인이 인간의 개입(human in the loop) 없이 실행됩니다.

핵심 요약 (Bottom Line)

이 워크플로우(workflow)는 API 호출 비용으로 하루에 몇 센트 정도가 들며, GitHub Actions 사용 시간(minutes)은 거의 들지 않습니다. 어려운 부분은 API 호출이 아니라 가드레일(guardrails)입니다. 인덱스를 깨끗하게 유지하는 중복 제거(dedup) 체크, 부실한 초안을 차단하는 단어 수 검증(word-count validation), 그리고 한 번의 훌륭한 브레인스토밍 세션을 두 달 치 콘텐츠로 바꿔주는 대기열(queue) 파일이 바로 그것입니다.

작게 시작하세요. 먼저 대기열 파일을 만드세요. published: true로 설정하기 전에, 초안 모드(draft mode)로 실행하며 2주 동안 모든 결과물을 수동으로 검토하십시오. 유사도 임계값(similarity threshold)은 제 예전 제목이 아닌, 귀하의 예전 제목을 기준으로 조정하십시오. 시스템을 신뢰할 수 있게 되면, 스위치를 켜고 실행되도록 두십시오.

제가 Claude를 이와 같은 프로덕션 시스템(production systems)에 어떻게 연결하는지 전체적인 그림을 보고 싶다면, Claude Blueprint에서 전체 설정을 처음부터 끝까지 설명합니다. 맞는 부분은 복사하고 맞지 않는 부분은 무시하며, 내일 아침 당신이 잠든 사이에 무언가를 출시(ship)하십시오.

이 기사에는 제휴 링크가 포함되어 있습니다. 이 링크를 통해 가입하시면, 귀하에게 추가 비용 부담 없이 저에게 소정의 수수료가 지급될 수 있습니다. (광고)

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0