학습 기록

Python 공부 기록 #24 - Discord Webhook으로 크롤링 성공과 실패 알림 받기

0. 목차

  • 인트로: 로그 파일을 매번 열어보는 게 맞나?
  • Webhook이 뭔지부터 정리하기
  • Discord에서 Webhook 주소 만들기
  • .env에 Webhook 주소 넣기
  • requests로 첫 메시지 보내기
  • 크롤링 성공과 실패를 나눠서 알리기
  • 알림 때문에 자동 실행이 멈추지 않게 만들기
  • 삽질했던 부분들
  • 아웃트로: 결과를 찾아가지 않고 받아보기

1. 인트로: 로그 파일을 매번 열어보는 게 맞나?

지난 글에서는 python-dotenv로 API 키를 .env 파일에 분리했습니다.

작업 스케줄러가 매일 아침 크롤링을 돌리고, 로그도 알아서 쌓이고, 설정도 파일로 빠졌습니다. 근데 문제가 하나 남아 있었습니다.

잘 돌아갔는지 확인하려면 결국 제가 PC 앞에 앉아서 로그 파일을 열어봐야 했습니다.

7월 말에 출장을 다녀오면서 사흘치 로그를 몰아서 봤는데, 둘째 날부터 크롤링이 실패하고 있었습니다. 사흘 뒤에야 안 거죠.

찾아보니까 Discord Webhook을 쓰면 Python에서 곧바로 제 Discord 채널로 메시지를 보낼 수 있었습니다. 폰으로 알림이 오니까 로그를 찾아갈 필요가 없어집니다.


2. Webhook이 뭔지부터 정리하기

Webhook은 쉽게 말하면 미리 발급받은 전용 주소로 메시지를 보내면, 그 주소에 연결된 채널에 글이 올라가는 구조입니다.

Discord 봇을 따로 만들지 않아도 됩니다. 로그인도, 토큰 관리도 없습니다. 주소 하나에 requests.post()만 하면 끝입니다.

대신 그 주소를 아는 사람은 누구나 채널에 글을 쓸 수 있습니다. 그래서 .env로 분리하는 게 맞았습니다.


3. Discord에서 Webhook 주소 만들기

순서는 이렇습니다.

  1. 알림을 받을 서버와 채널 준비하기
  2. 채널 이름 옆 톱니바퀴(채널 편집) 누르기
  3. 연동 > 웹후크 > 새 웹후크
  4. 이름을 정하고 웹후크 URL 복사

주소는 https://discord.com/api/webhooks/... 형태의 긴 문자열입니다. 이게 곧 비밀번호나 마찬가지라서 어디에도 붙여넣지 않고 바로 .env로 옮겼습니다.


4. .env에 Webhook 주소 넣기

지난 글에서 만든 .env에 한 줄만 추가했습니다.

OPENAI_API_KEY=sk-example-key
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/실제주소

.gitignore에 .env는 이미 들어가 있으니 추가로 할 일은 없었습니다.


5. requests로 첫 메시지 보내기

import os
import requests
from dotenv import load_dotenv

load_dotenv()

webhook_url = os.getenv("DISCORD_WEBHOOK_URL")

if not webhook_url:
    raise RuntimeError(".env의 DISCORD_WEBHOOK_URL을 확인하세요.")

response = requests.post(
    webhook_url,
    json={"content": "Python에서 보낸 첫 메시지입니다."},
    timeout=10,
)

print("응답 코드:", response.status_code)

json={"content": "내용"} 이 부분이 핵심입니다. Discord는 content라는 이름의 값을 메시지 본문으로 읽습니다.

성공하면 응답 코드가 204로 나옵니다. 200이 아니라서 처음엔 실패한 줄 알았습니다. 204는 “정상 처리했는데 돌려줄 내용은 없다”는 뜻입니다.

폰에서 알림이 띵 하고 울리는 순간, 이거 되겠는데? 싶었습니다.


6. 크롤링 성공과 실패를 나눠서 알리기

메시지를 보내는 부분을 함수로 빼고, 크롤링 결과에 따라 다른 내용을 넣었습니다.

import logging
from datetime import datetime

def send_discord(message):
    requests.post(
        webhook_url,
        json={"content": message},
        timeout=10,
    )

