기본 콘텐츠로 건너뛰기

GitHub Actions + Blogger API 자동 발행 시스템 30분 만에 만들기

GitHub Actions + Blogger API 자동 발행 시스템 30분 만에 만들기

마크다운 파일을 Git에 push한다. 끝. 블로그에 자동으로 올라간다.

이게 내가 지금 쓰고 있는 블로그 자동 발행 시스템이다. GitHub Actions가 트리거되면 Python 스크립트가 마크다운을 HTML로 변환하고, Blogger API를 통해 자동 발행한다. 비용은 $0.

이 글에서 전체 코드, OAuth 인증, GitHub Secrets 설정까지 30분 안에 따라할 수 있게 정리한다.

GitHub Actions에서 Blogger로 자동 발행되는 파이프라인 구조도

코드를 몰라도 OK — AI가 대신 실행합니다

코드를 몰라도 할 수 있다. 이 글에 나오는 코드가 어려워 보여도 걱정하지 마라. 이 글 전체를 Claude나 ChatGPT 같은 AI에게 그대로 주면, AI가 코드를 작성하고 세팅까지 안내해 준다. 당신이 할 일은 AI가 알려주는 대로 클릭하고 붙여넣는 것뿐이다. 실제로 나도 이 시스템을 AI와 함께 만들었다.


왜 이 조합인가

블로그 자동 발행 방법 비교

블로그 자동 발행 방법은 여러 가지다. 왜 굳이 GitHub Actions + Blogger API인가?

방법 비용 자동화 자유도 관리
수동 Blogger 편집기 무료 X 낮음 쉬움
WordPress + WP REST API $5~30/월 O 높음 복잡
GitHub Actions + Blogger API 무료 O 높음 단순
Zapier/Make 자동화 $20~50/월 O 중간 쉬움

GitHub Actions + Blogger API의 승리 포인트: 무료 + 자동 + 30줄 코드로 끝남.

GitHub Actions 무료 티어가 월 2,000분을 제공한다. 블로그 글 하나 발행하는 데 1분도 안 걸리니까, 하루 10글을 올려도 한 달에 300분밖에 안 쓴다.


전체 구조

자동 발행 파이프라인 전체 구조

당신의 작업 환경 (옵시디언/에디터)
    │
    │ 마크다운 작성 → git push
    ▼
GitHub Repository
    │
    │ push 감지 → Actions 트리거
    ▼
GitHub Actions Runner (ubuntu)
    │
    │ 1. 마크다운 → HTML 변환
    │ 2. YAML frontmatter에서 제목/태그 추출
    │ 3. Blogger API v3 호출
    ▼
Google Blogger (자동 게시)

총 관여 파일 3개: publish.py (발행 스크립트 30줄), publish-to-blogger.yml (Actions 워크플로우), blogs.yaml (블로그 ID 매핑).


사전 준비 (10분)

시작하기 전에 이것만 있으면 된다:

필요한 것 어디서 소요 시간
GitHub 계정 github.com 있으면 0분
Google Blogger 블로그 blogger.com 5분
Google Cloud 프로젝트 console.cloud.google.com 5분
Python 3.12 (로컬) 최초 토큰 발급 시만 설치돼 있으면 0분

Step 1: Google Cloud + Blogger API 설정 (5분)

Google Cloud OAuth 설정 5단계

1-1. Google Cloud Console에서 프로젝트 생성

console.cloud.google.com에 접속한다.

  1. 상단 프로젝트 선택 → 새 프로젝트 → 이름: blog-auto-publish
  2. 좌측 메뉴 → APIs & ServicesLibrary
  3. "Blogger API" 검색 → Blogger API v3Enable

1-2. OAuth 동의 화면 설정

  1. APIs & ServicesOAuth consent screen
  2. User type: External → Create
  3. App name: Blog Auto Publisher
  4. User support email: 본인 이메일
  5. Developer contact: 본인 이메일
  6. Save and Continue (나머지 기본값)

