카테고리: 업무 자동화

파이썬 업무 자동화#6예약 실행 자동화! 매일 정해진 시간에 스크립트 돌리기 (schedule·APScheduler)

목차14개 항목

안녕하세요! Mojangcam입니다 😎

지난 파이썬 업무 자동화 #5 - 로컬 LLM(LM Studio)으로 고객 문의 자동 분류·요약하기 에서는 돈 한 푼 안 들이고 AI에게 문의 정리를 시켜봤어요. 그런데 여기까지 오면 다들 똑같은 벽에 부딪혀요.

"스크립트는 잘 만들었는데… 매일 아침 제가 직접 실행 버튼을 눌러야 하네요?" 😅

자동화인데 사람이 실행시키면 반쪽짜리잖아요. 그래서 이번 편에서는 파이썬 스크립트가 정해진 시간에 알아서 돌아가게 만들어볼게요. 매일 아침 9시 리포트 자동 생성, 10분마다 서버 상태 체크, 평일 퇴근 시간에만 일일 정산 — 전부 코드 몇 줄이면 끝나요!

📌 준비물

  • Python 3.8 이상 (작성 기준: Python 3.14.x)
  • schedule 1.2.x — 사람이 읽기 쉬운 문법의 초간단 스케줄러
  • APScheduler 3.11.x — cron 표현식·요일 조건까지 필요할 때 (3단계에서 사용)
  • python-dotenv 1.x — 웹훅 URL 같은 비밀값을 .env에서 불러오기
  • requests 2.x — 디스코드 웹훅 전송용
터미널
pip install schedule APScheduler python-dotenv requests

💡 schedule에서 타임존(Asia/Seoul)을 지정하려면 pytz도 함께 설치해주세요 (pip install pytz).

🧠 왜 파이썬으로 예약 실행을 하나요?

윈도우 작업 스케줄러나 리눅스 cron을 써도 되는데 굳이 파이썬으로 하는 이유가 있어요.

  • 한 파일로 끝나요 : 스크립트 안에 스케줄 규칙이 같이 들어 있어서, 다른 PC로 옮겨도 그대로 동작해요.
  • 조건을 마음대로 걸 수 있어요 : "공휴일이면 건너뛰기", "실패하면 5분 뒤 재시도" 같은 로직을 파이썬 문법으로 그냥 짜면 돼요.
  • 로그를 내가 원하는 대로 남겨요 : 실행 기록을 디스코드로 바로 쏠 수도 있죠.

반대로 PC를 껐다 켜도 자동으로 살아나야 한다면 OS 스케줄러가 유리해요. 이건 아래 보너스 파트에서 같이 정리할게요!

📌 1단계 - schedule로 "매일 9시" 만들기

먼저 schedule 라이브러리의 기본 문법부터 볼게요. 영어 문장 읽듯이 읽히는 게 이 라이브러리의 매력이에요.

Python
# schedule 1.2.x / Python 3.8+
import time
import schedule

def morning_report():
    print("[리포트] 오늘의 업무 리포트를 생성했어요!")

# 매일 오전 9시
schedule.every().day.at("09:00").do(morning_report)

# 10분마다
schedule.every(10).minutes.do(morning_report)

# 매주 월요일 08:30
schedule.every().monday.at("08:30").do(morning_report)

while True:
    schedule.run_pending()   # 실행할 때가 된 작업이 있으면 실행
    time.sleep(1)            # 1초 쉬었다 다시 확인

하나씩 뜯어볼게요 💡

  • schedule.every() : "매 ~마다"의 시작점이에요. 뒤에 .day / .hour / .monday 같은 단위를 붙여요.
  • .at("09:00") : 실행 시각. "HH:MM" 또는 "HH:MM:SS" 형식이고, .every().hour에 붙일 땐 ":30"처럼 분만 적어요.
  • .do(함수) : 실행할 함수를 등록해요. ⚠️ 괄호를 붙이면 안 돼요. do(morning_report())라고 쓰면 지금 당장 실행돼버려요!
  • run_pending() : 등록된 작업 중 "지금 실행할 때가 된" 것만 골라 실행해요. 이 줄을 반복문 안에서 계속 호출해줘야 스케줄이 동작해요.
  • time.sleep(1) : 이게 없으면 CPU를 100% 태워요. 분 단위 작업이라면 sleep(30) 정도도 충분해요.

타임존을 확실히 하고 싶다면 이렇게 써요.

Python
schedule.every().day.at("09:00", "Asia/Seoul").do(morning_report)

📌 2단계 - 실전! 매일 아침 리포트를 디스코드로 보내기

