자동화 스크립트를 하나 만들 때마다 작업 스케줄러 창을 열고, “작업 만들기”를 누르고, 이름 적고, 트리거 탭으로 가서 시각을 정하고, 동작 탭에서 실행 파일과 인수와 시작 위치를 채우고, 조건 탭에서 전원 옵션을 끄고… 이 과정이 마우스 클릭 스무 번쯤 됩니다. 한 번이면 참을 만한데, PC를 새로 세팅하거나 스크립트 경로가 바뀌면 처음부터 다시입니다. 무엇보다 어떻게 등록했는지가 아무 데도 남지 않습니다. 이 글에서는 작업 스케줄러 등록 스크립트를 만들어, 명령 한 줄 또는 파일 하나로 같은 작업을 몇 번이든 똑같이 재현하는 방법을 정리합니다.
GUI로 등록하면 무엇이 문제인가
작업 스케줄러 창에서 손으로 등록하는 방식에는 세 가지 구조적인 약점이 있습니다.
- 재현이 안 됩니다. 노트북과 데스크톱에 같은 작업을 걸어두면, 두 곳의 설정이 미묘하게 달라도 알아챌 방법이 없습니다.
- 기록이 남지 않습니다. 6개월 뒤에 “이 작업 왜 새벽 3시로 해놨더라”를 떠올릴 단서가 없습니다. 스크립트로 만들어 두면 그 자체가 문서이자 주석입니다.
- 검증을 건너뛰게 됩니다. 경로를 한 글자 틀려도 등록 자체는 성공합니다. 실패는 다음 날 새벽에 조용히 일어나고, 스케줄러 목록에는 그냥 “준비”라고만 찍혀 있습니다.
반대로 등록 과정을 스크립트로 적어두면 이 세 가지가 한 번에 해결됩니다. 깃 저장소에 넣어두고, 새 PC에서는 그 파일 하나만 실행하면 끝입니다.
가장 빠른 방법: schtasks 한 줄
윈도우에는 schtasks라는 내장 명령이 있습니다. 설치할 것도, 모듈을 불러올 것도 없습니다. 매일 오전 9시에 파이썬 스크립트를 돌리는 작업은 이렇게 등록합니다.
schtasks /Create ^
/TN "daily-backup" ^
/TR "\"C:\Python312\python.exe\" \"D:\scripts\backup.py\"" ^
/SC DAILY /ST 09:00 ^
/RL HIGHEST ^
/F
옵션이 낯설어 보이지만 실제로 자주 쓰는 건 몇 개뿐입니다.
| 옵션 | 뜻 | 비고 |
|---|---|---|
/TN | 작업 이름 | backup\daily처럼 쓰면 폴더로 묶입니다 |
/TR | 실행할 명령 | 경로에 공백이 있으면 안쪽 따옴표를 \"로 탈출 |
/SC | 주기 | DAILY / HOURLY / WEEKLY / ONLOGON / ONSTART |
/ST | 시작 시각 | 24시간제. 09:00 |
/RL HIGHEST | 가장 높은 권한으로 실행 | GUI의 동일한 체크박스 |
/F | 같은 이름이 있으면 덮어쓰기 | 없으면 재실행 시 “이미 존재” 오류 |
여기서 한 가지가 빠져 있습니다. schtasks에는 “시작 위치”(작업 디렉터리)를 지정하는 옵션이 없습니다. 그래서 스크립트가 ./config.json처럼 상대 경로를 쓰고 있으면, 손으로 돌릴 땐 멀쩡하던 것이 스케줄러에서만 파일을 못 찾습니다. 기본 작업 디렉터리가 C:\Windows\System32이기 때문입니다. 굳이 schtasks로 해결하려면 cmd /c를 한 겹 씌웁니다.
schtasks /Create /TN "daily-backup" /SC DAILY /ST 09:00 /F ^
/TR "cmd /c cd /d D:\scripts && \"C:\Python312\python.exe\" backup.py >> logs\backup.log 2>&1"
이 형태는 로그 리다이렉션까지 같이 붙일 수 있어서 편하지만, 따옴표가 세 겹으로 겹치기 시작하면 금방 읽기 어려워집니다. 옵션을 두세 개 이상 손대야 한다면 다음 방법이 낫습니다.
제대로 하려면: PowerShell Register-ScheduledTask
PowerShell에는 작업을 구성 요소별로 나눠 만드는 명령들이 있습니다. 동작(Action), 트리거(Trigger), 설정(Settings), 계정(Principal)을 각각 객체로 만든 뒤 마지막에 등록합니다. 줄 수는 늘어나지만 GUI의 모든 탭과 일대일로 대응되기 때문에 오히려 이해하기 쉽습니다.
$taskName = "daily-backup"
$workDir = "D:\scripts"
$action = New-ScheduledTaskAction `
-Execute "C:\Python312\python.exe" `
-Argument "backup.py" `
-WorkingDirectory $workDir # GUI의 "시작 위치" 칸. 이게 핵심입니다
$trigger = New-ScheduledTaskTrigger -Daily -At "09:00"
# -StartWhenAvailable : PC가 꺼져 있어 놓친 실행을 켜진 뒤에 보충합니다
$settings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable `
-DontStopIfGoingOnBatteries `
-AllowStartIfOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Hours 1)
# -LogonType S4U : 비밀번호 저장 없이, 로그오프 상태에서도 실행
$principal = New-ScheduledTaskPrincipal `
-UserId $env:USERNAME `
-LogonType S4U `
-RunLevel Highest
Register-ScheduledTask -TaskName $taskName `
-Action $action -Trigger $trigger `
-Settings $settings -Principal $principal `
-Force # 같은 이름이 있으면 덮어씁니다
-WorkingDirectory가 있다는 점이 schtasks와의 가장 큰 차이입니다. 상대 경로 문제를 우회 없이 바로 해결할 수 있습니다.
PowerShell 스크립트를 등록 대상으로 삼을 때는 실행 정책과 프로필을 명시적으로 지정하세요. 내 계정에서는 잘 돌아가던 스크립트가 스케줄러 계정에서는 실행 정책에 막히는 일이 흔합니다.
$action = New-ScheduledTaskAction `
-Execute "powershell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"D:\scripts\report.ps1`"" `
-WorkingDirectory "D:\scripts"
-NoProfile을 빼면 프로필에 정의해 둔 함수나 별칭에 스크립트가 의존하게 될 수 있습니다. 그러면 내 콘솔에서는 되고 스케줄러에서는 안 되는, 원인 찾기 가장 까다로운 종류의 실패가 생깁니다.
등록 스크립트를 파일 하나로 묶기
여기까지를 register-task.ps1 한 파일로 정리합니다. 중요한 건 몇 번을 돌려도 결과가 같아야 한다는 점(멱등성)과, 등록에서 끝내지 않고 한 번 실행해서 결과 코드까지 확인한다는 점입니다.
param(
[string]$TaskName = "daily-backup",
[string]$ScriptPath = "D:\scripts\backup.py",
[string]$At = "09:00"
)
$ErrorActionPreference = "Stop"
# 1) 관리자 권한 확인 — 없으면 Register가 액세스 거부로 실패합니다
$isAdmin = ([Security.Principal.WindowsPrincipal] `
[Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) { throw "관리자 권한 PowerShell에서 실행하세요." }
# 2) 전제 조건 검사 — 경로 오타는 등록 시점엔 조용히 통과합니다
if (-not (Test-Path $ScriptPath)) { throw "스크립트를 찾을 수 없습니다: $ScriptPath" }
$workDir = Split-Path $ScriptPath -Parent
$python = (Get-Command python).Source
$action = New-ScheduledTaskAction -Execute $python `
-Argument (Split-Path $ScriptPath -Leaf) -WorkingDirectory $workDir
$trigger = New-ScheduledTaskTrigger -Daily -At $At
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable `
-ExecutionTimeLimit (New-TimeSpan -Hours 1)
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME `
-LogonType S4U -RunLevel Highest
# 3) -Force 가 있으므로 기존 작업이 있어도 그대로 덮어씁니다 (멱등)
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
-Settings $settings -Principal $principal -Force | Out-Null
# 4) 등록으로 끝내지 말고 한 번 돌려서 결과까지 확인합니다
Start-ScheduledTask -TaskName $TaskName
Start-Sleep -Seconds 5
Get-ScheduledTaskInfo -TaskName $TaskName |
Select-Object TaskName, LastRunTime, LastTaskResult, NextRunTime
마지막 두 단계가 이 스크립트의 핵심입니다. Register-ScheduledTask는 실행 파일 경로가 틀려도 군말 없이 성공합니다. 실제로 한 번 돌려보고 LastTaskResult를 눈으로 확인해야 등록이 끝난 겁니다. 이 값은 십진수로 표시되며, 자주 만나는 값은 다음과 같습니다.
| LastTaskResult | 16진수 | 뜻 |
|---|---|---|
| 0 | 0x0 | 정상 종료 |
| 1 | 0x1 | 프로그램이 오류로 종료 (스크립트 자체 문제) |
| 267009 | 0x41301 | 현재 실행 중 |
| 267011 | 0x41303 | 아직 한 번도 실행되지 않음 |
| 2147942402 | 0x80070002 | 지정한 파일을 찾을 수 없음 (경로 오타) |
다른 PC로 그대로 옮기기: XML 내보내기
이미 GUI로 공들여 만들어 둔 작업이 있다면, 처음부터 스크립트로 다시 쓸 필요 없이 XML로 내보내면 됩니다. 설정이 한 글자도 빠짐없이 그대로 복제됩니다.
# 내보내기 — 반드시 UTF-16(Unicode)으로 저장합니다
Export-ScheduledTask -TaskName "daily-backup" |
Out-File "D:\scripts\daily-backup.xml" -Encoding unicode
# 다른 PC에서 가져오기
Register-ScheduledTask -TaskName "daily-backup" `
-Xml (Get-Content "D:\scripts\daily-backup.xml" -Raw) -Force
# cmd 쪽을 선호한다면
schtasks /Create /TN "daily-backup" /XML "D:\scripts\daily-backup.xml" /F
여기서 자주 걸려 넘어지는 지점이 인코딩입니다. schtasks /Create /XML은 UTF-16(Unicode) 파일만 받습니다. 무심코 -Encoding utf8로 저장하면 “XML 파일의 형식이 잘못되었습니다”라는 다소 엉뚱한 메시지가 나옵니다. 파일 내용은 멀쩡한데 인코딩 때문에 거절당하는 것이라, 원인을 짐작하기가 특히 어렵습니다.
등록한 뒤 확인하고 지우기
등록·확인·삭제까지가 한 세트입니다. 확인 명령은 따로 외워두는 것보다 등록 스크립트 맨 아래에 주석으로 붙여두는 편이 실전에서 더 쓸모 있습니다.
# 등록 내용 전체 확인
schtasks /Query /TN "daily-backup" /V /FO LIST
# 마지막 실행 결과만 빠르게
powershell -Command "Get-ScheduledTaskInfo -TaskName daily-backup"
# 필요 없어졌다면
schtasks /Delete /TN "daily-backup" /F
자주 사고가 나는 지점
- 관리자 권한 없이 실행.
Register-ScheduledTask와schtasks /Create는 관리자 PowerShell·명령 프롬프트가 필요합니다. 아니면 “액세스가 거부되었습니다”로 끝납니다. - 작업 디렉터리 미지정. 앞서 다룬 그 문제입니다. 스크립트 안에서 상대 경로를 쓴다면
-WorkingDirectory는 선택이 아니라 필수입니다. - 로그온 상태 가정. 기본값은 “사용자가 로그온할 때만 실행”입니다. 자리를 비운 새벽에도 돌아야 한다면
-LogonType S4U(비밀번호 저장 없음)나-LogonType Password를 지정하세요. 다만 S4U는 네트워크 드라이브·공유 폴더 접근이 막힙니다. 원격 경로를 건드리는 작업이라면 비밀번호를 저장하는 쪽을 써야 합니다. - 절전 상태 고려 누락. PC가 자고 있으면 작업은 그냥 넘어갑니다.
-StartWhenAvailable을 주면 깨어난 뒤에 놓친 실행을 보충합니다. - 한글 경로와 인코딩. 등록 스크립트를
.ps1로 저장할 때 Windows PowerShell 5.1은 BOM 없는 UTF-8을 CP949로 읽습니다. 한글이 들어간 경로가 있다면 UTF-8 BOM으로 저장하세요.
등록은 성공했는데 아무 일도 일어나지 않는다면, 그건 등록의 문제가 아니라 실행 환경의 문제일 가능성이 높습니다. 이 블로그의 “작업 스케줄러는 ‘성공’인데 아무 일도 안 일어났을 때” 글에서 그 경우를 따로 다뤘습니다.
어느 방법을 쓸까
| 방법 | 적합한 경우 | 한계 |
|---|---|---|
schtasks | 옵션 두세 개짜리 단순 작업, 배치 파일 안에서 호출 | 작업 디렉터리 지정 불가, 따옴표 탈출이 번거로움 |
Register-ScheduledTask | 옵션을 세밀하게 잡아야 하는 대부분의 경우 | PowerShell 3.0 이상 필요 (윈도우 8 이후 기본 탑재) |
| XML 내보내기/가져오기 | 이미 만든 작업을 다른 PC로 복제 | UTF-16 저장 필수, 사람이 읽고 수정하기는 불편 |
셋 중 하나만 고른다면 Register-ScheduledTask입니다. 처음 몇 줄이 길어 보여도, GUI 탭과 그대로 대응되기 때문에 나중에 다시 읽을 때 부담이 가장 적습니다.
마무리
지금 스케줄러에 걸려 있는 작업 중 가장 중요한 것 하나를 골라, Export-ScheduledTask로 XML을 뽑아 스크립트 폴더에 넣어보세요. 5초면 끝나고, 그 순간부터 그 작업은 “복구 가능한 설정”이 됩니다. 다음에 새 작업을 만들 때 위의 register-task.ps1을 복사해 경로와 시각만 바꾸면, 클릭 스무 번짜리 일이 명령 한 줄로 줄어듭니다.
다음 글에서는 AI 응답을 프로그램에서 안전하게 받아쓰는 방법 — AI가 돌려준 답을 JSON으로 안정적으로 파싱하는 법을 다루겠습니다. 모델이 코드 블록이나 설명 문장을 섞어 보내는 바람에 json.loads()가 깨지는 상황을 어떻게 막을지 정리합니다.