1-3. OAuth 클라이언트 ID 생성

  1. CredentialsCreate CredentialsOAuth client ID
  2. Application type: Desktop app
  3. Name: blog-publisher
  4. Createclient_secret.json 다운로드

⚠️ client_secret.json은 절대 GitHub에 올리지 않는다. 로컬에서 토큰 발급할 때만 쓴다.


Step 2: Refresh Token 발급 (5분)

이게 가장 헷갈리는 부분이다. Blogger API는 Service Account를 지원하지 않아서 반드시 OAuth 2.0 refresh token이 필요하다. 한 번만 발급하면 자동으로 갱신된다.

로컬에서 토큰 발급

터미널에서:

pip install google-auth-oauthlib

get_token.py 파일을 만든다:

from google_auth_oauthlib.flow import InstalledAppFlow

flow = InstalledAppFlow.from_client_secrets_file(
    'client_secret.json',
    ['https://www.googleapis.com/auth/blogger']
)
creds = flow.run_local_server()

print("=" * 50)
print(f"REFRESH TOKEN: {creds.refresh_token}")
print(f"CLIENT ID: {creds.client_id}")
print(f"CLIENT SECRET: {creds.client_secret}")
print("=" * 50)
print("→ 이 3개 값을 GitHub Secrets에 등록하세요.")

실행:

python get_token.py

브라우저가 열리면 Google 로그인 → 블로그 권한 허용 → 터미널에 3개 값이 출력된다.

이 3개 값을 따로 저장한다. 다음 단계에서 GitHub Secrets에 넣을 것이다.


Step 3: GitHub Repository 세팅 (5분)

GitHub 레포 구조 + Secrets 설정

3-1. 레포 생성

GitHub에서 새 레포를 만든다: blog-auto-publish (Private 권장)

3-2. 폴더 구조

blog-auto-publish/
├── .github/
│   └── workflows/
│       └── publish-to-blogger.yml
├── posts/
│   ├── ko/          ← 한국어 글
│   └── en/          ← 영어 글 (나중에)
├── published/       ← 발행 완료된 글이 여기로 이동
├── publish.py       ← 핵심 발행 스크립트
└── README.md

3-3. GitHub Secrets 등록

레포 → SettingsSecrets and variablesActionsNew repository secret

Secret 이름
G_CLIENT_ID Step 2에서 받은 CLIENT ID
G_CLIENT_SECRET Step 2에서 받은 CLIENT SECRET
G_REFRESH_TOKEN Step 2에서 받은 REFRESH TOKEN
BLOGGER_BLOG_ID_KO Blogger 대시보드 URL의 숫자 부분

블로그 ID 찾는 법: Blogger 대시보드에 접속하면 URL이 https://www.blogger.com/blog/posts/1234567890 형태다. 이 숫자가 블로그 ID.


Step 4: 핵심 코드 작성 (10분)

publish.py 핵심 코드 흐름: 4단계 30줄

4-1. 발행 스크립트 (publish.py)

이 파일이 전부다. 30줄.

import os, glob, frontmatter, markdown
from datetime import datetime
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

def md_to_blogger(md_path):
    """마크다운 → Blogger HTML + 메타데이터"""
    post = frontmatter.load(md_path)
    html = markdown.markdown(post.content, extensions=[
        'extra', 'codehilite', 'tables', 'fenced_code'
    ])
    return {
        'title': post.metadata.get('title'),
        'content': html,
        'labels': post.metadata.get('keywords', []),
    }

# OAuth 인증 (refresh token 자동 갱신)
creds = Credentials(
    None,
    refresh_token=os.getenv('G_REFRESH_TOKEN'),
    token_uri='https://oauth2.googleapis.com/token',
    client_id=os.getenv('G_CLIENT_ID'),
    client_secret=os.getenv('G_CLIENT_SECRET')
)
service = build('blogger', 'v3', credentials=creds)

