
예약 발행은 절반만 만들 수 있었습니다
글을 세 편 써놓고 하루에 다 올리기가 아까웠습니다. 작성일을 이틀씩 앞으로 잡아두고, 발행일이 안 된 글은 목록에서 빼는 코드를 넣었습니다.
all.filter(
(post) => post.data.pubDate.getTime() <= Date.now(),
);
코드는 잘 돌았습니다. 발행일 전에는 글이 안 보였습니다.
그런데 발행일이 지나도 안 보였습니다.
사이트의 시계는 멈춰 있습니다
범인은 Date.now()였습니다. 정확히는 저 줄이 언제 실행되는지가요.
Astro는 빌드할 때 HTML을 전부 만들어놓고 끝냅니다. 저 filter도 그때 딱 한 번 돕니다. 방문자가 페이지를 열 때 도는 게 아닙니다.
그러니까 배포된 사이트에서 Date.now()는 “지금”이 아닙니다. “마지막으로 빌드한 시각”입니다. 9월 16일 오후에 배포했으면 그 사이트의 시계는 거기서 멈춰 있습니다. 9월 18일이 와도 사이트는 모릅니다. 알려줄 사람이 없으니까요.
빠져 있던 건 조건문이 아니라 다시 빌드하라고 말해줄 누군가였습니다.
그럼 누가 말해주나
세 가지를 놓고 봤습니다.
서버에서 매번 만들기. 요청이 올 때마다 페이지를 생성하면 Date.now()가 진짜 지금이 됩니다. 확실한데, 정적 사이트를 그만두는 선택입니다. 글 열두 편 보여주자고 할 일은 아닙니다.
브라우저에서 가리기. 예약글까지 전부 빌드해두고 자바스크립트로 숨기는 방법입니다. 이건 예약이 아니라 가리기입니다. HTML 안에 본문이 이미 들어 있어서 소스 보기 한 번이면 다 나옵니다. 검색엔진도 가져갑니다.
밖에서 시간 맞춰 다시 빌드시키기. 사이트 안에 시계를 못 넣으니 밖에 두는 겁니다.
세 번째를 골랐습니다.
버튼은 이미 있었습니다
Cloudflare Workers Builds에 배포 훅(Deploy Hook) 이 있습니다. 설정에서 하나 만들면 주소가 나오고, 그 주소로 POST를 보내면 빌드가 돕니다. 커밋을 밀었을 때 도는 것과 같은 빌드입니다.
Settings → Builds → Deploy Hooks → Create deploy hook
주의할 게 하나 있습니다. 이 주소에는 인증 헤더 같은 걸 따로 안 붙입니다. 주소 자체가 열쇠입니다. 문서에도 적혀 있습니다.
Anyone with access to the URL can trigger builds for your Worker.
그래서 소스에는 절대 안 넣고 GitHub 저장소 시크릿에 넣었습니다. 새면 훅을 지우고 새로 만들면 됩니다.
이제 이 버튼을 때맞춰 눌러줄 손만 있으면 됩니다. GitHub Actions를 쓰기로 했습니다.
“한 시간에 한 번 무조건 배포”는 안 하기로 했습니다
제일 간단한 건 매시간 무조건 훅을 때리는 겁니다. 정말 세 줄이면 끝납니다.
그런데 하루 24번, 한 달 720번 빌드가 돕니다. 글이 하나도 안 바뀐 날에도요. 배포 이력이 의미 없는 줄로 꽉 차고, 빌드 시간도 공짜는 아닙니다.
눌러야 할 때만 누르게 하고 싶었습니다. 그러면 “눌러야 할 때”를 어떻게 아느냐가 남습니다.
시각을 비교하면 한 번 놓치고 영영 못 따라잡습니다
처음 떠올린 건 이겁니다. “지난 한 시간 안에 발행일이 지난 글이 있으면 누른다.”
if (pubDate > oneHourAgo && pubDate <= now) deploy();
되긴 됩니다. 한 번도 안 밀렸을 때만요.
GitHub Actions의 예약 실행은 정시에 돈다는 보장이 없습니다. 문서에 대놓고 적혀 있습니다.
The
scheduleevent can be delayed during periods of high loads of GitHub Actions workflow runs. High load times include the start of every hour.
매시 정각이 혼잡 시간이라고 자기들이 적어뒀습니다. 5분마다 돌게 걸어둔 워크플로가 실제로는 18분 간격으로 돌더라는 기록도 있습니다.
그러면 저 한 시간짜리 창을 놓치는 순간이 옵니다. 실행이 한 번 건너뛰거나, 돌았는데 배포가 실패하거나. 그리고 한 번 놓치면 그 글은 영영 안 올라옵니다. 다음 시간에는 “지난 한 시간 안에 지난 글”이 아니니까요.
시간을 기준으로 판단한 게 문제였습니다.
시각 대신 상태를 비교했습니다
질문을 바꿨습니다. “방금 발행일이 지난 글이 있나”가 아니라 “발행일이 지났는데 사이트에 아직 없는 글이 있나”로요.
앞쪽 목록은 저장소의 글 파일에서 읽으면 됩니다. 뒤쪽 목록은 사이트가 직접 알려줍니다. sitemap-0.xml 이 그 목록입니다. 빌드할 때 만들어지니까, 지금 사이트에 실제로 올라와 있는 주소가 거기 다 들어 있습니다.
const missing = due.filter(
(slug) => !sitemap.includes(`/blog/${slug}/`),
);
due는 발행일이 지난 글, sitemap은 지금 올라와 있는 글. 차집합이 비어 있지 않으면 누릅니다.

