오전 9시 워크플로우가 새벽 1시에 실행된 이유: 프로덕션 환경에서만 발생하는 시간대 버그
요약
n8n에서 LLM 기반 워크플로우를 프로덕션 환경에 배포할 때 시간대(Timezone) 오류가 발생하기 쉽습니다. 개발 환경과 달리, 컨테이너화된 프로덕션 환경은 UTC 기준의 서버 시간을 사용해 의도치 않은 시간에 작업이 실행될 수 있습니다. 이를 해결하려면 `GENERIC_TIMEZONE` 환경 변수를 명시적으로 설정하여 시간대를 고정하고 워크플로우를 검증해야 합니다.
핵심 포인트
- 프로덕션 n8n은 UTC 기반의 서버 시간을 사용하기 쉬워 시간대 오류가 발생합니다.
- 시간대 문제는 Schedule Trigger와 Date & Time 노드 모두에 영향을 줍니다.
- `GENERIC_TIMEZONE` 환경 변수를 설정하여 워크플로우의 기준 시간대를 명확히 고정해야 합니다.
- 단순한 스케줄링 오류는 데이터 자체의 오래된 정보(stale data)를 가져오는 문제로 이어질 수 있습니다.
9시에 실행되기로 한 워크플로우가 새벽 1시에 작동한 이유
이 글은 n8n에서 LLM(대규모 언어 모델) 워크플로우를 프로덕션 환경에서 운영하는 과정 중 '빌드-인-퍼블릭' 시리즈의 일부입니다. 여기에 담긴 모든 내용은 제가 판매하고 지원하는 실제 템플릿을 기반으로 합니다. 실수한 부분은 저의 것이고, 수정된 부분은 테스트를 거쳤습니다.
아무도 요청하지 않았던 새벽 1시 이메일 발송
두 달 전, 저는 일일 요약본(daily digest) 워크플로우를 배포했습니다. 매일 아침 9시에 실행되어 전날의 핵심 지표들을 가져오고, LLM에게 짧은 요약을 작성하도록 요청한 다음, 이를 클라이언트에게 이메일로 발송하는 방식이었습니다. 간단했고, 몇 주 동안 완벽하게 작동했습니다.
그러던 어느 날 클라이언트로부터 연락이 왔습니다: "왜 어제 요약본을 새벽 1시에 받았나요?"
저는 아무것도 변경하지 않았습니다. Schedule Trigger는 9:00으로 설정되어 있었고, 실행 목록(executions list)에도 9:00이라고 표시되었습니다. 그런데도 이메일은 현지 시간으로 새벽 1시에 도착했습니다. 제 클라이언트는 새벽 1시에 잠에서 깨어나 '좋은 아침' 요약본을 읽고 있었습니다. 정말 최악의 순간이었습니다.
당신이 놓치고 있는 두 개의 시계
모든 스케줄링된 n8n 워크플로우에는 (최소한) 두 가지 시간대가 작용하며, 대부분의 사람들은 그중 하나만 생각합니다:
1. 서버 시간대(The server timezone). n8n은 Schedule Trigger cron 표현식(cron expressions)을 실행되는 기계(또는 컨테이너)의 시간대를 기준으로 평가합니다. 개인 노트북에서는 그것이 사용자의 현지 시간대입니다. 따라서 9:00은 사용자 시간으로 9:00을 의미하며, 모든 것이 정상적으로 작동합니다. 하지만 프로덕션 환경에서 n8n은 TZ=UTC가 설정된 Docker 컨테이너에서 실행될 가능성이 매우 높습니다. 같은 cron 표현식, 같은 트리거임에도 불구하고 이제 9:00은 UTC 기준 9:00을 의미하며, 이는 제 클라이언트(UTC+8)에게는 새벽 1시였습니다.
2. 워크플로우 자체의 가정(The workflow's own assumptions). 워크플로우 내부에서 $now와 Date & Time 노드는 서버 시계를 사용합니다. 저의 요약본은 실행 시간에 계산된 날짜를 사용하여 "어제 지표"를 가져왔습니다. 이 워크플로우가 아침 9시가 아닌 현지 시간 새벽 1시에 실행되었을 때, "어제"는 서버 시계상으로는 여전히 기술적으로 정확했지만, 상위의 지표 집계(metrics job upstream) 작업이 자체적인 일일 요약 작업을 아직 완료하지 못했기 때문에, 요약본은 오래된 데이터(stale data)를 기반으로 만들어졌습니다. 시간도 틀리고, 데이터도 틀린 것입니다. 단지 하나의 시간대 오류에서 비롯되었습니다.
이것이 버그가 프로덕션 환경에서만 발생하는 이유입니다. 개발 환경(사용자의 노트북, 로컬 시간대)과 프로덕션 환경(컨테이너, UTC)이 조용히 의견 불일치를 일으키고, n8n의 어떤 경고도 알려주지 않습니다.
해결책: 시간대를 고정하고 확인하기
해결책은 두 부분으로 나뉩니다. 설정과 검증 습관입니다.
파트 1 — GENERIC_TIMEZONE 고정. n8n에는 GENERIC_TIMEZONE이라는 환경 변수가 있는데, 이것이 Schedule Trigger와 Date & Time 노드의 시간대를 제어합니다. 이 변수가 설정되어 있지 않으면, n8n은 서버의 시간대로 폴백(fallback)합니다. 저는 이제 모든 배포에서 이를 명시적으로 설정합니다:
GENERIC_TIMEZONE=Asia/Shanghai
단 한 줄입니다. Schedule Trigger는 컨테이너가 시간을 무엇이라고 생각하든 상관없이 해당 시간대 기준으로 cron을 평가합니다. (만약 여러 지역의 클라이언트를 대상으로 한다면, 해당 워크플로우의 _청중(audience)_에 맞는 시간대로 고정하세요. 하나의 워크플로우는 하나의 평가 시간대만 가질 수 있으므로, 다중 청중 스케줄은 별도의 트리거를 가져야 합니다.)
파트 2 — 카나리 실행으로 검증하기. 저는 이제 설정만으로는 믿지 않습니다. 제가 배포하는 모든 예약된 워크플로우는 두 개의 노드로 구성된 가드(guard)로 시작합니다:
Schedule Trigger → Set (log execution_time: {{ $now.toFormat('yyyy-MM-dd HH:mm z') }})
첫 실제 실행은 실제 로컬 시간을 실행 로그에 기록합니다. 만약 제가 기대하는 내용이 아니라면, 1시에 클라이언트로부터 듣기 전에 첫날에 문제를 포착할 수 있습니다. 추가하는 데는 30초밖에 걸리지 않았고, 그 덕분에 벌써 두 번이나 저를 구했습니다.
일광 절약 시간제(DST) 보너스 라운드
만약 고정된 시간대가 일광 절약 시간제를 관찰한다면, cron 표현식은 전환일에 이중으로 실행되거나 아예 건너뛸 수 있습니다 (오전 2시 30분이 두 번 발생하거나, 혹은 한 번도 발생하지 않을 수 있습니다). 하루에 정확히 한 번 실행되어야 하는 어떤 것이든 간에, 저는 DST를 관찰하는 시간대에서 오전 1시~3시 사이의 시간대는 피하고, 워크플로우 내부에서 명시적인 변환을 사용하는 UTC 기반 스케줄링을 선호합니다. 지루하지만,
GENERIC_TIMEZONE은 배포 시 명시적으로 설정되며 (컨테이너로부터 상속되지 않음)- 크론 시간(cron time)은 내 시간이 아닌 '청중(audience)'의 시간대 기준으로 표현됩니다.
- 워크플로우의 첫 번째 노드는 시간대와 함께 실제 실행 시간을 기록하며 (
$now에 형식 +z), 저는 첫 실제 실행을 확인합니다. - "어제/오늘/이번 주" 날짜 계산은 서버 기본값(server default)이 아닌 고정된 시간대(pinned timezone)를 사용합니다.
- DST(일광 절약 시간제)를 준수하는 시간대에서는 오전 1시부터 3시 사이에는 일일 트리거가 예약되지 않습니다.
시간대는 클라이언트가 새벽 1시에 아침 요약(morning digest)을 읽는 순간까지는 "명백히 괜찮은" 것 중 하나입니다. 고정하고, 기록하며, 첫 실행을 확인하세요.
저는 AI Automation Lab을 구축하고 있습니다—프로덕션 레디한 n8n 워크플로우 템플릿입니다. 무료 AI Hook Generator는 제가 실제 워크플로우를 어떻게 구성하는지 보여주며, 저의 오픈 소스 템플릿은 github.com/zhp910318/n8n-ai-automation-lab에서 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기