# 한국어 블로그 발행
blog_id = os.getenv('BLOG_ID_KO')
for md_file in glob.glob('posts/ko/*.md'):
    data = md_to_blogger(md_file)

    result = service.posts().insert(
        blogId=blog_id, body={
            'title': data['title'],
            'content': data['content'],
            'labels': data['labels']
        }, isDraft=False
    ).execute()

    print(f"Published: {data['title']} → {result.get('url')}")

    # 발행 완료 → published/ 폴더로 이동
    os.makedirs('published/ko', exist_ok=True)
    os.rename(md_file, md_file.replace('posts/', 'published/'))

핵심 포인트:

  1. frontmatter.load()로 YAML frontmatter에서 제목, 키워드를 추출한다
  2. markdown.markdown()으로 마크다운 → HTML 변환
  3. service.posts().insert()로 Blogger에 게시
  4. 발행 완료된 파일은 published/로 자동 이동 (중복 발행 방지)

4-2. GitHub Actions 워크플로우

.github/workflows/publish-to-blogger.yml:

name: Publish to Blogger
on:
  push:
    branches: [ main ]
    paths: [ 'posts/**/*.md' ]
  workflow_dispatch:

permissions:
  contents: write

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install markdown google-auth google-api-python-client python-frontmatter

      - name: Publish posts
        env:
          G_CLIENT_ID: ${{ secrets.G_CLIENT_ID }}
          G_CLIENT_SECRET: ${{ secrets.G_CLIENT_SECRET }}
          G_REFRESH_TOKEN: ${{ secrets.G_REFRESH_TOKEN }}
          BLOG_ID_KO: ${{ secrets.BLOGGER_BLOG_ID_KO }}
        run: python publish.py

      - name: Commit published state
        run: |
          git config user.name "Blog Bot"
          git config user.email "bot@blog.com"
          git add -A
          git diff --staged --quiet || git commit -m "Published $(date +%Y-%m-%d)"
          git push

트리거 조건: posts/ 폴더에 .md 파일을 push하면 자동 실행. 수동 실행(workflow_dispatch)도 가능.

마지막 단계에서 published/로 이동된 파일을 다시 commit+push해서 레포 상태를 동기화한다.


Step 5: 테스트 (5분)

테스트 플로우 + 트러블슈팅 가이드

첫 번째 글 발행

posts/ko/ 폴더에 마크다운 파일을 넣고 push한다:

---
title: "테스트: GitHub Actions 자동 발행"
keywords: ["테스트", "자동발행"]
---

# 첫 번째 자동 발행 테스트

이 글은 GitHub Actions를 통해 자동으로 발행되었습니다.
git add posts/ko/test-post.md
git commit -m "Add test post"
git push origin main

GitHub repo → Actions 탭에서 워크플로우 실행을 확인한다. 초록색 체크가 뜨면 성공. Blogger 대시보드에서 글이 올라왔는지 확인한다.

문제가 생기면

증상 원인 해결
401 Unauthorized Refresh token 만료 get_token.py 다시 실행, 새 토큰 GitHub Secret에 업데이트
403 Write access denied (git push) GITHUB_TOKEN 권한 부족 YAML에 permissions: contents: write 추가 (위 코드 참고)
403 Forbidden (API) Blogger API 미활성화 Google Cloud Console → Blogger API v3 Enable
404 Not Found 블로그 ID 틀림 Blogger 대시보드 URL 재확인
Actions 미실행 paths 필터 미매칭 posts/ 폴더 경로 확인

⚠️ 가장 흔한 함정: 마지막 단계에서 published/로 이동한 파일을 다시 commit+push 해야 하는데, permissions: contents: write가 없으면 여기서 403이 뜬다. 나도 여기서 삽질했다.


확장: 다국어 블로그 (Phase 2)

같은 레포에서 영어 블로그도 운영할 수 있다.

# publish.py에 추가
BLOG_MAP = {
    'ko': os.getenv('BLOG_ID_KO'),
    'en': os.getenv('BLOG_ID_EN'),  # 영어 블로그 ID
}

