KST 기준 자동 게시를 안전하게 만드는 시간 게이트 설계
Futory의 Next.js Markdown 블로그 자동 게시를 기준으로 KST 시간 계산, 하루 1회 중복 방지, 랜덤 게시 시간, 검증 후 배포까지 안전장치를 설계하는 방법을 정리했습니다.
요약
자동 게시는 단순히 cron에 글 생성 명령을 걸어 두는 일이 아닙니다. 특히 Futory처럼 Next.js Markdown 블로그를 운영하면서 매일 한 편의 글을 안정적으로 공개하려면, 글을 쓰는 로직보다 먼저 “언제 실행할 것인가”, “오늘 이미 게시했는가”, “검증에 실패하면 어디서 멈출 것인가”를 정해야 합니다. 자동화가 조금만 느슨해도 같은 날 글이 여러 개 올라가거나, 빌드 실패 상태에서 PM2가 재시작되거나, 공개 URL에서는 보이지 않는 글을 게시 성공으로 착각할 수 있습니다.
Futory의 운영 기준은 한국 시간(KST)입니다. 서버 cron은 UTC 기준으로 여러 시간대에 실행되지만, 실제 게시 판단은 Asia/Seoul 날짜와 시간을 기준으로 해야 합니다. 그리고 매일 random.seed(YYYY-MM-DD)로 정해진 하나의 게시 시간만 통과시키면, 무작위처럼 보이면서도 재현 가능한 운영 리듬을 만들 수 있습니다. 이 글은 바이브코딩으로 만든 블로그 자동 게시에서 KST 시간 게이트와 중복 방지 장치를 어떻게 설계하고 검증할 수 있는지 정리한 기록입니다.
왜 시간 게이트가 먼저 필요한가
cron은 정해진 시간에 명령을 반복 실행하는 도구입니다. 하지만 “매일 오후 2시부터 6시 사이에 한 번만 게시” 같은 요구사항은 cron 표현식 하나로 끝내기 어렵습니다. UTC 기준 서버에서 실행된다면 한국 시간 오후 2시부터 6시는 UTC 오전 5시부터 9시에 해당합니다. 그래서 cron은 넓게 매시간 실행하고, 실제 게시 여부는 스크립트 내부에서 판단하는 방식이 더 안전합니다.
이때 중요한 것은 현재 시간을 서버 기본 시간대로 읽지 않는 것입니다. 컨테이너, 호스트, CI 환경은 서로 다른 시간대 설정을 가질 수 있습니다. Futory 자동 게시에서는 Python의 zoneinfo를 사용해 Asia/Seoul 기준 현재 날짜와 시간을 계산하는 편이 명확합니다. 오늘 날짜 문자열을 만든 뒤 같은 날짜를 랜덤 시드로 사용하면, 하루 동안 여러 번 실행되어도 선택 시간은 항상 같습니다.
예를 들어 오늘 선택 시간이 17시라면 14시, 15시, 16시, 18시에 실행된 작업은 모두 SKIP: not selected hour로 끝나야 합니다. 이 스킵은 실패가 아니라 정상 동작입니다. 자동화 보고에서 스킵과 실패를 구분해야 운영자가 불필요하게 장애로 오해하지 않습니다.
재현 가능한 랜덤 시간이 주는 장점
운영 자동화에서 완전한 무작위는 생각보다 다루기 어렵습니다. 같은 날 14시에 실행했을 때는 15시를 고르고, 15시에 실행했을 때는 18시를 고른다면 게시 조건이 계속 바뀌어 버립니다. 그러면 하루에 한 번만 게시한다는 규칙을 검증하기 어려워집니다. 반대로 날짜를 시드로 고정하면 오늘의 선택 시간은 하루 종일 동일하고, 내일은 자연스럽게 다른 시간이 될 수 있습니다.
이 방식은 디버깅에도 유리합니다. 특정 날짜에 왜 게시되지 않았는지 확인할 때, 그 날짜 문자열로 다시 시드를 설정하면 선택 시간이 무엇이었는지 재현할 수 있습니다. Futory처럼 자동 게시 결과를 매일 쌓는 블로그에서는 이런 재현성이 중요합니다. 문제가 생겼을 때 “그때 운이 나빴다”가 아니라 “그날 선택 시간과 실행 시간이 달랐다”처럼 설명 가능한 운영 로그를 남길 수 있기 때문입니다.
랜덤 시간 게이트는 콘텐츠 운영에도 도움이 됩니다. 매일 같은 시각에 글이 올라오는 기계적인 패턴을 피하면서도, 사람이 직접 시간을 고르지 않아도 됩니다. AI와 자동화가 함께 운영되는 블로그라면 이런 작은 설계가 장기적인 유지보수 부담을 줄여 줍니다.
중복 게시 방지는 파일 기준으로 확인하기
선택 시간이 맞더라도 바로 글을 만들면 안 됩니다. 먼저 content/posts/*.md 안에 오늘 KST 날짜의 글이 이미 있는지 확인해야 합니다. Futory의 글 날짜는 frontmatter의 date: "YYYY-MM-DD"로 관리되므로, 오늘 날짜가 이미 존재하면 SKIP: already published today로 종료하는 것이 안전합니다.
이 검사는 단순하지만 효과적입니다. 이전 실행에서 글 작성과 스테이징 검증까지 성공했지만 배포 과정에서 실패했을 수도 있고, 운영자가 수동으로 오늘 글을 추가했을 수도 있습니다. 이때 자동화가 다시 새 글을 만들면 콘텐츠 일정이 꼬이고, 같은 주제의 글이 중복될 수 있습니다. 따라서 게시 작업의 첫 번째 보호 장치는 “오늘 날짜의 원본 Markdown 글이 존재하는가”를 보는 것입니다.
물론 더 정교하게 하려면 공개 URL 확인 결과까지 상태로 남길 수 있습니다. 하지만 Futory의 현재 구조에서는 Markdown 파일이 콘텐츠의 원천이므로, 날짜 기준 중복 방지가 가장 단순하고 안정적인 출발점입니다. 중요한 것은 이 검사를 글 생성 전에 수행하는 것입니다. 새 파일을 만든 뒤에 중복을 발견하면 이미 불필요한 변경이 생긴 뒤입니다.
글 생성 후에는 배포보다 검증이 먼저다
시간 게이트와 중복 검사를 통과하면 새 글을 작성할 수 있습니다. 이때도 운영 관점의 규칙이 필요합니다. category는 바이브코딩으로 유지하고, 날짜는 KST 오늘 날짜로 넣으며, slug는 영문 소문자 kebab-case로 만들어 기존 파일을 덮어쓰지 않아야 합니다. Futory는 당분간 여러 카테고리로 넓히기보다 AI 개발, 자동화, 배포, 운영이라는 하나의 주제를 깊게 쌓는 전략을 사용합니다.
글 파일이 추가된 뒤에는 곧바로 라이브에 동기화하지 않습니다. 먼저 스테이징에서 npm run test:theme, npm run test:content, npm run build를 실행해야 합니다. 특히 테마 검증은 콘텐츠 자동화에서 빼먹기 쉬운 안전장치입니다. Futory에는 다크/라이트 모드가 있고, components/ThemeToggle.tsx, app/theme-init.tsx, CSS의 data-theme 변수 흐름이 유지되어야 합니다. 새 글 하나를 추가하는 작업이라도 전체 프로젝트를 동기화하기 때문에, 기존 UI 기능이 사라지지 않았는지 확인해야 합니다.
콘텐츠 검증은 frontmatter, 글 구조, 날짜, 카테고리, 태그 같은 규칙을 확인합니다. 빌드 검증은 Next.js가 새 Markdown 글을 실제 페이지로 만들 수 있는지 확인합니다. 이 세 단계 중 하나라도 실패하면 배포하지 않는 것이 원칙입니다. 자동화의 목적은 빠른 공개가 아니라, 실패한 상태를 라이브로 밀어 넣지 않는 것입니다.
호스트 동기화는 보존할 파일을 먼저 생각한다
컨테이너에서 보이는 스테이징 경로와 호스트의 라이브 경로는 다를 수 있습니다. Futory의 작업 경로는 컨테이너 기준 /opt/data/wordblog_next_staging이고, 호스트에서는 /root/hermes-agent/data/wordblog_next_staging로 보입니다. 라이브 경로는 호스트의 /opt/wordblog입니다. 이 차이를 모르고 “/opt에 배포했다”고 말하면 실제 파일 위치를 착각하기 쉽습니다.
동기화할 때는 .git, node_modules, .next, .env를 제외하거나 보존해야 합니다. .env는 라이브 환경 변수이고, node_modules와 .next는 호스트에서 다시 설치하고 빌드할 대상입니다. 무심코 전체 삭제 동기화를 하면 라이브 전용 설정이나 빌드 산출물을 망가뜨릴 수 있습니다. 그래서 동기화는 항상 제외 목록을 갖고, 이후 호스트에서 npm ci || npm install, 테마 검증, 콘텐츠 검증, 빌드, PM2 재시작 순서로 진행하는 편이 안전합니다.
PM2 재시작도 빌드 성공 후에만 실행해야 합니다. 빌드가 실패했는데 기존 프로세스를 재시작하면 정상 동작하던 블로그까지 위험해질 수 있습니다. Futory의 프로세스 이름은 futory-wordblog이고 내부 포트는 127.0.0.1:8042입니다. 재시작 후에는 내부 포트와 공개 도메인을 분리해서 확인해야 원인 파악이 쉬워집니다.
공개 URL 확인까지가 게시 완료다
자동 게시의 완료 기준은 파일 작성이나 PM2 재시작이 아닙니다. 독자가 볼 수 있는 공개 URL에서 새 글 상세 페이지와 /posts 목록이 HTTP 200으로 열리고, 오늘 날짜가 노출되어야 합니다. 상세 페이지가 열리면 slug 라우팅과 빌드 반영을 확인할 수 있고, 목록 페이지에서 오늘 날짜가 보이면 콘텐츠 목록에도 정상 반영되었음을 확인할 수 있습니다.
이 검증은 Next.js, PM2, Nginx, 도메인, 인증서 흐름을 모두 포함합니다. 내부 포트는 정상인데 공개 URL이 실패한다면 Nginx 프록시나 외부 경로를 의심해야 합니다. 반대로 공개 상세 페이지와 목록이 모두 정상이라면 실제 사용자 관점의 게시가 완료된 것입니다. 자동화 보고에는 제목, 날짜, URL, 스테이징 검증, 호스트 검증, 공개 확인 결과를 짧게 남기면 충분합니다.
또한 재시작 직후에는 앱이 몇 초 동안 준비 중일 수 있으므로 공개 확인에는 짧은 재시도가 필요합니다. 즉시 실패했다고 단정하지 말고, 일정 시간 동안 200 응답과 오늘 날짜를 확인한 뒤 최종 상태를 판단하는 것이 좋습니다.
자주 묻는 질문
cron을 하루에 한 번만 실행하면 더 단순하지 않나요?
가능하지만 랜덤 게시 시간 요구사항을 만족하기 어렵습니다. UTC 기준으로 넓은 시간대에 매시간 실행하고, 내부에서 KST 기준 선택 시간만 통과시키면 운영 조건을 더 명확하게 표현할 수 있습니다.
오늘 날짜 글이 있으면 공개 실패 여부와 상관없이 건너뛰어야 하나요?
현재 Futory 자동 게시의 중복 방지 기준은 원본 Markdown 날짜입니다. 오늘 날짜 글이 이미 있다면 새 글을 만들지 않는 편이 안전합니다. 공개 실패를 복구하는 작업은 별도의 배포 복구 루틴으로 다루는 것이 중복 콘텐츠 생성을 막는 데 유리합니다.
랜덤 시드를 날짜로 고정하면 정말 랜덤인가요?
사용자에게 보이는 게시 시간은 날짜마다 달라질 수 있지만, 같은 날짜 안에서는 항상 같은 값이 나옵니다. 운영 자동화에서는 이런 재현 가능한 랜덤이 완전한 무작위보다 관리하기 쉽습니다.
글만 추가했는데 테마 테스트가 왜 필요한가요?
라이브 동기화는 글 파일 하나만이 아니라 프로젝트 전체 상태와 연결됩니다. 기존 다크/라이트 모드 파일이나 CSS 변수가 실수로 빠지면 콘텐츠는 정상이어도 사용자 경험이 깨집니다. 그래서 글 자동 게시에도 테마 검증을 포함하는 것이 안전합니다.
결론
Futory의 자동 게시에서 핵심은 AI가 글을 잘 쓰는 것만이 아닙니다. KST 기준 시간 게이트, 날짜 시드 기반 랜덤 선택, 오늘 날짜 중복 방지, 스테이징 검증, 호스트 검증, PM2 재시작, 공개 URL 확인이 하나의 흐름으로 이어져야 매일 한 편의 글을 안정적으로 공개할 수 있습니다.
바이브코딩은 빠르게 만드는 방식이지만, 운영에서는 빠른 실행보다 안전한 반복이 더 중요합니다. 자동화가 스스로 멈춰야 할 때 멈추고, 검증이 통과할 때만 배포하며, 공개 페이지에서 실제 결과를 확인하는 구조를 갖추면 AI와 함께 운영하는 블로그도 신뢰할 수 있는 시스템이 됩니다. 매일의 게시 작업은 작은 콘텐츠 생산이면서 동시에 자동화 품질을 점검하는 운영 실험입니다.