def run_crawler():
    today = datetime.now().strftime("%Y-%m-%d")
    try:
        count = crawl_and_save()
        send_discord(
            f"[성공] {today} 크롤링 완료 / {count}건 저장"
        )
    except Exception as e:
        logging.exception("크롤링 실패")
        send_discord(
            f"[실패] {today} 크롤링 중단\n원인: {e}"
        )

try 안에서 크롤링이 끝나면 몇 건을 저장했는지까지 같이 보냅니다. 중간에 오류가 나면 except로 넘어가서 실패 메시지를 보냅니다.

건수를 같이 보내는 게 생각보다 중요했습니다. 오류 없이 끝났는데 0건이면 그것도 사실상 실패니까요. “성공”만 오면 안심하고 넘어갔을 텐데, 숫자가 붙으니까 이상한 날이 바로 눈에 띄었습니다.

logging.exception()은 오류 내용을 로그 파일에 그대로 남깁니다. 알림에는 짧게, 로그에는 자세히 남기는 식으로 나눴습니다.


7. 알림 때문에 자동 실행이 멈추지 않게 만들기

여기서 한 번 크게 당했습니다. Discord 쪽 응답이 느린 날에 알림을 보내다가 스크립트 전체가 멈춰 있었습니다.

알림은 어디까지나 곁다리인데, 이것 때문에 본 작업이 죽으면 안 됐습니다.

def send_discord(message):
    if not webhook_url:
        logging.warning("Webhook 주소가 없어 알림을 건너뜁니다.")
        return
    try:
        requests.post(
            webhook_url,
            json={"content": message},
            timeout=10,
        )
    except requests.exceptions.RequestException as e:
        logging.warning("Discord 알림 실패: %s", e)

timeout=10으로 10초 안에 응답이 없으면 포기하게 했습니다. 그리고 알림 함수 안을 통째로 try로 감쌌습니다.

알림이 실패해도 로그에 경고만 남기고 크롤링은 계속 갑니다. 자동 실행 스크립트에서는 이 순서가 중요했습니다.


8. 삽질했던 부분들

첫날엔 Webhook 주소를 코드에 그대로 적어놓고 테스트했습니다. 지난 글을 쓰고 하루 만에 똑같은 짓을 한 거죠. .env로 옮기면서 주소도 새로 발급받았습니다.

메시지가 2000자를 넘으면 Discord가 거부합니다. 오류 내용을 통째로 넣었다가 걸렸습니다. 지금은 f"원인: {e}" 정도로 짧게 자르고, 전체 내용은 로그 파일에서 봅니다.

json= 대신 data=를 쓴 적도 있었는데 400 오류가 났습니다. data=는 문자열 그대로 보내고, json=은 JSON 형식으로 만들어서 보냅니다. Discord는 후자를 기대합니다.

짧은 시간에 메시지를 여러 번 보내면 429 코드와 함께 잠깐 막힙니다. 기다리면 풀리고, 하루 한두 번 보내는 용도라면 걸릴 일이 거의 없었습니다.


9. 아웃트로: 결과를 찾아가지 않고 받아보기

오늘은 Discord Webhook으로 크롤링 결과를 폰으로 받아봤습니다.

핵심 정리

  1. Discord 채널 편집 > 연동 > 웹후크에서 주소 발급받기
  2. 주소는 .env에 넣고 os.getenv()로 읽기
  3. requests.post(url, json={"content": 내용})으로 전송하기
  4. try / except로 성공과 실패 메시지 나누기
  5. 알림 함수에 timeout과 예외 처리를 넣어 본 작업 지키기

이제 할 수 있는 것들

  • 자동 실행 결과를 폰 알림으로 받기
  • 실패한 날을 그날 바로 알아채기
  • 저장 건수까지 확인해서 빈 결과 걸러내기
  • 로그 파일을 열지 않고 상태 확인하기

직장인 Python 공부를 하면서 스크립트를 짜는 것보다 돌아가는 걸 어떻게 지켜볼지가 더 어려운 문제라는 걸 알았습니다. 업무 자동화는 만들고 끝이 아니라 계속 확인해야 하는 일이었습니다.

다음 포스팅에서는 지금까지 만든 크롤러를 함수 단위로 쪼개고, 파일도 역할별로 나눠서 정리해 보겠습니다. 코드가 한 파일에 200줄 넘게 쌓여서 어디를 고쳐야 할지 저도 헷갈리기 시작했습니다.