경쟁사 홈페이지의 가격이 언제 바뀌었는지, 관심 있는 상품이 언제 재입고됐는지 매번 직접 들어가서 확인하고 계신가요? 그러나 이런 확인 작업은 사람이 반복하기엔 비효율적이고, 무엇보다 타이밍을 놓치기 쉽습니다. 따라서 이런 상황에서는 웹사이트 모니터링 자동화를 구축해두는 것이 훨씬 효율적입니다. 오늘은 특정 페이지의 변경 사항을 자동으로 감지하고, 변화가 생겼을 때 즉시 알림까지 받는 방법을 소개하겠습니다.
기본 원리는 생각보다 단순하다
웹사이트 모니터링의 핵심은 “이전에 저장해둔 페이지 내용”과 “지금 새로 가져온 페이지 내용”을 비교하는 것입니다. 즉, 정해진 주기로 페이지를 크롤링한 뒤, 이전 실행 결과와 다르면 변경으로 판단하고 알림을 보내는 구조입니다. 물론 페이지 전체를 비교할 수도 있지만, 원하는 특정 영역(가격, 재고 상태 등)만 뽑아서 비교하면 훨씬 정확하고 노이즈도 줄어듭니다.
필요한 라이브러리
크롤링에는 requests와 BeautifulSoup을, 이전 상태 저장에는 간단한 텍스트 파일이나 파일 해시를 사용합니다.
pip install requests beautifulsoup4
실전 예제: 특정 영역 변경 감지하기
아래는 특정 상품 페이지의 가격 정보를 감지해, 이전과 다르면 알림을 보내는 스크립트입니다.
import requests
from bs4 import BeautifulSoup
import hashlib
from pathlib import Path
URL = "https://example.com/product/123"
HASH_FILE = Path("last_hash.txt")
def get_target_content() -> str:
response = requests.get(URL, timeout=10)
soup = BeautifulSoup(response.text, "html.parser")
price_tag = soup.select_one(".price")
return price_tag.text.strip() if price_tag else ""
def has_changed(content: str) -> bool:
current_hash = hashlib.sha256(content.encode()).hexdigest()
if not HASH_FILE.exists():
HASH_FILE.write_text(current_hash)
return False
previous_hash = HASH_FILE.read_text()
if current_hash != previous_hash:
HASH_FILE.write_text(current_hash)
return True
return False
if __name__ == "__main__":
content = get_target_content()
if has_changed(content):
print("변경 감지됨:", content)
else:
print("변경 없음")

