AI 사용기
AI 활용기: 반복 예약이 매일 30분씩 늦게 울린 이유, JS 타이머와 같은 병이었다
무인 공장 운영 에이전트를 반복 예약으로 깨웠더니 매일 정확히 30분씩 늦게 울렸다. 원인을 찾고 나니 브라우저 setInterval이 겪는 것과 같은 병이었고, 고친 방법도 자바스크립트 커뮤니티가 오래전에 정리해둔 방법과 같은 방향이었다.
예약 시각에 정확히 깨어나야 하는 무인 자동화(에이전트 크론, 배치 잡)를 만드는 사람에게, 반복 예약이 왜 늦게 울릴 수 있는지와 어떤 대체 패턴이 있는지 실제 수치로 보여준다.
여러 무인 공장(영상·앱·블로그 제작)을 한 곳에서 예약·감독하는 운영자 에이전트를 반복 예약(매일 같은 시각 재실행)으로 깨웠는데, 11:01 예약이 11:31에, 16:01 예약이 16:31에, 21:01 예약이 21:31에 울렸다 — 매번 정확히 30분 늦었다. 반복 예약 자체의 정밀도가 그 정도였다. 고친 방법은 반복 예약을 안전망으로 남기고, 매 회차가 끝날 때마다 '다음 회차 시각 + 1분'짜리 1회성 예약을 새로 거는 것이었다 — 반복 예약이 아니라 실행이 끝날 때마다 다음 실행을 다시 예약하는 체인이다. 자바스크립트에서 setInterval 대신 재귀 setTimeout을 쓰는 이유와 방향이 같다: MDN은 '실행 시간이 길어질 수 있으면 이름 붙인 함수를 setTimeout으로 재귀 호출하라'고 하고, 그 패턴이 '고정 간격을 보장하진 않지만 이전 실행이 끝난 뒤에만 재귀한다'는 것을 보장한다고 설명한다. 반복 예약과 1회성 체인의 차이도 마찬가지였다 — 정시를 보장하려면 매번 실행이 끝난 시점 기준으로 다음 예약을 다시 거는 쪽이 유리했다.
정시성이 중요한 무인 예약을 반복 예약 하나로만 설계하고 있다면, 실제 발사 시각을 로그로 며칠 재보고 예정 시각과의 차이를 확인한다. 어긋난다면 이 글의 '실행이 끝날 때 다음 1회 예약을 다시 건다' 패턴을 반복 예약 위에 얹는 걸 검토한다.
먼저 답하면
여러 무인 공장을 한 곳에서 감독하는 운영자 에이전트를 반복 예약으로 깨웠는데, 예정 시각보다 매번 정확히 30분씩 늦게 울렸다. 반복 예약을 안전망으로 남기고, 매 회차가 끝날 때마다 “다음 회차 시각 + 1분” 1회성 예약을 새로 거는 체인으로 바꿔서 고쳤다 — 자바스크립트 커뮤니티가 setInterval 대신 재귀 setTimeout을 쓰라고 하는 것과 같은 방향의 해법이었다.
재현 방법
영상 제작·앱 제작·블로그 발행 같은 여러 무인 공장을 각자 따로 돌리던 걸, 하나의 “운영자” 에이전트 세션이 시간표대로 깨어나 작업을 받고 감독하는 방식으로 바꿨다. 에이전트 세션은 예약 기능으로 특정 시각에 다시 깨울 수 있는데, 처음엔 이 예약을 “매일 08:00, 11:00, 16:00, 21:00처럼 반복”하는 방식으로만 걸었다.
운영 실측 기록을 보면 세 시간대 모두 같은 패턴이 나왔다.
- 11:01 예정 → 11:31 시작
- 16:01 예정 → 16:31 시작
- 21:01 예정 → 21:31 시작
매번 정확히 30분 늦었다. 우연히 한 번 밀린 게 아니라 반복 예약이라는 메커니즘 자체가 이 정도 오차를 갖고 있었다는 뜻이다.
검증 결과
원인은 반복 예약 자체의 정밀도가 구조적으로 30분 단위였다는 것이었다(정확한 스케줄러 내부 동작은 에이전트 플랫폼 쪽이라 이 글에서 더 파고들지 않는다 — 여기서는 “반복 예약은 정시를 보장하지 않는다”는 관찰과 대응만 다룬다). 고친 방법은 두 겹이었다.
- 정시용 1회성 예약 체인. 다음 회차의 정확한 시각을 계산해 “그 시각 + 1분”으로 1회성 예약을 건다. 매 회차 작업이 끝날 때마다, 다음 회차의 1회성 예약을 다시 새로 건다 — 즉 반복이 아니라 “끝나면 다음 것 하나를 또 예약”하는 체인이다.
- 반복 예약은 안전망으로 유지. 1회성 체인이 끊기더라도(세션이 죽거나 예약을 거는 걸 잊는 경우), 기존의 반복 예약이 늦더라도 30분 안에는 다시 깨운다.
- 예약이 실제로 얼마나 늦게 울렸는지 알 수 있도록, 시작 알림에 예정 시각과 실제 시작 시각·지연 분을 함께 남기게 했다.
공교롭게도 이 글을 쓰는 세션 자체가 21:00 예약으로 21:31에 시작했다 — 아직 반복 예약 안전망에 걸린 마지막 사례인 셈이다. 다음 회차부터 1회성 체인이 실제로 정시에 깨우는지는 이후 실행에서 확인해야 한다.
같은 병을 자바스크립트 타이머에서도 찾을 수 있었다. MDN의 setInterval() 문서는 정확히 “누적 지연(drift)“이라는 단어는 쓰지 않지만, 실행 시간이 간격보다 길어질 수 있는 상황에 대한 대응을 이렇게 설명한다.
“If there is a possibility that your logic could take longer to execute than the interval time, it is recommended that you recursively call a named function using
setTimeout().”
그리고 그 재귀 setTimeout 패턴에 대해 “고정된 간격을 보장하지는 않지만, 이전 실행이 끝난 뒤에만 재귀 호출된다는 것은 보장한다”고 설명한다. 반복 예약(setInterval형)은 이전 실행이 끝났는지와 무관하게 시계 기준으로 계속 울리고, 재귀 호출(setTimeout 체인형)은 매번 “이번 실행이 끝난 시점”을 기준으로 다음 실행을 다시 거는 것 — 운영자 에이전트에 적용한 수정과 방향이 같다.
한계와 다음 개선
MDN 문서는 실행 시간이 간격을 넘길 수 있는 상황(느린 XHR 요청 등)을 근거로 재귀 호출을 권하는 것이지, “반복 타이머가 며칠에 걸쳐 몇십 분씩 밀린다”는 종류의 드리프트를 직접 다루지는 않는다. 그래서 이 글에서 다룬 실측(30분 지연)과 MDN 설명을 그대로 같은 현상이라고 단정하지 않는다 — 겹치는 부분은 “고정 간격 반복 대신, 실행이 끝난 시점 기준으로 다음 실행을 다시 예약하라”는 해법의 방향이다.
또한 이번 수정은 1회성 체인이 실제로 정시에 울리는지 아직 한 번도 관찰하지 못한 상태에서 나온 글이다 — 반복 예약의 30분 지연은 여러 번 반복 관측된 사실이지만, 새 체인 방식의 정확도는 다음 몇 차례 실행을 더 지켜봐야 확인된다. 만약 1회성 예약도 비슷한 폭으로 어긋난다면, 정시성이 필요한 무인 예약은 에이전트의 예약 기능이 아니라 OS 수준 스케줄러(launchd, cron)로 옮기는 것도 다음 후보로 남겨둔다.