파이썬 자동화 #5 결과를 알려주기: 메일, 슬랙, 디스코드 알림

6 분 소요

스크립트가 잘 돌았는지 확인하려고 매일 아침 터미널을 열고 로그를 뒤지고 있다면, 그 확인 작업 자체가 또 하나의 수작업입니다. #2에서 엑셀을 만들고 #3·#4에서 데이터를 긁어 왔지만, 결과가 사람에게 도착하지 않으면 자동화는 절반만 끝난 것입니다. 이번 글에서는 스크립트가 스스로 결과를 보고하게 만들겠습니다. 슬랙과 디스코드 웹훅, 그리고 엑셀 파일을 첨부한 이메일까지 다루겠습니다.

  • #1 스크립트 첫걸음
  • #2 엑셀 자동화
  • #3 웹 스크래핑: 정적 페이지
  • #4 웹 스크래핑: 동적 페이지
  • #5 결과를 알려주기: 메일, 슬랙, 디스코드 알림 ← 이번 글
  • #6 스케줄링
  • #7 CLI 도구로 포장

가장 쉬운 길: 웹훅 #

알림 보내는 방법 중 가장 쉬운 것은 웹훅(webhook)입니다. 슬랙이나 디스코드가 발급해 주는 고유 URL에 JSON을 POST 하면 그 내용이 지정한 채널에 메시지로 올라옵니다. 봇 계정도, OAuth도, SDK도 필요 없습니다. 슬랙은 앱 설정에서 Incoming Webhooks를 켜고 채널을 지정하면 https://hooks.slack.com/services/... 형태의 URL을 발급해 주고, 디스코드는 채널 설정의 연동 메뉴에서 웹후크를 만들 수 있습니다. URL을 받았다면 코드는 이게 전부입니다.

notify.py: 슬랙·디스코드 알림
import httpx

SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/T000/B000/XXXX"
DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1234/abcd"


def notify_slack(text: str) -> None:
    httpx.post(SLACK_WEBHOOK_URL, json={"text": text}).raise_for_status()


def notify_discord(text: str) -> None:
    httpx.post(DISCORD_WEBHOOK_URL, json={"content": text}).raise_for_status()

이제 notify_slack("가격 수집 완료: 신규 32건") 한 줄로 채널에 메시지가 올라옵니다. 두 서비스의 차이는 JSON 키 하나뿐입니다. 슬랙은 text, 디스코드는 content를 씁니다. raise_for_status()를 붙여 두면 웹훅 URL이 만료됐거나 잘못됐을 때 조용히 넘어가지 않고 바로 에러가 납니다.

메시지 꾸미기 #

“완료"라는 한 단어보다, 열어 보지 않아도 상황을 알 수 있는 요약이 좋은 알림입니다. 성공이면 핵심 숫자를, 실패면 에러 내용을 담아 보내겠습니다.

요약과 에러를 담은 메시지
import traceback

try:
    rows = collect_prices()  # 3편에서 만든 수집 함수
    notify_slack(f"[OK] 가격 수집 완료: {len(rows)}건")
except Exception:
    notify_slack(f"[FAIL] 가격 수집 실패\n{traceback.format_exc()[-1500:]}")
    raise

실패 알림의 핵심은 traceback.format_exc()입니다. 예외의 전체 스택 트레이스를 문자열로 받아 메시지에 그대로 실어 보내므로, 알림만 보고도 어느 줄에서 무엇이 터졌는지 알 수 있습니다. 뒤에서 1500자만 잘라 보내는 이유는 슬랙·디스코드 모두 메시지 길이 제한이 있고, 트레이스백은 마지막 부분에 원인이 모여 있기 때문입니다. 마지막의 raise도 중요합니다. 알림을 보냈다고 예외를 삼켜 버리면 종료 코드가 0이 되어, 다음 편에서 다룰 스케줄러가 성공으로 착각합니다.

이메일: smtplib와 앱 비밀번호 #

웹훅은 간편하지만 파일을 보내기는 번거롭습니다. #2에서 만든 엑셀 보고서를 사람에게 전달하는 일이라면 이메일이 여전히 가장 확실합니다. 표준 라이브러리 smtplibemail만으로 충분합니다. 준비물이 하나 있습니다. 지메일은 일반 비밀번호로는 SMTP 로그인이 되지 않으므로, 구글 계정에서 2단계 인증을 켠 뒤 계정 설정의 “앱 비밀번호” 메뉴에서 16자리 비밀번호를 발급받아야 합니다. 이 16자리가 코드에서 쓸 비밀번호입니다.

send_mail.py: 엑셀 첨부 메일
import smtplib
import ssl
from email.message import EmailMessage
from pathlib import Path