이 스크립트는 매번 실행될 때마다 해당 영역의 텍스트를 해시로 변환해 저장해두고, 다음 실행 시 새로 가져온 값과 비교합니다. 그래서 페이지 전체가 아니라 원하는 부분만 정확하게 추적할 수 있습니다.
직접 돌려 보니 — 진짜 변경 1번에 알림이 5번 왔다
위 기본 코드를 이 PC(윈도우 11, Python 3.12.10, requests 2.34.2, beautifulsoup4 4.15.0)에서 시험했습니다. 남의 사이트에 반복 요청을 보낼 수는 없어서 파이썬 http.server로 상품 페이지를 직접 띄웠습니다. 이 페이지에는 실제 쇼핑몰처럼 요청할 때마다 바뀌는 부분을 넣었습니다. 하나는 현재 시각(마이크로초까지)이고, 다른 하나는 세 개 중 무작위로 바뀌는 광고 영역입니다. 가격(<span class="price">)은 일부러 바꿀 때만 바뀝니다.
먼저 “페이지 전체를 해시로 비교하면 안 되나?”부터 확인했습니다. 내용을 하나도 바꾸지 않고 20번 연속 요청해서, 직전 요청과 해시가 다른 횟수를 셌습니다.
| 비교 대상 | 20회 연속 요청 중 “바뀜”으로 나온 횟수 (19번 비교) |
|---|---|
| 페이지 전체 HTML | 19번 (매번 바뀜) |
.price 영역 텍스트만 | 0번 |
시각과 광고 때문에 전체 해시는 매번 달라집니다. 이 방식으로 30분마다 돌리면 하루 48번 알림이 옵니다. 그러면 금방 알림을 끄게 되고, 결국 진짜 변경도 놓칩니다. 본문에서 말한 “특정 영역만 비교하라”는 조언이 실제로 이 정도 차이를 냅니다.
그다음 기본 코드(영역만 비교하는 버전)로 상황 네 가지를 만들어 봤습니다. 상황마다 한 번 바꾸고, 정상 페이지로 되돌린 다음 한 번 더 실행했습니다.
| 상황 | 추출된 값 | 기본 코드 판정 | 정상 복귀 후 판정 |
|---|---|---|---|
| 가격 39,000원 → 35,000원 | '35,000원' | 변경 (정답) | 그대로 |
| 가격 앞뒤에 공백·줄바꿈만 추가 | '35,000원' | 그대로 (정답) | 그대로 |
| 서버 점검으로 503 응답 | '' (빈 문자열) | 변경 (가짜 알림) | 변경 (가짜 알림) |
사이트 개편으로 클래스명이 sale-price로 바뀜 | '' (빈 문자열) | 변경 (가짜 알림) | 변경 (가짜 알림) |
공백은 .strip()이 잘 걸러 줬습니다. 문제는 아래 두 줄입니다. 기본 코드는 응답 코드를 보지 않고, .price를 못 찾으면 빈 문자열을 돌려줍니다. 그래서 점검 페이지가 뜨면 “가격이 빈 값으로 바뀌었다”고 알림을 보냅니다. 점검이 끝나면 “빈 값에서 35,000원으로 바뀌었다”고 한 번 더 보냅니다. 가격은 한 번 바뀌었는데 알림은 총 다섯 번 왔고, 그중 네 번이 가짜였습니다. 실제로 쓰다 보면 가짜 알림이 가장 자주 나는 곳이 바로 여기입니다.
고친 코드: 못 가져왔으면 비교하지 않는다
핵심은 “가져오기 실패”와 “내용 변경”을 구분하는 것입니다. 응답이 200이 아니거나 선택자를 못 찾으면 예외를 던지고, 그 회차는 비교도 저장도 하지 않고 건너뜁니다. 덤으로 텍스트 안쪽의 연속 공백도 하나로 합쳐서 비교합니다.
import hashlib
import re
from pathlib import Path
import requests
from bs4 import BeautifulSoup
URL = "https://example.com/product/123"
SELECTOR = ".price"
HASH_FILE = Path("last_hash.txt")
class FetchError(Exception):
"""페이지는 받았지만 비교할 값을 못 꺼낸 경우"""
def get_target_content() -> str:
r = requests.get(URL, timeout=10)
if r.status_code != 200:
raise FetchError(f"HTTP {r.status_code}")
tag = BeautifulSoup(r.text, "html.parser").select_one(SELECTOR)
if tag is None:
raise FetchError(f"선택자 {SELECTOR} 없음")
return re.sub(r"\s+", " ", tag.get_text()).strip()
def has_changed(content: str) -> bool:
current = hashlib.sha256(content.encode()).hexdigest()
previous = HASH_FILE.read_text() if HASH_FILE.exists() else None
HASH_FILE.write_text(current)
return previous is not None and previous != current
if __name__ == "__main__":
try:
content = get_target_content()
except (FetchError, requests.RequestException) as e:
print("이번 회차 건너뜀:", e) # 저장된 해시는 그대로 둔다
else:
if has_changed(content):
print("변경 감지됨:", content)
else:
print("변경 없음")
같은 로컬 페이지에 같은 순서로 다시 돌린 결과입니다.
| 순서 | 고친 코드 결과 |
|---|---|
| 첫 실행 | 그대로 (기준값 저장) |
| 아무것도 안 바꾸고 10회 | 10회 모두 그대로 |
| 가격 변경 | 변경 → 알림 |
| 공백만 변경 | 그대로 |
| 503 점검 | 건너뜀(HTTP 503) |
| 정상 복귀 | 그대로 |
| 클래스명 변경 | 건너뜀(선택자 .price 없음) |
| 정상 복귀 | 그대로 |
알림은 진짜 가격 변경 한 번만 왔습니다. 건너뛴 회차에서 해시 파일을 덮어쓰지 않는 게 중요합니다. 덮어쓰면 점검 페이지가 새 기준값이 되어서, 복귀할 때 다시 가짜 알림이 납니다.
남은 함정 두 가지
첫째, 클래스명이 바뀐 경우는 이제 조용히 건너뛰기만 합니다. 사이트가 개편되면 감시가 고장 난 채로 아무 말 없이 돌아가는 셈입니다. 그래서 “건너뜀”이 몇 회 연속 나오면 그때는 “감시 대상을 못 찾음”이라는 알림을 따로 보내는 편이 좋습니다. 점검처럼 잠깐 실패하는 경우와 구조가 바뀐 경우를 횟수로 구분하는 겁니다.
둘째, 요청 간격입니다. 로컬 서버 기준으로 요청과 파싱 한 번에 평균 5.41ms(50회 측정, 최대 6.16ms)가 걸렸습니다. 내 PC 쪽 부담은 거의 없다는 뜻입니다. 그러니 간격은 상대 서버를 기준으로 정해야 합니다. 가격이나 재입고는 분 단위로 확인해 봐야 의미가 크지 않습니다. 10분이나 30분 간격이면 충분하고, robots.txt와 이용약관에서 자동 수집을 막고 있지 않은지도 먼저 봐야 합니다.
변경 감지되면 바로 알림 보내기
변경이 감지된 순간, 이전에 만들어둔 텔레그램 봇 알림 함수를 그대로 연결하면 됩니다.
import requests
TOKEN = "발급받은_봇_토큰"
CHAT_ID = "내_채팅_ID"
def send_telegram_message(text: str):
url = f"https://api.telegram.org/bot{TOKEN}/sendMessage"
requests.post(url, data={"chat_id": CHAT_ID, "text": text}, timeout=10)
if has_changed(content):
send_telegram_message(f"페이지 변경 감지: {content}")
이렇게 하면 변경 감지와 알림 발송이 하나의 파이프라인으로 자연스럽게 연결됩니다.
자동 반복 실행으로 완성하기
물론 이 스크립트도 한 번만 실행해서는 의미가 없습니다. 따라서 앞서 다룬 윈도우 작업 스케줄러나 크론잡에 등록해 10분, 30분, 혹은 1시간 간격으로 자동 실행되도록 설정해야 진짜 모니터링 시스템이 완성됩니다.
주의할 점
다만 크롤링 대상 사이트의 이용약관이나 robots.txt를 반드시 확인해야 합니다. 그리고 너무 짧은 주기로 반복 요청을 보내면 서버에 부담을 주거나 IP가 차단될 수 있으므로, 적절한 요청 간격을 두는 것이 중요합니다.
마무리
웹사이트 모니터링 자동화는 한 번 구축해두면 이후로는 정말 손댈 일이 거의 없는 자동화입니다. 가격 추적, 재입고 확인, 공지사항 변경 감지 등 다양한 상황에 응용할 수 있으니, 오늘 소개한 기본 구조를 자신의 필요에 맞게 확장해보시기 바랍니다.