이제 진짜 쓸모 있는 걸 만들어볼게요. 업무 자동화 #4 - 디스코드 웹훅으로 알림 보내기 에서 만든 웹훅을 재활용해서, 매일 아침 9시에 오늘 할 일 요약을 디스코드로 쏘는 자동화예요.

먼저 프로젝트 폴더에 .env 파일을 만들어요.

터미널
# .env  (절대 깃허브에 올리지 마세요!)
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/여기에_본인_웹훅_주소
REPORT_TIME=09:00

그리고 파이썬에서 이렇게 불러와요.

Python
# Python 3.8+ / python-dotenv 1.x
import os
from dotenv import load_dotenv

load_dotenv()  # .env 파일을 읽어서 환경변수로 올려줘요

WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
REPORT_TIME = os.getenv("REPORT_TIME", "09:00")  # 없으면 09:00 사용

하나씩 뜯어볼게요 💡

  • load_dotenv() : .env 파일을 찾아서 값을 환경변수로 등록해요. 이 줄이 os.getenv()보다 먼저 나와야 해요.
  • os.getenv("키", "기본값") : 값이 없을 때 쓸 기본값을 두 번째 인자로 줄 수 있어요.
  • 웹훅 주소나 토큰은 코드에 직접 적지 마세요. 실수로 깃허브에 올라가면 남이 마음대로 쓸 수 있어요.

💡 꼭 하세요: .gitignore 파일에 .env 한 줄을 추가해두면 실수로 커밋되는 사고를 막을 수 있어요.

📌 3단계 - 평일에만, cron처럼 정교하게 (APScheduler)

schedule은 쉬운 대신 "평일 09:00", "매월 1일" 같은 조건이 약해요. 이럴 땐 APScheduler의 CronTrigger가 답이에요.

Python
# APScheduler 3.11.x / Python 3.8+
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger

def weekday_report():
    print("평일 아침에만 실행돼요!")

sched = BlockingScheduler(timezone="Asia/Seoul")

# 월~금 오전 9시 정각
sched.add_job(weekday_report, CronTrigger(day_of_week="mon-fri", hour=9, minute=0))

# 매월 1일 00:10
sched.add_job(weekday_report, CronTrigger(day=1, hour=0, minute=10))

sched.start()   # 여기서 프로그램이 멈춰서 계속 대기해요

하나씩 뜯어볼게요 💡

  • BlockingScheduler : 이 스케줄러가 프로그램을 붙잡고(block) 계속 돌아요. 스케줄만 돌리는 단독 스크립트에 딱이에요.
  • timezone="Asia/Seoul" : 서버가 해외(UTC)에 있어도 한국 시간 기준으로 돌게 해줘요. VPS에 올릴 거면 필수!
  • CronTrigger(day_of_week="mon-fri", hour=9) : cron 표현식을 파이썬 인자로 쓴 거예요. day_of_week는 mon,tue,... 또는 0~6을 받아요.
  • 디스코드 봇 안에서 같이 돌리고 싶다면 BlockingScheduler 대신 AsyncIOScheduler를 쓰면 돼요.

📌 전체 코드 (복붙 OK)

Python
# daily_report.py
# schedule 1.2.x / python-dotenv 1.x / requests 2.x / Python 3.8+
import os
import time
import logging
from datetime import datetime

import requests
import schedule
from dotenv import load_dotenv

load_dotenv()

WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
REPORT_TIME = os.getenv("REPORT_TIME", "09:00")

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("scheduler.log", encoding="utf-8"),
        logging.StreamHandler(),
    ],
)

def send_discord(message: str) -> None:
    """디스코드 웹훅으로 메시지를 보냅니다."""
    if not WEBHOOK_URL:
        logging.warning(".env에 DISCORD_WEBHOOK_URL이 없어요. 전송을 건너뜁니다.")
        return
    res = requests.post(WEBHOOK_URL, json={"content": message}, timeout=10)
    res.raise_for_status()

def build_report() -> str:
    """실제 업무 로직 자리. 엑셀 집계·크롤링 결과 등으로 바꿔 쓰세요."""
    today = datetime.now().strftime("%Y-%m-%d (%a)")
    return (
        f"**📋 {today} 업무 리포트**\n"
        f"- 처리 대기 건: 12건\n"
        f"- 어제 완료: 8건\n"
        f"- 생성 시각: {datetime.now().strftime('%H:%M:%S')}"
    )

