# GitHub Actions로 3시간마다 예약 발행 체크하기

> 정적 블로그의 예약 발행과 Cloudflare 빌드 제한, 두 가지 과제를 동시에 해결하는 방법을 공유합니다. GitHub Actions로 3시간마다 퍼블리시 체크를 돌리고, 조건이 맞을 때만 빌드하는 운영 전략을 정리했어요.

**Published:** 2026-01-30 | **Updated:** 2026-01-30

---


## 들어가며

정적 블로그를 운영하다 보면 “예약 발행”과 “빌드 제한”이 동시에 문제가 됩니다. 특히 **매번 빌드하는 방식**은 Cloudflare Pages 같은 플랫폼에서 빌드 제한에 쉽게 걸릴 수 있어요.

이 글에서는 제가 실제로 운영 중인 방식, **GitHub Actions로 3시간마다 퍼블리시 체크를 돌려 예약 발행을 처리하는 방법**을 정리합니다. 핵심은 “정해진 시간에만 확인하고, 필요할 때만 퍼블리시한다”는 전략이에요.

{{< img src="images/contents/og_bg_thumb.png" alt="3시간마다 퍼블리시 체크를 실행하는 워크플로우를 상징하는 썸네일 이미지 - GitHub Actions와 Cloudflare Pages의 조합" caption="이미지: Nanobanana AI로 생성" >}}

---

## 왜 3시간 주기 체크가 필요했나

### 1) 예약 발행을 안정적으로 처리하기 위해

정적 사이트는 기본적으로 **시간이 지나도 자동으로 글이 공개되지 않습니다.** 빌드가 다시 돌아야만 게시 상태가 바뀌죠. 그래서 “예약 발행”을 하려면 **정해진 시점에 빌드가 트리거**되어야 합니다.

### 2) Cloudflare 빌드 제한을 피하기 위해

글을 자주 다듬거나 여러 포스트를 준비하다 보면 빌드 횟수가 급격히 증가할 수 있어요. 매 커밋마다 빌드가 도는 구조는 **제한을 쉽게 소진**합니다. 그래서 저는 **3시간 주기로 상태를 확인하고, 필요할 때만 빌드되도록** 흐름을 바꿨습니다.

{{< img src="images/contents/traffic-signal-build-limit.jpg" alt="Cloudflare 빌드 제한 초과되는건 마치 신호등 빨간불 처럼 멈춤이 필요하다는 거겠죠" caption="사진: <a href='https://unsplash.com/ko/사진/신호등이-빨간-신호를-보여준다-tK082PWHpuk' target='_blank' title='새 창에서 열림'>Unsplash</a>의 <a href='https://unsplash.com/ko/@exoloomx' target='_blank' title='새 창에서 열림'>Matias Argandona</a>" >}}

---

## 동작 방식 요약

전체 흐름은 다음과 같습니다.

1. **3시간마다** GitHub Actions가 실행 (UTC 기준)
2. 저장소에서 **예약 발행 대상이 있는지 확인**
3. 조건을 만족하면 퍼블리시 스크립트 실행
4. 변경 사항을 커밋/푸시 → Cloudflare가 빌드

이렇게 하면 “예약 발행 체크”는 주기적으로 일어나지만, **실제 퍼블리시는 조건이 맞을 때만** 발생합니다.

{{< img src="images/contents/scheduled-publish-flow.png" alt="3시간마다 체크에서 조건 통과 시 퍼블리시로 이어지는 플로우 다이어그램 - 전체 동작 흐름 요약" caption="이미지: Nanobanana AI로 생성" >}}

---

## 정적 사이트 빌드에서 공개 여부가 결정되는 순간

Hugo 같은 정적 사이트는 **빌드 시점**에 `draft`와 `date`를 평가합니다. 즉, 빌드가 돌아가는 순간에 다음 조건을 만족하면 그 글은 **공개 페이지로 생성**돼요.

- `draft: false`일 것
- `date`가 **현재 시간보다 과거**일 것

그래서 “예약 발행”은 **빌드 시점에만 성립**합니다. 빌드가 없으면 예약 시간은 지나가도 페이지가 만들어지지 않아요. 결국 **예약 발행 = 빌드 타이밍 설계**라는 뜻입니다.

{{< img src="images/contents/build-visibility-decision.png" alt="정적 사이트 빌드 과정에서 draft와 date를 평가해 공개 여부가 결정되는 흐름 다이어그램 - Hugo의 빌드 로직" caption="이미지: Nanobanana AI로 생성" >}}

