본문 바로가기
튜토리얼/자동화

[Python] Telegram 봇 만들기 — 명령어, 날씨 API, 인라인 버튼, 메모

by 루까(Luka) 2026. 3. 4.
반응형

왜 만들었나

알림, 날씨 확인, 간단한 메모처럼 반복적으로 하는 작업이 있다. 이걸 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 메모 전체 삭제
일반 텍스트 에코 (그대로 반환)

봇 토큰 발급

  1. Telegram에서 @BotFather 검색
  2. /newbot 입력 → 봇 이름, 사용자명 설정
  3. 발급된 토큰을 .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.AsyncClientasync 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

반응형