이렇게 하면 밀려도 상관없습니다. 9시 5분에 돌 게 9시 48분에 돌아도, 그때도 여전히 “지났는데 없는 글”이라 잡힙니다. 배포가 실패해도 다음 시간에 다시 잡습니다. 이미 올라온 뒤에 또 돌면 차집합이 비어서 아무것도 안 합니다.
몇 번을 돌리든 결과가 같습니다. 밖에서 뭔가를 자동으로 돌릴 때 이게 제일 중요한 성질인 것 같습니다. “정확히 한 번 실행되게” 만드는 것보다 “여러 번 실행돼도 문제없게” 만드는 쪽이 훨씬 쉽습니다.
cron을 정각에 안 잡았습니다
on:
schedule:
- cron: '5 * * * *'
정각(0 * * * *)이 아니라 5분입니다. 위에 인용한 그 문서가 매시 정각이 혼잡하다고 했고, 다른 분으로 옮기면 지연이 준다고 권하고 있어서요. 한 글자 바꾸는 거라 안 할 이유가 없습니다.
두 가지 더 있습니다.
이 시각은 기본이 UTC입니다. 한국 시간이 아닙니다. 저는 매시간 도는 거라 상관없지만, “매일 오전 9시”처럼 잡을 거면 9시간을 빼서 적거나, cron 옆에 timezone: "Asia/Seoul"을 붙이면 됩니다. 이 옵션은 올해 3월에 생겼습니다. 며칠 전에 같은 이유로 사이트 날짜가 하루씩 밀렸던 걸 생각하면 두 번 겪을 일은 아닙니다.
60일 규칙이 있습니다. “저장소에 60일 동안 활동이 없으면 예약 실행이 꺼진다”는 이야기를 여기저기서 봤습니다. 문서를 직접 찾아보니 조건이 하나 더 붙어 있었습니다.
In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days.
공개 저장소라고 적혀 있습니다. 제 블로그 저장소는 비공개라 문서상으로는 해당이 안 됩니다.
그래도 적어둡니다. 글을 쓰는 동안에는 커밋이 계속 생기니 두 달을 비울 일도 없지만, 이런 건 “안 걸린다”보다 “왜 안 걸리는지”를 알아두는 쪽이 낫습니다. 저장소를 공개로 돌리는 날이 오면 그때 걸리는 조건이니까요.
개발 서버에서는 다 보이게 뒀습니다
거르는 건 배포된 사이트에서만입니다.
const published = import.meta.env.DEV
? all
: all.filter(
(p) => p.data.pubDate.getTime() <= Date.now(),
);
npm run dev로 띄우면 예약글도 그대로 보입니다. 안 그러면 다음 주에 나갈 글을 쓰는 동안 제 화면에서도 안 보입니다. 미리보기가 안 되는 예약 발행은 쓸 수가 없습니다.
결국 몇 분 늦게 열립니다
솔직하게 적으면 이 구조는 정확하지 않습니다.
작성일을 오전 9시로 잡아도 글이 9시 정각에 열리지 않습니다. 9시 5분에 확인이 돌고, 그게 밀려서 9시 20분에 돌 수도 있고, 훅을 누른 뒤 빌드가 끝나기까지 또 1~2분 걸립니다. 실제로 열리는 건 9시 10분일 수도 9시 40분일 수도 있습니다.
개인 블로그라 상관없습니다. 그런데 상관있는 일이면 — 정각에 맞춰 뭔가 공개해야 한다면 — 이 구조를 쓰면 안 됩니다. 그때는 정적 사이트를 그만두거나 시간을 지켜주는 다른 걸 붙여야 합니다.
뭘 포기했는지 알고 포기하는 것과 모르고 포기하는 건 다릅니다. 저는 분 단위 정확도를 내주고 단순함을 가져왔습니다.
정리
정적 사이트에 “시간이 지나면 바뀌는 것”을 붙이려면 조각이 두 개 필요합니다.
빌드 안쪽 — 무엇을 보여줄지 정하는 코드. 이건 평범한 조건문입니다.
빌드 바깥쪽 — 언제 다시 만들지 정하는 것. 이쪽을 빠뜨리기 쉽습니다. 조건문을 쓰고 나면 다 만든 기분이 들거든요.
저는 앞쪽만 만들어놓고 예약 발행을 붙였다고 생각했습니다. 사이트에는 시계가 없는데요.
그리고 밖에서 거는 자동화는 정시에 도는 걸 믿지 말고, 여러 번 돌아도 괜찮게 만드는 편이 낫습니다. 시각을 비교하면 한 번 밀릴 때 무너지고, 상태를 비교하면 밀려도 다음 번에 따라잡습니다.
아직 오래 굴려보진 않았습니다. 크게 밀리는 날이 오면 그때 다시 적겠습니다.
고친 곳
2026년 9월 29일, 글에 붙일 참고 문서를 하나씩 다시 열어보다가 본문과 어긋나는 곳을 찾아 고쳤습니다.
- cron 시간대 설명을 고쳤습니다. GitHub Actions는 2026년 3월부터
timezone옵션을 지원해서, 9시간을 빼 적지 않아도 됩니다.
참고한 문서
- Cloudflare 문서 — Workers Builds Deploy Hooks — 주소 자체가 비밀번호라는 점, 만드는 경로
- GitHub 문서 — schedule 이벤트 — 정각 지연, 공개 저장소 60일 비활성화
- GitHub 문서 — on.schedule 문법 (timezone) — cron 기본값은 UTC, timezone 옵션
- Astro 문서 — On-demand rendering — 기본은 빌드할 때 한 번 만드는 정적 페이지
댓글
로그인도 이메일도 필요 없습니다. 이름만 적고 편하게 남겨주세요.
지금은 댓글을 불러올 수 없습니다. 잠시 뒤에 다시 시도해 주세요. 급한 내용이면 ryuma@kakao.com로 보내주셔도 됩니다.