본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 16. 16:44

【개인 개발】 AI 뉴스 bot의 X 자동 게시, '매시 실행'을 그만두고 JST 피크 시간대로 집중했더니 보인 것들

요약

AI 뉴스 자동 게시 봇의 운영 효율을 높이기 위해 매시 실행 방식에서 JST 피크 시간대 집중 방식으로 전환한 경험을 다룹니다. Vercel Cron의 UTC 기준 설정 주의점과 시간대 변환 시 발생하는 기술적 함정을 코드와 함께 설명합니다.

핵심 포인트

  • 단순 노출 횟수 증가보다 사용자 활동 시간대(Peak Time)에 맞춘 게시가 효과적임
  • Vercel Cron 설정 시 UTC와 JST 간의 9시간 시차 계산 주의 필요
  • 서버리스 환경(Vercel Functions)의 서버 시각은 UTC로 동작함을 인지해야 함
  • 과도한 게시 빈도는 스팸 판정 및 팔로우 취소 리스크를 초래할 수 있음

개인적으로 「AI 뉴스를 일본어로 번역하여 X(구 Twitter)에 자동으로 게시하는 bot」을 운영하고 있습니다. RSS를 통해 18개 이상의 AI 계열 피드(OpenAI / Anthropic / Google AI 등)를 수집하여, 번역한 뒤, Vercel Cron으로 정기 게시하는 구성입니다.

이 bot에는 오랫동안 **「매시 실행 (0 * * * *)」**이라는 투박한 cron 설정이 적용되어 있었습니다. 하루 24회, 심야 3시든 상관없이 게시합니다. 코드는 작동했고 에러도 발생하지 않았습니다. 하지만 숫자는 늘지 않았습니다.