---

## 퍼블리시 조건 설계

제가 쓰는 방식의 핵심은 “퍼블리시 조건”입니다. 예를 들어:

- `draft: false`로 전환된 글이 있는가?
- 예약 시간(`date`)이 현재 시간보다 과거인가?
- 이미 배포된 커밋인가?

이 조건을 충족할 때만 실제 퍼블리시 스크립트를 돌립니다. 이렇게 하면 **헛도는 빌드**를 최소화할 수 있어요.

실제 조건 확인은 워크플로우에서 `.github/scripts/check-scheduled-posts.sh`를 실행해 판단합니다. 이 스크립트가 `true`를 반환하면 퍼블리시 단계로 넘어가요.

### 실제 체크 로직 요약 (내 워크플로우 기준)

스크립트는 **예약 글**과 **새 댓글** 두 가지를 확인합니다.

1. **예약 글 체크**
   - `draft: false`인 글만 대상
   - `date`가 **현재 UTC 시간보다 과거**일 것
   - **최근 6시간 이내**에 발행 시간이 도래한 글만 포함
     (이미 오래된 글은 제외해서 불필요한 빌드를 막음)

2. **새 댓글 체크(요약)**
   댓글 갱신을 반영하기 위해 **간단한 체크만 수행**합니다. 자세한 구조와 운영 방식은 이전 글에서 설명했어요.
   → [정적 사이트에서 동적 댓글 시스템 만들기: Giscus + GraphQL API](/posts/blog-comment-visibility-features/)

즉, “예약 발행”과 “댓글 반영” 두 조건 중 하나만 충족해도 **빌드가 트리거**되도록 설계했습니다.

---

## GitHub Actions 스케줄 설정

GitHub Actions는 `schedule`로 정기 실행을 지원합니다. 저는 **3시간마다** 실행하도록 설정했고, 필요할 때 수동으로 돌릴 수 있게 `workflow_dispatch`도 함께 사용합니다. (UTC 기준)

```yaml
on:
  schedule:
    - cron: "0 */3 * * *"
  workflow_dispatch:
```

정각 근처에 실행되기 때문에 **예약 발행과 잘 맞습니다.** 다만 GitHub Actions의 실행 시간은 지연될 수 있으니, **몇 분 정도 여유를 두고 예약 시간 설정**을 하는 편이 안전해요. 참고로 위 크론은 UTC 기준이라, 한국 시간(KST)으로는 **3시간 간격(예: 09:00, 12:00, 15:00)**으로 동작합니다.

**참고**: `schedule` 워크플로우는 **기본 브랜치에 있는 워크플로우 파일**로 운영하는 편이 안전합니다.

{{< img src="images/contents/github-actions-schedule.png" alt="GitHub Actions 스케줄이 표시된 워크플로우 화면 캡처 - cron 표현식과 workflow_dispatch 설정" caption="캡처: scheduled-publish.yml 화면" >}}

---

## 실제 워크플로우 구성 포인트

제가 쓰는 워크플로우(`scheduled-publish.yml`)는 크게 네 단계입니다.

1. **체크아웃 + 원격 동기화**
2. **예약 발행 조건 검사** (`check-scheduled-posts.sh`)
3. **조건 충족 시 빈 커밋으로 빌드 트리거**
4. **요약 리포트 출력**

아래는 핵심 구조만 요약한 부분입니다.

```yaml
jobs:
  check-and-publish:
    steps:
      - uses: actions/checkout@v4
      - run: git fetch origin main && git pull --rebase origin main
      - run: .github/scripts/check-scheduled-posts.sh
      - if: steps.check.outputs.should_build == 'true'
        run: |
          git commit --allow-empty -m "chore: trigger build for scheduled posts"
          git push
```

이 구성 덕분에 **예약 글이 없을 때는 빌드가 발생하지 않도록** 제어할 수 있습니다.

---

## 운영하면서 얻은 장점

### 1) 예약 발행이 안정적으로 가능해짐
“정해진 시간마다 체크”하는 구조 덕분에 **예약 발행이 자동화**됩니다.

### 2) 빌드 제한 대응
필요할 때만 퍼블리시 하므로 **Cloudflare 빌드 제한을 크게 완화**할 수 있었어요.