for lang, blog_id in BLOG_MAP.items():
    if not blog_id:
        continue
    for md_file in glob.glob(f'posts/{lang}/*.md'):
        # ... 동일한 발행 로직

GitHub Secrets에 BLOGGER_BLOG_ID_EN만 추가하면 된다. 중요: 한국어와 영어 블로그는 별도 Blogger 블로그로 생성한다. 한 블로그에 두 언어를 섞지 않는다.


스케줄 발행 (보너스)

특정 날짜에 자동 발행하고 싶다면, frontmatter에 publish_date를 넣는다:

---
title: "예약 발행 테스트"
publish_date: 2026-04-01
---

publish.py에서 날짜 체크를 추가:

from datetime import datetime

# 발행 처리 전에 날짜 확인
if data.get('publish_date'):
    pub_date = datetime.fromisoformat(str(data['publish_date']))
    if pub_date > datetime.utcnow():
        print(f"Scheduled for {pub_date}: {data['title']}")
        continue  # 아직 날짜 안 됐으면 스킵

Actions cron을 매일 돌리면 (cron: '0 10 * * *'), 날짜가 도래한 글만 자동으로 발행된다.


정리

자동 발행 시스템 전체 요약

항목 내용
총 코드량 ~30줄 (publish.py) + 20줄 (YAML)
셋업 시간 30분
월 비용 $0
발행 방식 posts/ko/에 .md push → 자동 발행
다국어 폴더 분리 (ko, en, ja) + 별도 블로그
스케줄 cron + publish_date frontmatter

30분 투자로 앞으로 모든 블로그 발행이 자동화된다. git push 한 번이면 끝.

다시 한 번 강조: 코드가 어려워 보여도 직접 이해할 필요 없다. 이 글 URL을 AI(Claude, ChatGPT 등)에게 주고 "이대로 세팅해 줘"라고 하면 된다. AI가 코드를 작성하고, 어디에 붙여넣을지, 어디를 클릭할지 하나하나 안내해 준다. 코딩 경험 0이어도 30분이면 끝난다.


자주 묻는 질문

Refresh token이 만료되면 어떻게 하나요?

Google OAuth refresh token은 기본적으로 만료되지 않는다. 단, 6개월간 미사용하거나 사용자가 비밀번호를 변경하면 무효화된다. 그때는 get_token.py를 다시 실행해서 새 토큰을 발급받고, GitHub Secret을 업데이트하면 된다.

GitHub Actions 무료 티어 한도가 걱정됩니다.

월 2,000분 무료다. 블로그 글 1편 발행에 약 30초~1분 소요. 하루 10편을 올려도 한 달에 300분밖에 안 쓴다. 개인 블로그 수준에서는 한도에 걸릴 일이 거의 없다.

한 번에 여러 글을 push하면 어떻게 되나요?

posts/ko/ 폴더에 마크다운 파일 3개를 넣고 한 번에 push하면, publish.py가 3개를 순서대로 전부 발행한다. Actions는 push 이벤트 1번에 1번 실행되므로, 모든 글이 한 번의 워크플로우에서 처리된다.

Blogger 말고 WordPress에도 적용할 수 있나요?

같은 원리다. publish.py에서 Blogger API 대신 WordPress REST API (wp-json/wp/v2/posts)를 호출하면 된다. 인증 방식이 다르지만 (Application Password 또는 JWT), GitHub Actions 워크플로우 구조는 동일하다.


관련 글


💬 더 많은 인사이트 받기

이 블로그의 새 글과 실시간 크립토/AI 인사이트를 받아보고 싶으시다면:

댓글

이 블로그의 인기 게시물

AI 에이전트 자동화 완전 가이드 2026: Claude로 돈 버는 시스템 만드는 법

유튜브 영상 자동화 시스템을 바이브코딩으로 만들었더니 — 1일1영상 한달만에 구독자 1000명 달성

코딩 까막눈이 AI에게 글쓰기 시켜 블로그·X·텔레그램·네이버 4채널 공장 차린 한 달