이 기사는 그 매시 실행을 그만두고 JST(일본 표준시)의 피크 시간대(7:00 / 12:00 / 18:00 / 20:00)에만 게시를 집중시키는 변경(실제 PR: Issue #20)을 주제로,

  • cron의 UTC/JST 변환에서 빠지기 쉬운 함정
  • 「게시 수를 시간대에 따라 동적으로 변경하는」 구현의 구체적인 코드
  • 그리고
    게시 최적화를 "횟수의 최대화"라고 생각했던 자신이 무엇을 잘못 이해하고 있었는지

를, 동작하는 코드와 솔직한 반성 내용을 담아 작성합니다.

대상 독자: Vercel/서버리스(Serverless) 환경에서 정기 실행(cron)을 한 번이라도 다뤄본 적이 있으며, 「일단 돌아가게만 만든 정기 게시」를 한 단계 업그레이드하고 싶은 개인 개발자 및 중급자. cron 표기법(* * * * *)을 어렴풋이 읽을 수 있는 사람을 상정하고 있습니다.

첫 번째 구현은 다음과 같았습니다.

// vercel.json (변경 전)
{
"crons": [
...

0 * * * *는 「매시 0분」을 의미합니다. 즉, 하루 24회 기동합니다. 얼핏 보면 노출을 많이 할 수 있어 좋아 보입니다.

매시 실행 그대로 두면 되지 않을까? 노출은 많을수록 이득 아닌가?

처음의 저도 그렇게 생각했습니다. 하지만 운영해 보니 두 가지 문제가 보였습니다.

  • 심야 시간대의 게시가 "헛발질"이 된다. 일본 사용자들이 잠든 UTC 기준의 한밤중(JST의 낮 시간과 어긋남)에 게시해도, 누구의 타임라인에도 남지 않고 흘러가 버립니다.
  • 게시 빈도가 너무 높으면 스팸 판정 리스크가 있다. 팔로워의 타임라인(TL)을 자신의 게시물로 가득 채우면, 오히려 팔로우 취소를 초래합니다.

즉, 문제는 「노출이 부족한 것」이 아니라 **「침묵해야 할 시간에도 계속 떠들고 있다」**는 것이었습니다. 이 부분이 이번의 핵심입니다.

Vercel Cron의 스케줄은 UTC 기준으로 작성합니다. 이 부분이 첫 번째 함정입니다. 일본 시간(JST)은 UTC+9이므로, JST의 게시하고 싶은 시각에서 9시간을 뺀 값을 cron에 작성해야 합니다.

시간대JSTUTC (cron에 작성할 값)이유
🌅 아침07:0022:00 (전날)출근·기상 직후의 스마트폰 체크
...

실제 차이(vercel.json)는 다음과 같아졌습니다.

{
"path": "/api/cron/auto-post",
- "schedule": "0 * * * *"
...

하루 24회 → 하루 4회. 횟수를 「6분의 1」로 줄이는 변경입니다.

게시 횟수를 6분의 1로 줄여서 정말 성장할 수 있을까? 오히려 노출이 줄어드는 것 아닌가?

이 부분이 직관에 반하는 지점입니다. 후술하겠지만, 노출의 총량보다 「누가 보고 있는 시간에 내보내는가」가 더 효과적이다라는 것이 이번의 도박이었습니다.

cron을 UTC로 작성하는 것은 익숙해지면 되지만, 애플리케이션 코드 내에서 시간대 판정을 할 때도 동일한 함정이 있습니다. Vercel Functions의 서버 시각은 UTC로 동작하기 때문에, new Date().getHours()를 그대로 사용하면 JST인 줄 알았는데 UTC로 판정되어 9시간 차이가 발생합니다.

그래서 JST의 「시(hour)」를 명시적으로 계산하는 헬퍼(helper)를 사용하고 있습니다.

/** JST (일본 표준시) 기준 현재의 「시」를 취득 */
function getJSTHour(): number {
const now = new Date();
...

포인트는 마지막의 getUTCHours()입니다. getTime()에 9시간 분량의 밀리초를 더한 「보정된 시각」에 대해, 다시 로컬 타임존 보정이 적용되는 getHours()를 사용하면 이중 보정이 됩니다. 보정한 후에는 반드시 getUTCHours()로 읽는다고 기억해 두면 사고를 방지할 수 있습니다.

cron으로 「언제 동작할지」를 좁혔다면, 다음은 「1회당 몇 건을 게시할지」도 시간대에 따라 바꾸고 싶어집니다. 피크 시간대는 두텁게, 준 피크 시간대는 중간 정도로, 만약 오프 피크(off-peak)에 동작하더라도 얇게.

구현은 심플한 분기문입니다.

/**
* 시간대에 기반하여 최대 게시 수를 동적으로 결정한다.
* 피크 타임은 많이, 오프 피크는 억제한다.
...

대략 말하자면 "사람이 있는 시간은 5건, 없는 시간은 2건"이라는 강약 조절입니다. 환경 변수 AUTO_POST_MAX_PER_RUN이 설정되어 있다면 그 값을 우선한다는 퇴로를 첫 번째 줄에 남겨둔 것도 은근히 중요한데, 검증 시에 "지금만 모든 시간대에 10건씩 흘려보내고 싶다"와 같은 덮어쓰기가 가능하기 때문입니다.

나아가, 게시할 기사는 적당히 고르는 것이 아니라 **소스 우선순위 (Source Priority)**로 정렬한 뒤 상위 N건만 출력합니다.

export const SOURCE_PRIORITY: Record<string, number> = {
"OpenAI Blog": 10,
"Anthropic News": 10,
...

"사람이 있는 시간에, 가치 높은 소스로부터 내보낸다" —— 시간과 내용 모두 피크 타임에 자원을 집중하는 설계입니다.

PR의 목표는 인게이지먼트 (Engagement) 비율 20~40% 향상이었습니다. 다만 —— 이 부분은 솔직하게 쓰겠습니다 ——

이 20~40%는 "목표/가설"이며, 아직 A/B 테스트로 엄밀하게 검증된 확정치는 아닙니다.

이유는 두 가지입니다. 첫째, 변경 전후로 "게시 시간"과 "게시 수"를 동시에 움직였기 때문에 효과의 분리(시간대 덕분인지, 건수를 줄인 덕분인지)가 되지 않았습니다. 둘째, X의 개인 계정 규모에서는 임프레션 (Impression)의 분산이 커서 며칠 만으로는 유의미한 차이라고 단정 지을 수 없습니다.

따라서 현시점에서 자신 있게 말할 수 있는 것은, **"정량적 효과는 검증 중. 다만 오프 피크의 무의미한 게시가 제로가 된 것은 확실함"**이라는 점까지입니다. 과장하지 않고 여기서 멈추겠습니다.

검증 환경 (2026년 6월 시점):

  • 런타임: Node.js v20.12.2 / Vercel Functions
  • 라이브러리: twitter-api-v2@^1.29.0
  • 플랜: Vercel Pro (후술할 이유로 필수)

장점만 쓰면 거짓말 같으니, 현실적인 이야기를 3가지 하겠습니다.

1. Vercel Pro (유료)가 거의 필수적이 되었다.

1회 실행 시 "최대 5건 × 10초 간격"이라면 처리에 최대 100초 정도 걸립니다. Function의 기본 타임아웃인 10초로는 전혀 부족하여 maxDuration: 300이 필요합니다. 이는 무료 Hobby 플랜의 상한(10초)을 초과하기 때문에 Pro 플랜이 전제되어야 합니다. "개인 개발에서 월정액 비용이 발생한다"는 점은 솔직히 말해야 할 단점입니다.

2. 문서와 코드가 어긋나 있었다 (자책).

리포지토리 내의 오래된 설계 메모에는 "하루 3회 (08:00/12:30/20:00)"라고 적혀 있는데, 실제 cron은 "4회 (7/12/18/20)"입니다. 스케줄이라는 "프로덕트의 의지"가 코드와 문서에서 이중 관리되고 있었고, 한쪽이 부패해 있었습니다. cron 값은 코드인 동시에 운영 문서이기도 하다는 것을 뼈저리게 느낀 부분입니다.

3. cron의 기동 시각과 "게시 수 테이블"이 완전히 맞물리지 않는다.

getMaxPostsForCurrentTime()은 "18~21시는 피크로 5건"이라고 판정하지만, cron이 저녁에 기동하는 것은 18:00 정각에 딱 한 번뿐입니다. 테이블 쪽은 더 세밀한 시간대를 상정하여 만들어 두었기에, 현재는 구현의 여력이 앞서 있는 상태입니다. 향후 cron을 늘렸을 때 효과를 발휘할 설계라고 긍정적으로 받아들이고 있지만, "지금 이 순간 풀(full)로 사용되고 있지 않다"는 것은 사실입니다.

이 변경을 통해 내 안의 정의가 하나 바뀌었습니다.

게시의 최적화란 "횟수를 최대화하는 것"이 아니라, "언제 침묵할지를 결정하는 것"이다.

매시 실행을 그만두는 것은 기능을 추가하는 변경이 아니라 기능을 빼는 변경입니다. 코드는 오히려 복잡해졌지만 (시간대 판정이 늘어남), bot의 동작은 "조용해졌다". 이 비대칭성이 이번에 가장 흥미로웠던 점이었습니다.

배운 점 3줄 요약:

  • cron은 UTC로 작성한다. JST는 "원하는 시각 - 9시간". 앱 내부 판정에서는 getUTCHours()로 이중 보정을 피한다.
  • 시간도 내용도 피크 타임에 집중한다. 기동 시각을 좁히고, 건수를 동적으로 만들며, 우선순위가 높은 소스로부터 내보낸다.
  • 줄이는 최적화를 두려워하지 않는다. 노출의 총량보다 "누가 보고 있는 시간에 내보낼 것인가". 침묵의 설계는 비용이 아니라 전략이다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0