Vercel에서 node-cron과 setInterval이 안 도는 이유
Next.js 앱을 Vercel에 올려두고 “매일 아침에 메일 보내기” 같은 걸 붙이려다 이런 코드를 짜본 적이 있으실 겁니다.
import cron from 'node-cron';
cron.schedule('0 9 * * *', () => {
sendDailyReport();
});
로컬에서는 완벽하게 돕니다. 배포하면 아무 일도 일어나지 않습니다. 에러도 안 납니다. 그냥 조용히 실행되지 않습니다.
왜 안 되나
버그가 아니라 구조입니다.
node-cron이나 setInterval은 “계속 살아 있으면서 시계를 세는 프로세스”를 전제합니다. 9시가 될 때까지 누군가는 깨어서 기다려야 하니까요.
서버리스에는 그 프로세스가 없습니다. 요청이 들어오면 함수 인스턴스가 뜨고, 응답을 돌려주면 종료됩니다. 실행 시간이 끝나면 그 안에 등록해둔 타이머도 인스턴스와 함께 사라집니다. 9시까지 기다려줄 주체가 존재하지 않는 겁니다.
setInterval도 같습니다. 등록은 성공하지만, 함수가 종료되는 순간 같이 없어집니다.
정리하면 이렇습니다.
| 상주 서버 | 서버리스 | |
|---|---|---|
| 프로세스 수명 | 계속 살아 있음 | 요청 처리 후 종료 |
setInterval | 유지됨 | 함수와 함께 소멸 |
node-cron | 정상 동작 | 등록만 되고 실행 안 됨 |
| 웹소켓 유지 | 가능 | 불가 |
흔한 오해: “요청이 오면 깨어나니까 괜찮지 않나”
깨어나긴 합니다. 문제는 누가 깨우느냐입니다.
스케줄러의 본질은 “아무도 부르지 않아도 정해진 시각에 스스로 실행되는 것”입니다. 그런데 서버리스는 외부에서 요청이 와야만 실행됩니다. 즉 깨워줄 외부 주체가 반드시 따로 필요합니다. 그게 바로 크론 서비스가 하는 일이고, 그래서 node-cron을 코드에 넣는 것으로는 해결이 안 됩니다.
선택지 세 가지
1. Vercel Cron Jobs
Vercel이 공식으로 제공하는 방법입니다. vercel.json에 스케줄을 적어두면 Vercel이 정해진 시각에 지정한 경로를 호출해줍니다.
{
"crons": [{ "path": "/api/daily-report", "schedule": "0 0 * * *" }]
}
앱을 옮기지 않아도 되니 가장 자연스러운 답입니다. 다만 제약이 있습니다.
- 실행 빈도가 요금제에 묶여 있습니다. 무료 플랜은 촘촘한 주기를 쓸 수 없습니다. 정확한 한도는 Vercel 요금제 문서에서 현재 값을 확인하세요 — 정책이 바뀌는 항목이라 여기 숫자를 적어두면 금방 틀립니다.
- 여전히 함수 실행 시간 제한을 받습니다. 오래 걸리는 배치 작업에는 맞지 않습니다.
- 상주가 필요한 일은 여전히 불가능합니다. 웹소켓을 붙들거나, 큐를 계속 소비하거나, 메모리에 상태를 유지하는 작업은 해결되지 않습니다.
몇 시간에 한 번 짧게 도는 작업이라면 이걸로 충분합니다. 여기서 끝나는 경우가 실제로 많으니 먼저 검토해보세요.
2. 외부 크론 서비스로 API 호출
cron-job.org 같은 외부 서비스가 주기적으로 여러분의 API를 호출하게 두는 방식입니다. Vercel Cron과 원리는 같고, 빈도 제약을 우회할 수 있습니다.
주의할 점은 그 엔드포인트가 인터넷에 열려 있다는 것입니다. 누구나 호출할 수 있으면 정산이나 메일 발송이 아무 때나 돌 수 있으니, 비밀 토큰을 헤더나 쿼리로 확인하는 절차를 반드시 넣으세요.
3. 크론 워커만 따로 분리
프론트엔드는 Vercel에 그대로 두고, 상주가 필요한 부분만 별도의 작은 서버로 빼는 구성입니다.
[ Next.js — Vercel ] [ 크론 워커 — 상주 서버 ]
사용자 화면 node-cron 이 시각을 셈
API 라우트 DB 작업, 메일 발송, 크롤링
└──── 같은 DB ────┘
이 방법의 장점은 기존 스택을 하나도 안 버려도 된다는 겁니다. Vercel의 배포 경험과 CDN은 그대로 쓰고, 서버리스가 구조적으로 못 하는 일만 떼어냅니다.
워커는 화면을 그리지 않으니 자원도 거의 안 씁니다. 크론 몇 개 도는 워커라면 메모리 300MB로 충분하고, 인바운드 트래픽도 사실상 0입니다.
워커를 분리할 때 반드시 챙길 것: 시간대
이건 실제로 많이들 당하는 부분입니다.
대부분의 서버는 시스템 시간이 UTC입니다. 그래서 '0 9 * * *'라고 적으면 한국 시간 오전 9시가 아니라 오후 6시에 돕니다.
node-cron은 시간대 옵션을 지원하니 명시적으로 지정하세요.
import cron from 'node-cron';
cron.schedule(
'0 9 * * *',
() => {
sendDailyReport();
},
{ timezone: 'Asia/Seoul' },
);
이렇게 두면 서버 시간이 UTC든 무엇이든 항상 한국 시간 9시에 돕니다. 서버를 옮겨도 코드가 그대로 동작한다는 점에서, 시간대를 코드에 박아두는 편이 서버 시간을 KST로 바꾸는 것보다 안전합니다.
정리
node-cron·setInterval이 Vercel에서 안 도는 건 버그가 아니라 서버리스의 정의입니다- 몇 시간에 한 번이면 Vercel Cron Jobs로 충분합니다. 먼저 이걸 보세요
- 촘촘한 주기나 상주가 필요하면 워커만 분리하세요. 프론트는 옮길 필요 없습니다
- 워커를 만들었다면 시간대를 반드시 명시하세요
참고로 이 글을 쓴 곳은 이런 워커를 올려두기 위한 Node.js 호스팅을 운영합니다. 슬립이 없어서 node-cron이 계속 시각을 셉니다.
다만 서버가 유럽에 있고, 자동 백업이 없으며, 장애 보상을 약속하지 않습니다. 크론 워커는 지연이 문제되지 않는 워크로드라 잘 맞지만, 정확한 시각 실행을 보장한다고는 말씀드리지 않겠습니다 — 저희도 SLA가 없습니다. 분 단위 정확도가 사업에 중요하다면 관리형 스케줄러를 쓰시는 편이 맞습니다.