왜 만들었나
알림, 날씨 확인, 간단한 메모처럼 반복적으로 하는 작업이 있다. 이걸 Telegram 봇으로 만들면 폰에서 채팅 한 줄로 처리된다. 별도 앱 없이도 되고, 서버가 있으면 24시간 돌릴 수도 있다.
python-telegram-bot은 Telegram Bot API의 Python 래퍼다. v20부터 완전한 async/await 구조로 바뀌었고, 명령어 핸들러부터 인라인 버튼까지 필요한 기능이 모두 갖춰져 있다.
앱 구조
24-python-telegram-bot/
├── main.py # 봇 메인 스크립트
├── requirements.txt # python-telegram-bot, python-dotenv, httpx
└── .env # BOT_TOKEN=your_token (직접 생성)
구현할 기능:
| 명령어 | 설명 |
|---|---|
/start |
환영 메시지 + 인라인 버튼 |
/help |
명령어 목록 |
/weather [도시] |
Open-Meteo 날씨 조회 |
/memo [내용] |
메모 저장 |
/list |
메모 목록 |
/clear |
메모 전체 삭제 |
| 일반 텍스트 | 에코 (그대로 반환) |
봇 토큰 발급
- Telegram에서 @BotFather 검색
/newbot입력 → 봇 이름, 사용자명 설정- 발급된 토큰을
.env파일에 저장
# .env
BOT_TOKEN=1234567890:AAF...your_token
핵심 구현
앱 초기화 & 핸들러 등록
def main() -> None:
if not BOT_TOKEN:
raise ValueError("BOT_TOKEN이 설정되지 않았습니다.")
app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", help_command))
app.add_handler(CommandHandler("weather", weather))
app.add_handler(CommandHandler("memo", memo))
app.add_handler(CommandHandler("list", list_memos))
app.add_handler(CommandHandler("clear", clear_memos))
app.add_handler(CallbackQueryHandler(button_callback))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
app.run_polling(allowed_updates=Update.ALL_TYPES)
CommandHandler는 /명령어를, MessageHandler는 일반 텍스트를 처리한다. filters.TEXT & ~filters.COMMAND는 "텍스트이면서 명령어가 아닌" 메시지만 에코 핸들러로 보낸다.
인라인 버튼 (/start)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [
[
InlineKeyboardButton("📋 명령어 보기", callback_data="show_help"),
InlineKeyboardButton("🌤 날씨 예시", callback_data="weather_example"),
],
[InlineKeyboardButton("📝 메모 예시", callback_data="memo_example")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text("안녕하세요! ...", reply_markup=reply_markup)
InlineKeyboardButton은 메시지에 버튼을 붙인다. callback_data는 버튼을 눌렀을 때 CallbackQueryHandler로 전달되는 식별자다.
인라인 버튼 콜백
async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer() # 로딩 스피너 제거
if query.data == "show_help":
await query.edit_message_text("📌 *명령어 목록*\n\n...", parse_mode="Markdown")
elif query.data == "weather_example":
await query.edit_message_text("`/weather Seoul`\n...", parse_mode="Markdown")
query.answer()는 버튼 클릭 후 스피너를 제거한다. 호출하지 않으면 Telegram 클라이언트에서 로딩 표시가 계속 남는다. edit_message_text()는 원래 메시지를 새 내용으로 교체한다.
날씨 조회 (Open-Meteo, API 키 불필요)
async def weather(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
city = " ".join(context.args)
async with httpx.AsyncClient(timeout=10) as client:
# 1단계: 도시명 → 위도/경도
geo = await client.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={"name": city, "count": 1, "language": "ko"},
)
result = geo.json()["results"][0]
lat, lon = result["latitude"], result["longitude"]
# 2단계: 날씨 조회
w_resp = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": lat, "longitude": lon,
"current": "temperature_2m,relative_humidity_2m,weathercode,windspeed_10m",
"timezone": "auto",
},
)
w = w_resp.json()["current"]
await update.message.reply_text(
f"🌡 *{result['name']}* — {w['temperature_2m']}°C\n"
f"💧 습도: {w['relative_humidity_2m']}%\n"
f"💨 풍속: {w['windspeed_10m']} km/h",
parse_mode="Markdown",
)
httpx.AsyncClient를 async with로 열어서 비동기 HTTP 요청을 보낸다. requests는 동기 라이브러리라 async 봇 코드 안에서 그냥 쓰면 이벤트 루프를 블로킹한다. httpx는 동일한 API에서 비동기를 지원한다.
사용자별 메모 저장
user_memos: dict[int, list[str]] = {}
async def memo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
user_id = update.effective_user.id
content = " ".join(context.args)
user_memos.setdefault(user_id, []).append(content)
await update.message.reply_text(f"✅ 저장 완료! (총 {len(user_memos[user_id])}개)")
user_id를 키로 사용하면 봇을 여러 명이 써도 메모가 섞이지 않는다. 인메모리 저장이라 봇 재시작 시 초기화된다. 영구 저장이 필요하면 SQLite나 Redis를 연결하면 된다.
실행 방법
pip3 install -r requirements.txt
# .env 파일 생성
cp .env.example .env
# .env 열어서 BOT_TOKEN 입력
python3 main.py
터미널에 봇 시작... 로그가 보이면 Telegram에서 봇과 대화할 수 있다. Ctrl+C로 종료.
비교 테이블
| 항목 | python-telegram-bot | aiogram | telebot (pyTelegramBotAPI) |
|---|---|---|---|
| 비동기 | ✓ (v20+) | ✓ (기본) | 부분 지원 |
| 문서 | 풍부 | 풍부 | 보통 |
| 학습 곡선 | 낮음 | 중간 | 낮음 |
| 커뮤니티 | 가장 큼 | 활발 | 보통 |
| 한국 자료 | 많음 | 적음 | 보통 |
입문용으로는 python-telegram-bot이 가장 자료가 많고 문서도 잘 돼 있다.
삽질 기록
requests 대신 httpx 써야 하는 이유
처음에 날씨 API를 requests.get()으로 짰더니 봇이 응답할 때 전체가 짧게 멈추는 현상이 있었다. async 함수 안에서 동기 I/O를 호출하면 이벤트 루프가 블로킹된다. httpx.AsyncClient로 바꾸니 해결됐다.
query.answer() 호출 빠뜨리기
인라인 버튼 콜백에서 await query.answer()를 안 쓰면 Telegram 클라이언트에서 버튼 클릭 후 로딩 표시가 수십 초간 유지된다. 항상 콜백 핸들러 시작 부분에 넣어야 한다.
/start가 두 번 실행되는 것처럼 보이는 현상
run_polling() 시작 시 Telegram 서버에 쌓인 이전 업데이트들이 한꺼번에 처리된다. 개발 중 여러 번 /start를 보냈다면 봇 재시작 후 그 메시지들이 몰려올 수 있다. run_polling(drop_pending_updates=True) 옵션으로 시작 시 밀린 업데이트를 무시할 수 있다.
마무리
Telegram 봇의 핵심은 핸들러 등록 패턴이다. 어떤 메시지/명령어가 왔을 때 어떤 함수를 실행할지 선언하면 나머지는 라이브러리가 처리한다. 날씨, 알림, 파일 전송, DB 연동 등 뭐든 붙일 수 있다.
24시간 돌리려면 서버(VPS, Railway, Fly.io 등)에 배포하거나, Webhook 방식으로 전환하면 된다. Polling 방식은 개발 단계에서만 쓰고 운영에선 Webhook이 더 효율적이다.
기술 스택: Python 3.x · python-telegram-bot 21.6 · httpx · python-dotenv · Open-Meteo API
소스 코드: GitHub
'튜토리얼 > 자동화' 카테고리의 다른 글
| [n8n + Make] 블로그 포스팅 자동화 — Markdown push → 티스토리 자동 발행 (0) | 2026.03.05 |
|---|---|
| [Python + Ollama] API 키 없이 PDF 요약 자동화 스크립트 (0) | 2026.03.05 |
| [Python] BeautifulSoup으로 웹 스크래퍼 만들기 — 파싱, 페이지네이션, CSV 저장 (0) | 2026.03.04 |
| [Python] openpyxl로 엑셀 보고서 자동 생성하기 — 스타일링, 수식, 차트까지 (0) | 2026.03.03 |
| [Python] 유튜브 자막 추출기 만들기 (0) | 2026.02.28 |