이 블로그는 Eleventy로 빌드해서 Vercel에 올리는 순수 정적 사이트입니다. 요청이 들어올 때 돌아가는 코드가 한 줄도 없습니다. 그런데 글은 미리 써두고 정해진 날에 공개하고 싶었습니다.
동적 사이트에서라면 고민할 것도 없습니다. 요청이 올 때마다 서버가 현재 시각과 발행일을 비교하면 됩니다. 정적 사이트에는 비교할 주체가 없습니다. HTML은 빌드 시점에 전부 만들어져 CDN에 얹히고, 그 뒤로는 아무도 시간을 확인하지 않습니다.
그래서 문제를 두 조각으로 나눴습니다.
1번이 없으면 미래 글이 그대로 공개됩니다. 2번이 없으면 발행일이 지나도 사이트는 어제 그대로입니다. 둘 다 있어야 예약 게시가 됩니다. 하나씩 보겠습니다.
1. 빌드에서 미래 글을 빼기
Eleventy에는 디렉터리 데이터 파일이 있습니다. posts/ 폴더 옆에 posts.11tydata.js를 두면 그 폴더 안 모든 글에 공통 데이터가 적용됩니다. 여기에 eleventyComputed를 쓰면 글마다 계산된 값을 넣을 수 있습니다.
핵심은 permalink를 false로 만들면 파일 자체가 생성되지 않는다는 점입니다.
두 줄이 각각 다른 일을 합니다.
permalink: false— 글 페이지 HTML이 아예 안 만들어집니다. URL을 직접 쳐도 404입니다.eleventyExcludeFromCollections: true— 컬렉션에서 빠집니다. 목록 페이지에도,sitemap.xml에도 안 들어갑니다.
컬렉션에서만 빼면 목록에는 안 보이는데 URL은 살아 있습니다. 파일만 안 만들면 목록과 sitemap에 링크는 남는데 클릭하면 404입니다. 후자가 특히 나쁩니다 — 검색엔진에 죽은 URL을 직접 제출하는 셈이라서요.
2. 로컬에서는 미래 글이 보여야 한다
위 코드에서 눈에 띄는 조건이 하나 있습니다.
Eleventy는 실행 모드를 이 환경 변수로 알려줍니다. eleventy는 build, eleventy --serve는 serve입니다. 그래서 npm run dev에서는 미래 글이 그대로 보이고, npm run build에서만 사라집니다.
일부러 이렇게 뒀습니다. 예약 게시의 가장 큰 위험은 발행 당일에 오타를 발견하는 것입니다. 로컬에서까지 글이 숨어버리면 공개되기 전에 확인할 방법이 없습니다.
대신 이 비대칭은 반드시 문서에 적어야 합니다. 적어두지 않으면 "dev에서는 보이는데 build 결과물에는 없다"가 버그로 보입니다. 정상 동작인데도요.
3. 날짜는 UTC로 파싱된다 — 한국시간 아침 9시
YAML front matter의 날짜는 UTC 자정으로 파싱됩니다.
날짜만 쓰면 한국 독자 기준으로 그날 아침 9시에 공개됩니다. 우연히도 나쁘지 않은 시각이라 굳이 시:분을 붙이지 않고 그대로 씁니다. 하지만 이걸 모르면 "8월 22일로 걸었는데 자정에 안 올라왔다"고 한참 헤매게 됩니다.
4. 진짜 문제 — 정적 사이트에는 아무 일도 일어나지 않는다
여기까지가 흔히 소개되는 방법입니다. 그리고 여기서 멈추면 예약 게시는 동작하지 않습니다.
빌드는 무언가를 푸시할 때 돕니다. 8월 22일이 되었다는 사실은 저장소에 아무 커밋도 만들지 않습니다. 커밋이 없으면 빌드도 없고, 빌드가 없으면 CDN에 얹힌 HTML은 마지막 배포 그대로입니다. 글은 영원히 안 나옵니다.
그래서 시간이 흐른다는 사실 자체를 누군가 알려줘야 합니다. GitHub Actions 크론이 매일 Vercel 배포 훅을 때리는 방식으로 해결했습니다.
배포 훅은 Vercel 프로젝트 설정에서 만드는 URL입니다. POST 한 번이면 지정한 브랜치로 배포가 돕니다. 크론 시각을 00:05 UTC로 둔 건 앞서의 UTC 자정 파싱과 맞추기 위해서입니다 — 자정에 발행 조건이 참이 되고, 5분 뒤 빌드가 그 사실을 반영합니다.
스케줄은 최선 노력 기반이라 러너가 붐비면 수십 분씩 밀립니다. 예약 게시에는 상관없습니다 — 몇 분 늦게 올라오는 건 아무도 모릅니다. 하지만 분 단위 정확도가 필요한 일에 이 크론을 쓰면 안 됩니다.
5. 밟고 나서 알게 된 함정들
로케일이 둘이면 날짜가 어긋난다
이 사이트는 모든 글이 한국어·영어 한 쌍입니다. 그런데 발행 판정은 파일별 date로 하니, 두 파일의 날짜가 다르면 한쪽만 먼저 공개됩니다.
그러면 공개된 쪽의 hreflang은 아직 존재하지 않는 URL을 가리킵니다. canonical과 hreflang이 조용히 깨지는데, 브라우저로 보면 멀쩡해 보입니다. 검색엔진만 이상하게 봅니다.
그래서 두 파일의 date가 다르면 빌드를 실패시키는 검사를 넣었습니다. 사람이 매번 기억하는 대신 빌드가 막게 하는 편이 훨씬 안전합니다.
글끼리 거는 링크가 미래를 향할 수 있다
이게 더 미묘합니다. 8월 22일 글에서 8월 30일 글로 링크를 걸면, 22일부터 30일까지 그 링크는 404입니다. 링크를 걸 때는 대상 글 파일이 저장소에 멀쩡히 있으니 아무 문제 없어 보입니다. 8일 뒤에 스스로 나으니 흔적도 잘 안 남습니다.
규칙은 단순합니다. 글끼리 거는 링크는 항상 과거 방향이어야 합니다. 이것도 빌드에서 검사합니다 — 본문의 글 링크를 전부 긁어서 대상이 존재하는지, 그리고 대상의 날짜가 나보다 이르거나 같은지 확인합니다. 날짜가 YYYY-MM-DD 문자열이라 사전순 비교가 곧 날짜 비교입니다.
발행일을 미래로 되돌리면 글이 내려간다
이미 공개된 글의 date를 미래로 바꾸면, 다음 빌드에서 그 글은 사이트에서 사라집니다. 오탈자를 고치면서 날짜를 같이 손대면 URL이 통째로 죽습니다. 발행일 수정은 항상 조심해야 합니다.
6. 정리
전부 합쳐서 애플리케이션 코드 40줄 남짓입니다. 정적 사이트에 데이터베이스나 서버리스 함수를 끌어들이지 않고도 예약 게시가 돕니다.
구조를 다시 요약하면 이렇습니다.
- 빌드 시각을 유일한 판정 기준으로 삼는다. 요청 시각이라는 개념이 없으므로 다른 기준은 없습니다.
- 미래 글은 파일과 컬렉션 양쪽에서 뺀다. 한쪽만 빼면 404 링크나 살아 있는 URL이 남습니다.
- 재빌드를 외부에서 밀어준다. 이게 빠지면 나머지가 전부 맞아도 글은 안 나옵니다.
- 로컬에서는 미래 글이 보이게 둔다. 발행 전에 확인할 방법이 필요합니다.
정적 사이트로 블로그를 운영하면서 "쓸 때 몰아 쓰고 나눠서 내보내고 싶다"는 생각이 들었다면, 생각보다 훨씬 적은 코드로 됩니다. 대신 재빌드를 밀어주는 쪽을 잊지 마세요. 처음 만들면 거의 반드시 그걸 빠뜨립니다.