def safe_job() -> None:
    """에러가 나도 스케줄러가 죽지 않도록 감싸주는 래퍼입니다."""
    try:
        send_discord(build_report())
        logging.info("리포트 전송 성공")
    except Exception as e:
        logging.exception("리포트 전송 실패: %s", e)

def main() -> None:
    schedule.every().day.at(REPORT_TIME).do(safe_job)
    schedule.every(30).minutes.do(
        lambda: logging.info("스케줄러 생존 확인 (heartbeat)")
    )

    logging.info("스케줄러를 시작합니다. 매일 %s에 리포트를 보냅니다.", REPORT_TIME)
    logging.info("등록된 작업: %s", schedule.get_jobs())

    while True:
        schedule.run_pending()
        time.sleep(1)

if __name__ == "__main__":
    main()

실행은 이렇게 해요.

터미널
python daily_report.py

📌 실행 결과

터미널에 이런 로그가 찍히고, 지정한 시간이 되면 디스코드에 리포트가 도착해요.

텍스트
2026-08-15 08:59:31 [INFO] 스케줄러를 시작합니다. 매일 09:00에 리포트를 보냅니다.
2026-08-15 08:59:31 [INFO] 등록된 작업: [Every 1 day at 09:00:00 do safe_job() ...]
2026-08-15 09:00:00 [INFO] 리포트 전송 성공
2026-08-15 09:29:31 [INFO] 스케줄러 생존 확인 (heartbeat)

scheduler.log 파일에도 같은 내용이 그대로 쌓이니, 나중에 "어제 진짜 돌았나?"를 확인할 때 아주 유용해요 👍

💡 보너스 - PC를 껐다 켜도 살아나게 하기

파이썬 스크립트는 터미널을 닫으면 같이 죽어요. 완전 무인 운영을 하려면 OS에 맡기는 게 좋아요.

윈도우 (작업 스케줄러)

  1. Win + R → taskschd.msc 입력
  2. 작업 만들기 → 트리거: "시스템 시작 시" 또는 "매일"
  3. 동작: 프로그램은 python.exe의 전체 경로, 인수는 daily_report.py, 시작 위치에 스크립트 폴더 경로를 꼭 넣기

리눅스·라즈베리파이 (systemd)

터미널
sudo nano /etc/systemd/system/daily-report.service
텍스트
[Unit]
Description=Daily Report Scheduler
After=network-online.target

[Service]
WorkingDirectory=/home/pi/report
ExecStart=/usr/bin/python3 /home/pi/report/daily_report.py
Restart=always

[Install]
WantedBy=multi-user.target
터미널
sudo systemctl enable --now daily-report

Restart=always 덕분에 스크립트가 죽어도 자동으로 다시 살아나요. 자세한 배포 방법은 파이썬 VPS 배포 입문 편에서 다뤘어요!

📌 자주 나는 에러

1. 시간이 됐는데 아무 일도 안 일어나요

원인 : while True 안에서 schedule.run_pending()을 호출하지 않았거나, 스크립트가 이미 종료된 경우예요.

해결 : 반복문이 살아 있는지, 터미널이 계속 켜져 있는지 확인하세요. .at("9:00")처럼 한 자리 시(時)를 쓰면 안 돼요 — 반드시 "09:00"으로 두 자리를 맞춰주세요.

2. ScheduleValueError: Invalid time format for a daily job (valid format is HH:MM(:SS)?)

원인 : .at()에 넘긴 문자열 형식이 틀렸어요. "09시", "9:0", "09:00 AM" 전부 안 돼요.

해결 : "09:00" 또는 "09:00:00" 형태로만 적어주세요. .env에서 읽어올 땐 앞뒤 공백도 조심하고요.

3. TypeError: the first argument must be callable

원인 : .do(job())처럼 괄호를 붙여서 함수를 실행한 결과를 넘긴 거예요.

해결 : .do(job)으로 함수 이름만 넘기세요. 인자를 주고 싶으면 .do(job, "인자값") 또는 .do(lambda: job("인자값"))을 쓰면 돼요.

4. 서버(VPS)에 올렸더니 9시가 아니라 18시에 실행돼요

원인 : 해외 서버의 기본 시간대가 UTC라서 9시간 차이가 나요.

해결 : APScheduler는 BlockingScheduler(timezone="Asia/Seoul"), schedule은 .at("09:00", "Asia/Seoul")로 타임존을 명시하세요. 서버 자체를 바꾸려면 sudo timedatectl set-timezone Asia/Seoul도 방법이에요.

5. 작업 하나가 에러 나니까 스케줄러 전체가 멈춰버려요

