환경 POSIX.1-2024 crontab 규격 · Vixie cron(macOS·Debian) crontab(5) 기준 · 2026-08-01 공식 문서 확인
터미널에서 붙여넣으면 잘 도는 명령이 크론에서만 조용히 실패하는 경우가 있습니다. 대개 시간 설정이 아니라 PATH, % 문자, 작업 디렉터리 셋 중 하나입니다. POSIX 규격과 crontab 맨페이지에 적힌 내용을 기준으로 확인 순서를 정리했습니다.
크론이 넘겨주는 환경은 로그인 셸의 환경이 아니다
POSIX crontab 규격은 크론탭 항목의 명령을 실행할 때 구현이 기본 환경을 제공하며, 최소한 HOME·LOGNAME·PATH·SHELL 을 정의한다고 규정합니다. 그리고 바로 다음 문장이 이 문제의 핵심입니다. crontab 을 실행할 때의 이 변수 값들은, 예약된 명령이 실행될 때 제공되는 기본값에 영향을 주지 않는다고 못박습니다.
즉 크론탭을 편집한 셸의 PATH 는 그대로 전달되지 않습니다. 규격이 PATH 에 대해 보장하는 것은 표준 유틸리티를 모두 찾을 수 있는 검색 경로라는 점뿐입니다. Homebrew 가 쓰는 /opt/homebrew/bin, 직접 만들어 둔 /usr/local/bin, nvm 이나 pyenv 가 심어 놓은 shim 경로가 여기 포함된다는 보장은 없습니다.
그래서 node·python3·docker·gh 처럼 패키지 매니저나 버전 관리자가 깔아 준 명령이 command not found 로 떨어집니다. macOS 와 Debian 의 crontab(5) 은 SHELL 이 /bin/sh 로 설정되고 LOGNAME 과 HOME 이 계정 정보에서 채워진다고 적지만, PATH 의 기본값은 명시하지 않습니다. 구현에 따라 다르다는 뜻이므로 짐작하지 말고 실제 값을 찍어 보는 편이 빠릅니다.
# 크론이 실제로 넘기는 환경을 파일로 받아 둔다 (1분 뒤 한 번 실행됨)
* * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1
# 1분 기다린 뒤 로그인 셸의 환경과 나란히 비교한다
diff <(/usr/bin/env | sort) <(sort /tmp/cron-env.txt)
# 스크립트가 쓰는 명령들이 어디에 있는지 확인한다
command -v node python3 docker
# 크론과 비슷한 최소 환경으로 재현해 본다
env -i HOME="$HOME" SHELL=/bin/sh /bin/sh -c '/Users/me/bin/backup.sh'
PATH 한 줄만 비교해도 대부분 설명이 됩니다. 확인이 끝나면 위에 넣은 임시 크론 줄은 지웁니다.
터미널에서 되는지는 근거가 되지 않습니다. 크론이 넘기는 PATH 는 따로 확인해야 합니다.
명령줄의 % 는 셸에 닿기 전에 개행으로 바뀐다
두 번째 함정은 크론탭 파일 자체의 문법에 있습니다. crontab(5) 은 명령 부분의 퍼센트 기호가 백슬래시로 이스케이프되지 않으면 개행 문자로 바뀌고, 첫 퍼센트 뒤의 모든 데이터는 그 명령의 표준 입력으로 전달된다고 설명합니다. POSIX 규격도 같은 내용을 규정하며, 백슬래시가 앞에 붙은 문자는 문자 그대로 취급된다고 덧붙입니다.
날짜가 들어간 파일 이름을 만들 때 이 규칙에 걸립니다. date +%Y-%m-%d 를 크론탭에 그대로 적으면 명령은 date + 까지만 실행되고 나머지는 표준 입력으로 흘러갑니다. 결과 파일 이름이 중간에서 잘리거나 날짜 부분이 빈 채로 만들어지는 이유가 여기 있습니다. 터미널에서 같은 줄을 붙여넣으면 멀쩡히 도는 것도 당연합니다. 이 치환은 셸이 아니라 크론이 하기 때문입니다.
고치는 방법은 두 가지입니다. 크론탭 안에 그대로 두겠다면 퍼센트마다 백슬래시를 붙입니다. 더 나은 쪽은 명령을 스크립트 파일로 옮기고 크론탭에는 스크립트 경로만 남기는 것입니다. 스크립트 내부에는 이 치환 규칙이 적용되지 않으므로 이스케이프를 기억하지 않아도 됩니다.
# 잘못된 줄 — date + 까지만 실행되고 뒤는 표준 입력으로 넘어간다
0 3 * * * tar czf /backup/db-$(date +%Y-%m-%d).tgz /var/lib/db
# 크론탭 안에서 쓴다면 % 마다 백슬래시를 붙인다
0 3 * * * tar czf /backup/db-$(date +\%Y-\%m-\%d).tgz /var/lib/db
# 권장 — 명령은 스크립트로 옮기고 크론탭에는 경로만 남긴다
0 3 * * * /Users/me/bin/backup.sh
세 번째 방식이면 퍼센트 규칙을 신경 쓸 일이 없습니다. 스크립트 안은 평범한 셸 문법 그대로입니다.
크론탭의 명령 필드는 셸에 그대로 전달되지 않습니다. % 는 크론이 먼저 해석합니다.
작업 디렉터리는 규격에 적혀 있지 않다
세 번째는 상대 경로입니다. 스크립트가 config/settings.yml 이나 logs/today.log 같은 상대 경로를 쓰고 있으면, 어디에서 실행되느냐에 따라 결과가 달라집니다. 터미널에서는 늘 프로젝트 폴더 안에서 실행하니 드러나지 않던 문제입니다.
POSIX crontab 규격은 기본 환경으로 제공할 변수는 나열하지만, 명령이 어느 디렉터리에서 실행되는지는 규정하지 않습니다. crontab(5) 맨페이지도 HOME 이 계정 정보에서 설정된다고만 적을 뿐 작업 디렉터리를 명시하지 않습니다. 실제로는 홈 디렉터리에서 시작하는 구현이 많지만, 그건 문서에 없는 동작에 기대는 셈입니다.
그래서 스크립트 첫머리에서 작업 디렉터리를 직접 정해 두는 편이 안전합니다. 스크립트 자신의 위치를 기준으로 이동하면 어디서 호출되든 같은 결과가 나옵니다. macOS 라면 launchd 를 쓰는 선택지도 있습니다. macOS 의 crontab(5) 은 Darwin 에서 이 기능이 launchd 로 흡수됐다고 안내하고 있고, launchd.plist(5) 에는 실행 전에 이동할 디렉터리를 지정하는 WorkingDirectory 키와 환경 변수를 지정하는 EnvironmentVariables 키가 따로 있습니다.
#!/bin/bash
set -euo pipefail
# 스크립트가 놓인 위치를 기준으로 작업 디렉터리를 고정한다
cd "$(dirname "$0")"
# 필요한 경로를 앞에 직접 붙인다 (값은 command -v 로 확인한 실제 경로)
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
./node_modules/.bin/some-task
상대 경로를 계속 쓰고 싶다면 cd 를 명시하는 편이 낫습니다. 경로를 전부 절대 경로로 바꾸는 방법도 물론 됩니다.
문서에 적히지 않은 기본 동작은, 환경이 바뀔 때 가장 먼저 흔들리는 부분입니다.
다음 실패가 조용히 지나가지 않게 해두기
세 가지를 고쳐도 다음에 또 다른 이유로 실패하면 같은 자리에서 시간을 씁니다. 크론은 명령이 출력을 남기면 그 크론탭의 소유자에게 메일로 보냅니다. 문제는 요즘 서버나 개인 맥에 로컬 메일을 읽을 수단이 없는 경우가 많다는 점입니다. 그래서 정작 원인이 적힌 출력이 그대로 사라집니다.
crontab(5) 은 MAILTO 를 설명하면서, 값이 정의되어 있고 비어 있지 않으면 그 이름의 사용자에게 메일을 보내고, 정의되어 있지만 빈 문자열이면 메일을 보내지 않는다고 적습니다. 메일에 기대지 않을 생각이라면 MAILTO 를 비우고 출력을 파일로 직접 남기는 편이 확실합니다.
표준 출력과 표준 오류를 함께 받아 두면 나중에 원인을 볼 수 있습니다. 로그 파일 경로도 절대 경로로 적습니다. 여기서 상대 경로를 쓰면 앞서 본 작업 디렉터리 문제 때문에 로그부터 어디에 생겼는지 찾게 됩니다.
# 메일은 끄고, 출력은 파일로 남긴다
MAILTO=""
# 표준 출력과 표준 오류를 함께 append — 경로는 절대 경로로
0 3 * * * /Users/me/bin/backup.sh >> /Users/me/logs/backup.log 2>&1
# 다음 날 확인
tail -n 40 /Users/me/logs/backup.log
2>&1 을 빼면 정작 필요한 오류 메시지가 로그에 남지 않습니다.
출력을 어디에도 남기지 않은 크론 작업은, 실패했는지조차 알 수 없습니다.
조치 후 확인할 것
- 크론이 넘기는 PATH 를 env 덤프로 실제로 확인했는지 봅니다. 짐작한 값과 다른 경우가 많습니다.
- 크론탭의 명령 필드에 이스케이프하지 않은 % 가 남아 있지 않은지 확인합니다. 남았다면 백슬래시를 붙이거나 스크립트로 옮깁니다.
- 홈 디렉터리로 이동한 뒤 스크립트를 절대 경로로 직접 실행해 봅니다. 여기서 실패하면 크론이 아니라 경로 문제입니다.
- 로그 파일에 실행 흔적이 남는지 다음 실행 후에 확인합니다. 파일이 아예 만들어지지 않았다면 크론 줄 자체가 돌지 않은 것입니다.
확인한 문서
- POSIX.1-2024 crontab 규격 (기본 환경 변수 · % 치환) (2026-08-01 확인)
- crontab(5) — Linux man-pages (SHELL·LOGNAME·HOME, MAILTO, % 이스케이프) (2026-08-01 확인)
- crontab(5) — Debian manpages (Vixie cron 기준 동일 항목 확인) (2026-08-01 확인)
정리하면 확인 순서는 셋입니다. 크론이 넘기는 PATH 를 눈으로 보고, 명령 필드의 % 를 처리하고, 작업 디렉터리를 스크립트 안에서 고정합니다. 그 다음에 출력을 파일로 남겨 두면, 다음 실패는 로그 한 번 보는 일로 끝납니다.
'복사금지 블로그 짜증나서 만든 개발문서' 카테고리의 다른 글
| docker stop이 10초씩 걸리고 종료 코드 137이 뜰 때 확인하는 순서 (0) | 2026.08.03 |
|---|---|
| rm 했는데 디스크 용량이 안 줄어들 때 확인하는 순서 (0) | 2026.08.02 |
| npm ci가 CI에서만 실패한다면 package-lock.json부터 보세요 (0) | 2026.07.31 |
| gitignore에 추가했는데 파일이 계속 올라온다면 확인할 것 (0) | 2026.07.31 |
| git pull --rebase --autostash 충돌 복구, 오늘 다시 확인한 안전한 순서 (0) | 2026.07.30 |