def send_report(subject: str, body: str, attachment: Path) -> None:
    msg = EmailMessage()
    msg["From"] = "me@gmail.com"
    msg["To"] = "boss@example.com"
    msg["Subject"] = subject
    msg.set_content(body)
    msg.add_attachment(
        attachment.read_bytes(),
        maintype="application",
        subtype="vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        filename=attachment.name,
    )
    context = ssl.create_default_context()
    with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
        server.login("me@gmail.com", "발급받은 앱 비밀번호 16자리")
        server.send_message(msg)

이제 send_report("주간 가격 보고", "수집 결과를 첨부합니다.", Path("가격_정리.xlsx"))처럼 호출하면 됩니다. 포인트를 짚겠습니다. SMTP_SSL과 465 포트 조합은 처음부터 암호화된 연결을 쓰고, ssl.create_default_context()는 인증서 검증까지 포함한 기본 보안 설정을 만들어 줍니다. 첨부는 add_attachment() 한 번이면 되고, subtype에 적은 긴 문자열은 xlsx 파일의 MIME 타입입니다. PDF라면 pdf, CSV라면 csv로 바꾸면 됩니다.

비밀 관리: 토큰을 코드에 적지 않기 #

위 코드에는 문제가 하나 있습니다. 웹훅 URL과 앱 비밀번호가 코드에 그대로 적혀 있습니다. 웹훅 URL은 그 자체가 비밀번호입니다. URL을 아는 사람은 누구나 그 채널에 메시지를 보낼 수 있고, 깃허브에 올라가는 순간 유출입니다. 비밀 값은 .env 파일로 빼고 uv add python-dotenv로 로더를 설치하겠습니다.

.env
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T000/B000/XXXX
GMAIL_APP_PASSWORD=abcdabcdabcdabcd
환경변수로 읽기
import os
from dotenv import load_dotenv

load_dotenv()  # .env 파일을 환경변수로 로드
SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]
GMAIL_APP_PASSWORD = os.environ["GMAIL_APP_PASSWORD"]

os.environ["..."]은 값이 없으면 즉시 KeyError를 냅니다. os.environ.get()으로 조용히 None을 받는 것보다, 설정 누락을 시작 시점에 바로 터뜨리는 쪽이 자동화 스크립트에는 안전합니다. 그리고 .env는 반드시 .gitignore에 추가합니다. 이 파일이 저장소에 올라가면 분리한 의미가 없어집니다.

성공도 알릴까, 실패만 알릴까 #

기술적으로는 둘 다 보낼 수 있습니다. 그런데 매일 도착하는 “[OK] 완료” 알림은 일주일이면 아무도 읽지 않게 됩니다. 알림이 무뎌지면 정작 실패 알림도 묻힙니다. 운영에서 흔히 쓰는 절충은 이렇습니다.

  • 실패는 즉시 알립니다. 알림의 존재 이유입니다.
  • 성공은 숫자가 이상할 때만 알립니다. 평소 300건 수집되던 작업이 5건이면, 에러 없이 끝났어도 사실상 실패입니다.
  • 정기 요약은 주 1회면 충분합니다. “이번 주 7회 실행, 7회 성공” 한 줄이면 살아 있다는 확인이 됩니다.

종합: 스크래핑 결과에 알림 붙이기 #

3편과 4편에서 만든 수집 스크립트에 지금까지의 내용을 붙이면 이런 모습이 됩니다.

job.py: 수집 + 알림 종합
import sys
import traceback

from notify import notify_slack
from scraper import collect_prices  # 3편의 수집 함수

try:
    rows = collect_prices()
    if len(rows) < 100:  # 평소 수집량 기준선
        notify_slack(f"[WARN] 수집 건수가 평소보다 적습니다: {len(rows)}건")
except Exception:
    notify_slack(f"[FAIL] 가격 수집 실패\n{traceback.format_exc()[-1500:]}")
    sys.exit(1)

성공하면 침묵하고, 건수가 이상하면 경고하고, 예외가 나면 트레이스백과 함께 즉시 알립니다. sys.exit(1)로 실패를 종료 코드에도 남겨 두었습니다. 이 종료 코드는 다음 편의 스케줄러가 읽게 됩니다.

정리 #

이번 글에서 만든 것을 정리하겠습니다.

  • 슬랙·디스코드 웹훅: httpx.post 한 번으로 전송하고, 실패 시 traceback.format_exc()를 실어 로그 확인 단계 생략
  • smtplib + SMTP_SSL + 앱 비밀번호로 엑셀 첨부 메일 발송
  • 웹훅 URL과 비밀번호는 .env로 분리하고 .gitignore에 등록
  • 실패는 즉시, 성공은 숫자가 이상할 때만 알리는 운영 기준

이제 스크립트는 결과를 만들고 보고까지 합니다. 남은 문제는 하나입니다. 아직 사람이 직접 실행해야 합니다. 다음 글(#6 스케줄링)에서는 cron과 작업 스케줄러로 스크립트를 정해진 시각에 자동 실행하고, 이번 글에서 남긴 종료 코드를 활용하는 방법을 다루겠습니다.

X