### 3) 운영 리듬이 단순해짐
글을 쓰고 `draft: false`로 바꾸기만 하면, **다음 3시간 주기에 자동 발행**됩니다. 운영 부담이 줄어듭니다. 이런 자동화 접근은 블로그의 다른 기능에도 적용하고 있어요. [다국어 블로그의 언어 전환 UX 개선](/posts/language-switcher-banner/)처럼, 한번 설정해두면 운영 부담 없이 돌아가는 구조를 선호합니다.

---

## GitHub/Cloudflare 무료 사용량 (2026-01 기준)

운영 전략을 짤 때 무료 사용량을 **명확히 알고 있는 것**이 가장 중요합니다. 아래는 공식 문서 기준의 핵심 요약이에요.

### GitHub Actions (무료 구간 요약 카드)

- Public 저장소: 기본 GitHub-hosted runner 사용 시 **Actions 사용량 무료**
- Private 저장소: 요금제별 **무료 분/스토리지 제공**
- GitHub Free 개인: **2,000분/월**, **500MB 스토리지**
- 분당 가중치: Linux 1x, Windows 2x, macOS 10x

> Public 저장소는 기본 runner 기준 무료지만, **larger runner 사용 시 과금**될 수 있습니다. 자세한 기준은 아래 참고 링크를 확인하세요.

### Cloudflare Pages (무료 구간 요약 카드)

- 빌드 제한: 월 **500회 빌드**
- 빌드 타임아웃: **20분**
- 동시 빌드: 계정 단위로 집계

> Cloudflare Pages의 Free 플랜 제한은 공식 Limits 문서를 기준으로 정리했습니다.

이 글의 핵심 전략은 “필요할 때만 빌드”하도록 해 **500회 빌드 제한을 지키는 것**입니다.

---

## 참고 링크 (공식 문서)

```
[1] GitHub Actions billing and usage (public repo 무료, 요금제별 무료 분/스토리지, larger runner 과금)
    https://docs.github.com/en/actions/concepts/billing-and-usage

[2] GitHub Actions product billing (플랜별 무료 분/스토리지 표)
    https://docs.github.com/en/enterprise-cloud@latest/billing/concepts/product-billing/github-actions

[3] Cloudflare Pages limits (Free 플랜 빌드 500회, 20분 타임아웃, 동시 빌드 계정 단위 집계)
    https://developers.cloudflare.com/pages/platform/limits/
```

---

## 예약 발행 팁: “정각 지정”은 지연을 부를 수 있어요

GitHub Actions는 스케줄 기반이지만 **몇 분 지연**이 발생할 수 있습니다. 특히 예약 시간을 **정각에 맞춰두면**, 스케줄이 이미 지나간 뒤에 실행되어 **다음 3시간 주기에 빌드**될 수도 있어요.

제가 쓰는 해결 방법은 다음과 같습니다.

1. **예약 시간을 정각보다 5~10분 앞당겨 설정**
   예: 12:00 공개를 원하면 `date: 11:50`처럼 약간 앞당겨 둡니다.

2. **필요할 때는 `workflow_dispatch`로 수동 실행**
   정각에 맞춰야 하는 경우, 예약 시간 직후 수동 트리거로 즉시 빌드를 돌립니다.

3. (선택) **스케줄을 더 촘촘하게 설정**
   빌드 제한 여유가 있다면 3시간 → 1시간으로 줄이는 것도 가능합니다. 다만 이 경우 빌드 횟수와 비용을 다시 계산해야 합니다.

예약 발행은 “정확한 시각”보다 **안정적인 공개**가 중요하다는 점을 기준으로 설계하는 편이 현실적이었습니다.

---

## Cloudflare 빌드 트리거 방식

예약 발행이 필요한 경우, 워크플로우는 **빈 커밋(empty commit)** 을 만들어 Cloudflare Pages 빌드를 트리거합니다. 실제 변경이 없더라도 빌드가 일어나므로 예약 발행에 적합해요.

```bash
git commit --allow-empty -m "chore: trigger build for scheduled posts"
git push
```

저장소에 기록이 남기 때문에, **빌드가 언제 왜 트리거되었는지** 추적하기도 쉽습니다.

---

## 무료 한도 초과를 피하는 운영 전략

### GitHub Actions 쪽 전략

1. **Linux runner 고정**
   macOS/Windows는 분당 가중치가 커서 무료 분을 빠르게 소진합니다. 가능하면 Linux로 고정하는 편이 안전합니다.

2. **스케줄 최소화 + 조건부 실행**
   지금처럼 “조건 만족 시에만 실행”하도록 하면 분 소비가 크게 줄어듭니다.

