본문 바로가기
개발노트/기타 개발

텔레그램 봇 만들기 (3) - koyeb에 24시간 봇 올려두기

by 정느 2025. 3. 28.

좌충우돌 끝에 나온 메인함수.

지난 글에 이어서 텔레그램 봇 설정을 이것저것 하고 나서, 서버에 올려두려 하니

생각보다 비용이 많이 나간다. 나 혼자 써서 트래픽도 얼마 안되는데...? 무료로 될거같은데?

해서 이것저것 찾아보다가 (railwayapp, 오라클 무료인스턴스. 파이선에니웨어 등등)
2025년 3월 기준 koyeb이 가장 적당한 것 같아서 여기로 선택.

 

단 여기도 그냥 백그라운드로 돌릴 순 없어서

fastAPI와 uvicorn을 달아 꼼수를 사용했다.

서버는 그냥 켜놓기만 하고 실제 본체는 텔레그램 봇. 아래는 최종 결과물이다.

app = FastAPI()

@app.get("/health")
async def health_check():
    return {"status": "ok"}  # HTTP 200 응답을 반환

@app.get("/")
async def read_root():
    return {"message": "Bot is running!"}

 

코드 위쪽에 이렇게 설정

이렇게 해두면 루트( https://~~~~.com/ )  에서는 Bot is running! 이라고 뜨고
https://~~~~.com/health 에서는 ok 라고 뜬다.

# 비동기 방식으로 main 함수 정의
async def main() -> None:
    print("Bot run...")
    application = Application.builder().token(token).build()

    # 알람 등록
    await load_and_schedule_all_alarms()

    # 핸들러 등록
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
    application.add_handler(CommandHandler("start", start))
    application.add_handler(CommandHandler("help", help_command))

    # 알람 관련 핸들러
    application.add_handler(CommandHandler("addAlarm", add_alarm))  # 알람 설정
    application.add_handler(CommandHandler("listAlarm", list_alarms))  # 등록된 알람 확인
    application.add_handler(CommandHandler("deleteAlarm", delete_alarm))  # 알람 삭제
    application.add_handler(CommandHandler("modifyAlarm", modify_alarm))  # 알람 수정
    application.add_handler(CommandHandler("aA", add_alarm))  # 알람 설정
    application.add_handler(CommandHandler("lA", list_alarms))  # 등록된 알람 확인
    application.add_handler(CommandHandler("dA", delete_alarm))  # 알람 삭제
    application.add_handler(CommandHandler("mA", modify_alarm))  # 알람 수정

    # 채널 알람 관련 핸들러
    application.add_handler(CommandHandler("channelAlarm", channel_alarm))  # 채널 알람 설정
    application.add_handler(CommandHandler("listChannelAlarm", list_channel_alarm))  # 채널 알람 목록 확인
    application.add_handler(CommandHandler("deleteChannelAlarm", delete_channel_alarm))  # 채널 알람 삭제
    application.add_handler(CommandHandler("ca", channel_alarm))  # 채널 알람 설정
    application.add_handler(CommandHandler("lca", list_channel_alarm))  # 채널 알람 목록 확인
    application.add_handler(CommandHandler("dca", delete_channel_alarm))  # 채널 알람 삭제    

    # 멤버 관련 핸들러
    application.add_handler(CommandHandler("addMember", add_member))  # 멤버 추가
    application.add_handler(CommandHandler("deleteMember", delete_member))  # 멤버 삭제
    application.add_handler(CommandHandler("listMembers", list_members))  # 멤버 리스트
    application.add_handler(CommandHandler("lm", list_members))  # 멤버 리스트2
    application.add_handler(CommandHandler("searchMember", search_member))  # 멤버 검색
    application.add_handler(CommandHandler("addMembers", add_multiple_members))  # 멤버 여러명 추가
    application.add_handler(CommandHandler("moveMember", move_member))  # 멤버 순서 변경

    # 기타 핸들러
    application.add_handler(CommandHandler("chosung", chosung))
    application.add_handler(CommandHandler("time", current_time))
    application.add_handler(CommandHandler("debug", debug))
    
    # 폴링 시작
    await application.run_polling()  # 비동기로 실행됨
    # Graceful shutdown 처리 (필요시)
    await application.shutdown()  # 종료 시 shutdown 호출

async def start():
    port = int(os.environ.get("PORT", 8000))  # Koyeb에서는 환경 변수를, 로컬에서는 8000 사용
    config = uvicorn.Config("main:app", host="0.0.0.0", port=port, reload=True)
    server = uvicorn.Server(config)
    print(config)
    print("server on")
    
    await asyncio.gather(
        main(),  # 텔레그램 봇 비동기 작업 실행
        server.serve()        # uvicorn 서버 실행
    )

if __name__ == '__main__':
    # 이미 실행 중인 이벤트 루프가 있을 때, 새로운 루프를 생성하지 않도록 처리
    nest_asyncio.apply()  # 이미 실행 중인 이벤트 루프에서 비동기 코드 실행 허용
    asyncio.run(start())

 

알람 형태로 봇을 사용하려다 보니 내 소스의 메인함수는 뭐가 덕지덕지 많다

아래 async def start(): 여기 부분에서

config = uvicorn.Config("main:app", host="0.0.0.0", port=port, reload=True)

 

이부분에서 "main:app" 의 왼쪽 main은 내 파이썬 메인파일 이름이 되어야 한다.

필자의 경우 main.py였기에 main:app이라고 쓴 것.

만약 메인파일 이름이 mainfileimhahaha 라면

mainfileimhahaha:app 이라고 써야함

 

로컬에서 테스트 할 때는 host="127.0.0.1" 로 할 것

 

port = int(os.environ.get("PORT", 8000))  # Koyeb에서는 환경 변수를, 로컬에서는 8000 사용

 

이부분은 koyeb에서 환경변수를 받아올 때 환경변수가 있으면 읽기, 없으면 8000 사용토록 설정한 것.

 

 

 

그럼 대충 업로드까지 좌충우돌 막혔던 부분을 적어본다

 

1. github private로 프로젝트 생성

깃을 안쓰고 하려고 했는데, 이거만한게 없다... 보안이 문제가 된다고 생각하면 private로 하자.

이 게시물을 보는 분들은 대부분 개인프로젝트를 하는 분들일테니 혼자서나 소규모로 할 땐 private도 무료제공 된다.


깃 쓰는 방법은 인터넷에 많으니 패스.

나는 테스트버전 없이 바로 마스터로 푸쉬했기에
git add .

git commit -m "블라블라"

git push origin master

이 세개 명령어만 썼다.

봇 토큰 같은 정보는 깃에 업로드할때 올라가면 좀 곤란하므로
.env 파일을 만들어서 설정.
이건 로컬 테스트할때 쓰는 파일이고
실제 업로드는 안되므로 서버에 올릴 때 이 정보들은 따로 환경변수로 등록하자.

 

 

나같은경우는 koyeb의 환경변수와 스타일을 맞춰야 해서

기본은 스트링으로,

배열의 경우는 "~~~~, ~~~~~, ~~~~~" 식으로 콤마로 구분해서 넣었다.

 

admin_chat_id = int(admin_chat_id_str) if admin_chat_id_str else None
ALLOWED_CHANNEL_IDS = [int(x) for x in allowed_channel_ids_str.split(",")]
ALLOWED_USER_IDS = [int(x) for x in allowed_user_ids_str.split(",")]
token = os.getenv("TELEGRAM_BOT_TOKEN")

 

불러올땐 이렇게 불러왔다.

 

그리고 .env파일이 깃 푸쉬에 포함되면 안되므로

.gitignore 파일을 만들어서

안에 내용에 '.env'를 넣어주자

 

이거 말고도 폴더 안에 있는 것 중에 업로드가 안됐으면 싶은 것들 파일 이름을 엔터로 구분해서 다 넣어주면 된다.

 

대충 코드수정 다 했고 서버에 올릴 준비 해야지 싶으면

git add .

를 치면 바뀐 애들이 다 add가 된다.

 

이 상태에서 git commit -m "커밋 이유 블라블라" 를 해주면

commit상태가 되고 푸쉬 준비 끝

 

git push origin master

이런식으로 푸쉬해주면 된다.

푸쉬가 안되고 뭐 충돌 어쩌구 하면 pull 받아와서 코드 맞추고 뭐시기 뭐시기... 귀찮아진다.

혼자 프로젝트 하면 꼬일 일이 없지만 여러명이 하면 종종 이런 일이 발생한다. 

 

2. requirements.txt 생성

로컬 테스트는 이쯤부터 어지간한건 다 잘 됐는데

서버에 업로드하려니 문제가 생겼다.

아니 테스트만 잘되면 뭐하나 서버에 올려야지!!!

 

일단 내 봇의 워크플레이스 폴더에서 (예를 들면 c:\user\username\TEST 같은 폴더)
pip freeze > requirements.txt 명령어를 사용하여
서버에 설치해야 하는 목록들을 만들어둔다.

문제는 서버 환경에 따라서 이게 설치가 되기도 하고 안되기도 하고. 그런다는거다.

난 내 개인컴이 윈도우인데 서버컴은 리눅스여서

윈도우 전용 라이브러리가 requirements.txt에 포함되어있는것 때문에 오류가 뜨고 그랬다.

 

나같은경우는 그냥 설치로는 자꾸 오류가 떠서 Dockerfile을 만들어서 설치하게 했다.

chatgpt한테 도커파일 만들어달라고 하니까 그냥 슥 만들더라. 요즘 세상 좋다.

# Step 1: Python 베이스 이미지 설정
FROM python:3.9-slim

# Step 2: 의존성 설치
RUN pip install --upgrade pip && \
    apt-get update -y && \
    apt-get install -y \
    openjdk-17-jdk-headless \
    build-essential \
    python3-dev \
    libffi-dev \
    && apt-get clean

# Step 3: JAVA_HOME 환경 변수 설정
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
ENV PATH=$JAVA_HOME/bin:$PATH

# Step 4: 작업 디렉토리 설정
WORKDIR /app

# Step 5: requirements.txt 복사 (최적화를 위해 먼저 복사)
COPY requirements.txt .

# Step 6: 의존성 설치
RUN pip install --no-cache-dir -r requirements.txt

# Step 7: 애플리케이션 코드 복사
COPY . .

# Step 8: 애플리케이션 실행
CMD ["python", "main.py"]

 

자바 환경변수 설정이랑 이런부분 오류가 계속 생겨서
dockerfile을 이런 식으로 만들었다.

서버 설치 과정에서 오류 메세지 확인해서 거기에 맞춰서 도커파일을 잘 설정하면 될 듯 싶다.

 

3.koyeb으로 웹서비스 설정

무료 인스턴스는 워싱턴쪽 서버만 되는듯

백그라운드 서비스는 무료제공을 안하기에,

꼼수로 웹서비스로 대충 /index 페이지랑 /health 페이지

이렇게 두개만 열어놓고

실제로는 봇이 백그라운드에서 돌아가도록 코드를 짜놨던 것.

 

환경변수 설정 해주고

 

웹서비스다보니 이거 최소 하나는 열어놔야된다.

8000포트로 http 하나 열어두자. 아까 그 app에서 '/'랑 '/health' 설정해둔게 요거때문

 

서비스가 살아있는지 확인하는건데 필자는 대충 이렇게 해놨다

설정해두고 적절하게 잘 살아있는거 확인했으니 이제 집컴을 꺼도 되겠다.

서버역할 대신 하느라 고생했어.

 

아무튼 koyeb 말고

iwinv나 기타등등 국내업체에서도 월 만원 이내로 서버대여를 싸게 해준다.

규모있게 쓰실 분들은 적절한 곳 가격을 알아보거나 아니면 회사 서버컴퓨터를 더 혹사시켜보자.

 

 

이번에 텔레그램 봇생성 관련 메뉴얼을 정리해 둘 일이 생겨서

기념으로 이것저것 만들어봤는데

이틀동안 재밌게 놀았던 것 같다

 

그럼 다들 즐겁게 코딩하시길 바라며 이만 슝

댓글