원인 : .do()에 등록한 함수에서 예외가 터지면 그대로 위로 전파돼요.

해결 : 위 전체 코드의 safe_job()처럼 try/except로 감싸세요. 로그만 남기고 다음 스케줄은 계속 돌아가게 하는 게 핵심이에요.

💬 schedule vs APScheduler, 뭘 쓸까요?

상황 추천
"매일 9시", "10분마다" 정도면 충분 schedule (문법이 가장 쉬움)
평일만·매월 1일·cron 표현식 필요 APScheduler
재부팅해도 예약이 남아 있어야 함 APScheduler + JobStore(DB)
디스코드 봇 안에서 주기 실행 discord.py의 tasks.loop

처음이라면 schedule로 시작해서, 조건이 복잡해지는 순간 APScheduler로 갈아타는 흐름을 추천해요!

📌 마무리

오늘은 파이썬 스크립트를 정해진 시간에 알아서 돌게 만들어봤어요. 정리하면 이래요.

  • schedule.every().day.at("09:00").do(job) + while True: run_pending() 조합이 기본형
  • 웹훅·토큰 같은 비밀값은 .env + python-dotenv로 분리
  • 작업 함수는 try/except로 감싸서 스케줄러가 죽지 않게
  • 평일·매월 조건이나 타임존이 중요하면 APScheduler CronTrigger
  • 무인 운영은 윈도우 작업 스케줄러 / 리눅스 systemd에 맡기기

다음 편인 파이썬 업무 자동화 #7 - 동적 페이지·로그인 자동화 (Selenium) 에서는 로그인이 필요한 사이트에서 자료를 자동으로 긁어오는 방법을 다뤄볼게요. requests로는 안 잡히던 그 페이지들이요! 기대해주세요 🙌

오늘 만든 스케줄러에 어떤 작업을 물려두셨는지 댓글로 알려주세요. 다음 글감으로 만들어볼게요 😎

📚 파이썬 업무 자동화 시리즈

  1. #1 - 엑셀 자동화 입문 (openpyxl)
  2. #2 - 웹 크롤링 입문 (requests + BeautifulSoup)
  3. #3 - 파일 정리 자동화 (이름 일괄 변경)
  4. #4 - 디스코드 웹훅(Webhook)으로 알림 보내기
  5. #5 - 로컬 LLM(LM Studio)으로 고객 문의 자동 분류·요약
  6. #6 - 예약 실행 자동화 (schedule·APScheduler) ← 지금 이 글!
  7. #7 - 동적 페이지·로그인 자동화 (Selenium) (예정)

번외: 파이썬 이메일 자동 발송하기 - 지메일(Gmail) 자동화 (smtplib)

연재 · 14편 중 6번째

파이썬 업무 자동화

  1. #1엑셀 자동화 입문 (openpyxl)!
  2. #2웹 크롤링 입문 (requests + BeautifulSoup)!
  3. #3파일 정리 자동화 (이름 일괄 변경)!
  4. #4디스코드 웹훅(Webhook)으로 알림 보내기 (requests)!
  5. #5로컬 LLM(LM Studio)으로 고객 문의 자동 분류·요약하기! (무료 AI)
  6. #6예약 실행 자동화! 매일 정해진 시간에 스크립트 돌리기 (schedule·APScheduler)지금 읽는 글
  7. #7동적 페이지·로그인 자동화! requests로 안 되는 사이트 긁기 (Selenium)
  8. #8PDF 자동 처리! 여러 파일 병합·분할부터 비밀번호까지 (pypdf) 📄
  9. #9마우스·키보드 GUI 자동화! 클릭·타이핑을 파이썬이 대신하게 하기 (PyAutoGUI)
  10. #10워드(Word) 문서 자동 생성! 엑셀 명단으로 계약서·안내문 100장 뽑기 (python-docx) 📄
  11. #11개인화 대량 메일 자동 발송! 명단별 첨부파일·발송 로그·재시도까지 (smtplib 실전편) 📬
  12. #12구글 시트 연동하기! 팀원과 같이 보는 자동 시트 (gspread) 📗
  13. #13파이썬 자동화 로그·재시도·디스코드 실패 알림 만들기 - 조용히 멈추는 스크립트 잡기
  14. #14파워포인트(PowerPoint) 보고서 자동 생성! 엑셀 숫자만 바꾸면 발표자료 완성 (python-pptx) 📊
전체 14편 보기접기

글쓴이

Mojangcam

직접 코드를 짜고, 서버를 돌리고, 막힌 에러를 해결하며 배운 것들을 기록하는 개발·IT 실전 기록 블로그