3. **불필요한 아티팩트/캐시 줄이기**
   저장소 사용량도 무료 한도에 포함되므로, 필요 이상으로 아티팩트를 보관하지 않습니다.

### Cloudflare Pages 쪽 전략

1. **빌드 트리거를 한 곳으로 모으기**
   배포 훅/빈 커밋 등 트리거 방식을 하나로 통일하면 중복 빌드를 막을 수 있습니다.

2. **예약 발행 주기와 빌드 주기 정렬**
   예약 발행 시간은 스케줄 주기에 맞춰 배치해 “한 번의 빌드에 여러 글이 포함”되도록 조정합니다.

3. **빌드 실패 방지**
   빌드 타임아웃(20분)을 넘지 않도록 이미지 최적화/캐시를 사전에 관리합니다.

## Cloudflare API 키(토큰) 연동 방식

현재 워크플로우는 **빈 커밋으로 Cloudflare Pages 빌드를 트리거**하는 구조라서, **Cloudflare API 키가 필수는 아닙니다.**
다만 다음과 같은 경우에는 API 토큰이나 Deploy Hook을 사용하는 편이 더 명확합니다.

- GitHub 커밋 없이 **외부 이벤트로 빌드를 트리거**하고 싶을 때
- 빌드 트리거를 **정교하게 제어**하고 싶을 때

### 방법 A) Deploy Hook 사용 (가장 단순)

Cloudflare Pages의 Deploy Hook은 **URL 한 번 호출**로 빌드를 트리거합니다. 별도의 인증이 없기 때문에, **URL 자체를 비밀로 관리**해야 합니다. 따라서 **이 URL을 GitHub Secrets에 저장**해 사용하는 방식이 안전합니다.

```bash
curl -X POST "$CLOUDFLARE_DEPLOY_HOOK_URL"
```

### 방법 B) Cloudflare Pages API 사용 (토큰 기반)

Pages API를 사용하려면 **API Token**을 만들고, **Cloudflare Pages 권한을 Edit 수준으로** 부여합니다. 이 토큰은 GitHub Secrets에 저장합니다.

예시 (Secrets 이름):
- `CLOUDFLARE_API_TOKEN`
- `CLOUDFLARE_ACCOUNT_ID`
- `CLOUDFLARE_PROJECT_NAME`

```bash
curl "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/pages/projects/$CLOUDFLARE_PROJECT_NAME/deployments" \\
  --request GET \\
  --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
```

{{< img src="images/contents/cloudflare-api-token-create.png" alt="Cloudflare 대시보드에서 API 토큰을 생성하는 화면 캡처 - Pages Edit 권한 설정" caption="캡처: Cloudflare API tokens" >}}

### GitHub Secrets에 키 저장하기

GitHub Actions에서는 **Secrets에만 키를 저장**하고, 워크플로우에서 환경 변수로 사용합니다. UI 또는 `gh` CLI로 등록할 수 있어요.

```bash
gh secret set CLOUDFLARE_API_TOKEN
```

{{< img src="images/contents/github-secrets-cloudflare-token.png" alt="GitHub Secrets에 Cloudflare API 토큰을 등록하는 화면 캡처 - 보안 키 관리" caption="캡처: GitHub Actions secrets" >}}

**주의**: 토큰/훅 URL은 로그에 노출되지 않도록 절대 echo로 출력하지 않습니다. Secrets는 기본적으로 마스킹되지만, 직접 출력하면 사고가 날 수 있어요.

---

## 추가로 생각하는 개선 방향

현재는 “퍼블리시 체크”만 자동화되어 있습니다. 앞으로는 다음도 고려하고 있어요.

- **배포 전 자동 검증** (링크, alt 텍스트, 메타데이터 길이) — [코드 블록 접근성 개선](/posts/code-block-accessibility-improvement/)에서 경험한 것처럼, 수동 점검에 의존하면 놓치는 부분이 생기거든요.
- **실패 알림** (Slack/Discord 알림)

---

## 정리하며

GitHub Actions로 3시간마다 퍼블리시 체크를 돌리는 방식은 **정적 블로그의 예약 발행 문제를 실무적으로 해결**해줍니다. 동시에 Cloudflare 빌드 제한 같은 운영 이슈도 함께 대응할 수 있어요.

정적 사이트 운영에서 “자동화”는 선택이 아니라 생존 전략이더라고요. 비슷한 고민을 하고 있다면, 이 방식이 도움이 될 수 있습니다.

