밴픽후닫 알림 봇 만들기: API와 데이터 활용
프로 경기에서 밴픽이 끝나면 많은 것이 동시에 움직인다. 중계 화면은 스폰서 영상으로 넘어가고, 분석 데스크가 마지막 포인트를 정리하고, 일부 플랫폼은 배팅 마감을 누른다. 이 시점을 놓치지 않고 받아서 알림으로 전달하는 효용은 생각보다 크다. 내부 데이터 파이프라인을 기민하게 맞물리게 할 수도 있고, 커뮤니티에 즉시 정보를 전파할 수도 있다. 롤토토나 롤배팅 실시간 사이트처럼 밴픽후닫, 밴픽후마감 시간을 기준으로 상태를 바꾸는 곳이라면 자동 알림으로 운영 부담을 크게 덜 수 있다.
이 글은 밴픽 종료 이벤트를 안정적으로 포착하고, 알림으로 배포하는 봇을 처음부터 설계하고 구현하는 과정을 다룬다. 실전에서 겪은 지연, 엣지 케이스, 데이터 출처의 신뢰도 같은 지점을 구체적으로 짚는다. 언급하는 API와 구조는 리그 오브 레전드 생태계를 기준으로 하지만, 원리는 다른 종목에도 쉽게 이식된다.
밴픽 종료라는 이벤트를 기술적으로 정의하기
사람에게 밴픽 종료는 직관적이다. 마지막 픽이 잠기고, 양팀의 챔피언이 확정되면 끝이다. 시스템 입장에서는 다음과 같이 더 세밀하게 정의해야 한다.
데이터 소스에서 한 게임의 draft 상태가 locked 또는 ready 로 바뀐다. 모든 챔피언 슬롯이 할당되었고, 후속 상태가 게임 시작 전 대기 상태로 전이된다. 중계 소스에서는 draft overlay가 사라지고 인게임 로딩 화면으로 넘어간다.
이 중 하나만으로는 충분하지 않을 때가 많다. 예를 들어 쇼 매치나 서드파티 대회는 UI가 다르거나 데이터가 끊길 수 있다. 실전에서는 두 가지 신호를 합쳐서 신뢰도를 높이고, 타임아웃을 두어 비정상 상태를 처리한다. 밴픽 종료 알림은 늦으면 가치가 떨어지지만, 잘못된 알림은 신뢰 자체를 깎아먹는다.
어떤 데이터로 잡을 것인가: 공식, 준공식, 보정 신호
실시간 이벤트를 만들 때 가장 먼저 결정할 것은 데이터 소스다. 지연, 정확도, 안정성, 약관 준수, 유지보수 난이도의 균형을 맞춰야 한다. 아래는 현업에서 자주 쓰는 경로를 간략히 비교한 것이다.
LoL Esports 공개 엔드포인트: lolesports.com이 제공하는 이벤트, 스케줄, 라이브 피드. 공식 중계와 동기화되어 있고 지연이 짧다. 경기별 토픽 ID를 얻으면 소켓 구독도 가능하다. 다만 공개 문서화가 제한적이며, 이벤트 스키마가 변할 수 있어 헬스 체크와 스키마 감지를 붙여야 한다. 토너먼트 운영사 API: 일부 대회는 별도 API를 내준다. 접근 권한이 필요하지만 품질이 안정적이고 SLA가 명확하다. 권한이 없으면 선택지가 되지 않는다. Data Dragon 및 일반 라이브 스펙테이트: 솔로큐 중심이라 프로 경기와 직접 연결되지 않는다. 보조 지표 정도로만 의미가 있다. 화면 기반 신호: 방송 화면의 draft overlay를 컴퓨터 비전으로 감지한다. 어디서나 쓸 수 있고 공급자에 덜 종속적이지만, 지연이 길고 해상도나 그래픽 변경에 취약하다. 예외 처리 비용이 크다.
실무에서는 첫 번째 경로를 기본으로 두고, 화면 기반 신호를 보정값으로 얹는 형태가 현실적이다. 특정 리그만 다루면 운영사 API가 최선이지만, 권한과 계약이 전제된다.
아키텍처 그림 그리기
알림 봇이라도 구조는 깔끔할수록 문제 추적이 쉽다. 지나치게 요란한 분산 시스템을 만들 필요는 없다. 다만 수집, 판별, 발행의 세 단계를 떼어놓기만 해도 장애가 국소화되고, 재시도 정책을 나눌 수 있다.
수집기: API를 폴링하거나 소켓으로 구독해 원시 이벤트를 받아온다. 라우 디스패처는 토픽과 매치 ID를 기준으로 메시지 큐에 적재한다. 상태기계: 한 게임의 상태를 draftstarted, drafting, draftlocked, pregame 으로 흘려보내며 전이 조건을 평가한다. 전이가 성립하면 idempotent 키를 만들어 발행한다. 발행기: Slack, Discord, Telegram, 웹훅, 내부 MQ 등으로 알림을 뿌린다. 구독자별 포맷팅과 레이트 리밋, 중복 억제를 처리한다. 저장소: 최근 N개 경기의 상태 스냅샷, 전이 로그, 알림 발송 결과를 적재한다. Redis 같은 인메모리 키밸류로 현재 상태를 관리하고, 장기 보관은 Postgres로 돌린다. 관측: 수집 지연, 전이 지연, 실패율, 큐 적체를 계량화한다. SLO를 알림 지연 p95 기준으로 잡으면 운영 감각이 선다.
규모가 작다면 한 프로세스에 담아도 된다. 다만 모듈 간 인터페이스를 분리해두면 나중에 확장할 때 비용이 작다.
LoL Esports 데이터 끌어오기
Lolesports.com은 토너먼트, 매치, 게임 단위로 정보를 제공한다. 공개 문서가 단출하지만, 다음 흐름으로 안정적인 파이프를 만들 수 있다.
1) 스케줄에서 오늘 경기를 가져온다. 토너먼트와 매치 ID를 매핑한다.
2) 각 매치의 게임별 토픽이나 라이브 엔드포인트를 찾는다.
3) 초반에는 폴링으로 안전하게 시작하고, 안정화가 되면 소켓 구독을 붙인다.
엔드포인트 예시는 환경에 따라 바뀌므로 여기서는 의사 코드로 흐름만 보여준다.
Import httpx From datetime import datetime, timezone BASE = "https://esports-api.lolesports.com/persisted/gw" HEADERS = "x-api-key": "<YOUR_KEY>" # 일부 엔드포인트는 키가 필요할 수 있다. Async def fetch_schedule(): Params = "hl": "ko-KR" Async with httpx.AsyncClient(timeout=10) as client: R = await client.get(f"BASE/getSchedule", headers=HEADERS, params=params) R.raise_for_status() Data = r.json()["data"] Return data["schedule"]["events"] Def extract_today_matches(events): Today = datetime.now(timezone.utc).date() Matches = [] For ev in events: Start = datetime.fromisoformat(ev["startTime"].replace("Z", "+00:00")) If start.date() == today and ev["type"] == "match": Matches.append( "match_id": ev["match"]["id"], "start": start, "block": ev.get("blockName"), "teams": [t["name"] for t in ev["match"]["teams"]] ) Return matches
게임 단위의 라이브 데이터는 토픽 ID나 gameId를 기반으로 접근한다. 실제 필드명은 리그와 시즌에 따라 조금씩 달라지니, 스키마 변화를 감지하는 가드가 필요하다. 필드가 빠지면 즉시 경고를 올리고 폴백 경로로 전환하는 식이다.
드래프트 상태 파싱과 전이 판단
드래프트의 상태는 보통 다음처럼 잡을 수 있다.
각 팀의 bans 배열 길이가 밴 수 기준과 일치한다. picks 배열 또는 slots가 모두 채워졌고 locked 플래그가 참이다. 새로운 이벤트가 draft_completed 또는 lock 이라는 타입으로 푸시된다.
이 신호 가운데 두 개 이상이 동시에 참이면 확신을 높인다. 반대로 30초 이상 업데이트가 없고, 마지막 상태가 drafting 이라면, 화면 기반 신호가 있더라도 완료로 취급하지 않는다. 운영 환경에서 자주 겪는 함정은 더블 라운드 로빈에서 동일한 매치 컨테이너 안에 여러 게임이 있는 경우다. GameNumber 기준으로 상태를 분리해 관리하는 습관이 중요하다.
간단한 상태기계의 예시는 다음과 같다.
From enum import Enum, auto From dataclasses import dataclass, field From typing import Dict, Any Class Phase(Enum): PENDING = auto() DRAFT_STARTED = auto() DRAFTING = auto() DRAFT_LOCKED = auto() PREGAME = auto() @dataclass Class GameState: Match_id: str Game_id: str Phase: Phase = Phase.PENDING Last_update_ts: float = 0.0 Picks: Dict[str, list] = field(default_factory=lambda: "blue": [], "red": []) Bans: Dict[str, list] = field(default_factory=lambda: "blue": [], "red": []) Locked: bool = False Def transition(state: GameState, payload: Dict[str, Any], now_ts: float): State.last_update_ts = now_ts # payload에서 picks/bans/locked 갱신 # 여기서는 예시로만 처리 State.locked = payload.get("locked", state.locked) State.picks = payload.get("picks", state.picks) State.bans = payload.get("bans", state.bans) If state.phase == Phase.PENDING and (state.picks["blue"] or state.picks["red"] or state.bans["blue"] or state.bans["red"]): State.phase = Phase.DRAFT_STARTED If state.phase in Phase.DRAFT_STARTED, Phase.DRAFTING: State.phase = Phase.DRAFTING If state.locked: State.phase = Phase.DRAFT_LOCKED If state.phase == Phase.DRAFT_LOCKED and all_complete(state): State.phase = Phase.PREGAME Def all_complete(state: GameState): # 예시 기준: 총 픽 10, 총 밴 10 Total_picks = len(state.picks["blue"]) + len(state.picks["red"]) Total_bans = len(state.bans["blue"]) + len(state.bans["red"]) Return total_picks == 10 and total_bans in (8, 10) # 리그별 밴 수 차이를 고려
실제 구현에서는 리그별 밴 수, 블루 레드 스왑, 리메이크로 인한 재드래프트를 반영해야 한다. 재드래프트는 동일한 gameId가 재사용되거나 새로운 gameId로 생성될 수 있다. 이때는 draftlocked 전이와 pregame 전이 사이의 시간을 기록해두고, 동일 매치에서 동일 양팀 조합으로 다시 draftstarted가 발생하면 이전 알림을 취소하거나 정정 알림을 내보내도록 한다.
화면 기반 보정: 써야 할 때와 쓰지 말아야 할 때
데이터가 빈약한 리그는 방송 화면 외에 선택지가 없을 때가 있다. 이 경우 드래프트 오버레이에서 픽, 밴 슬롯이 모두 채워지는 순간을 컴퓨터 비전으로 잡는다. 템플릿 매칭이나 OCR로 챔피언 아이콘을 파악하는 식이다. 간단한 매칭으로도 80% 수준의 정확도를 낸다. 하지만 테마가 바뀌거나 그래픽이 다르면 지표가 무너진다. 또, 스트리밍 지연과 프레임 저하로 알림이 10초 이상 늦어질 때가 있다.
그래서 화면 기반은 보정자로만 쓰는 편이 낫다. API 신호가 늦어질 때 보정으로 잠깐 대체하거나, API에 의심이 갈 때 이중 확인에 활용한다. 약관 위반을 피하려면 스크린샷 수집과 처리 방식, 저장 기간을 명확히 하고 상업적 이용 조건을 점검해야 한다.
통지 채널과 메시지 설계
알림은 구독자의 맥락에서 내용을 최소한으로, 타이밍은 최대한 빠르게 하는 쪽이 가장 효율적이다. 밴픽후마감 시점을 쓰는 곳이라면 다음 데이터가 핵심이다.
팀명, 세트 번호, 리그, 예정 시작 시각 확정된 챔피언 조합과 밴 목록 최초 드래프트 완료 시각과 지연 초 데이터 출처 신뢰도 레벨
슬랙과 텔레그램은 구현 난이도가 낮고 전파가 빠르다. 텔레그램 봇은 초대와 권한 제어가 쉬워 운영 부담이 적다. 반대로 다량의 퍼블릭 구독자에게는 푸시 폭주를 막기 위한 샤딩이나 주제별 채널 분리가 필요하다.
텔레그램으로 보내는 예시 코드는 아래처럼 단순하다.
Import httpx Import asyncio TELEGRAM_TOKEN = "<BOT_TOKEN>" CHAT_ID = "<CHAT_ID>" Async def send_tg(text): Async with httpx.AsyncClient(timeout=10) as client: Await client.post(f"https://api.telegram.org/botTELEGRAM_TOKEN/sendMessage", Json="chat_id": CHAT_ID, "text": text, "parse_mode": "HTML") Def fmt_message(match, game, blue_picks, red_picks, locked_at, source="Lolesports"): Return ( F"[match['league']] match['teams'][0] vs match['teams'][1] - Game game\n" F"밴픽 확정: locked_at:%H:%M:%S (src: source)\n" F"Blue: ', '.join(blue_picks)\n" F"Red: ', '.join(red_picks)" )
슬랙은 웹훅 인커밍 또는 앱을 사용하면 된다. 임베디드 이미지가 필요하면 챔피언 아이콘 CDN을 묶어도 된다. 과하면 알림 시인성이 떨어진다. 기본은 텍스트, 다음은 단색 배지 정도가 낫다.
구축 순서, 실무용 체크리스트 스케줄 수집과 매치-게임 매핑을 만든다. GameId 단위로 상태를 보관한다. 드래프트 상태 파서를 구현하고, 리그별 밴 수 차이를 설정으로 분리한다. 상태 전이 로직을 만들고, draft_locked 전이를 idempotent 키로 발행한다. 발행 채널을 붙이고, 중복 억제와 재시도, 레이트 리밋 정책을 넣는다. 관측 지표를 박고, p95 알림 지연과 실패율을 대시보드로 본다.
이 다섯 가지만 통과하면 초판은 쓸 만한 품질이 나온다. 이후에는 리메이크 대응, 소켓 전환, 다국어 템플릿, 장애 시 폴백 같은 확장을 넣는다.
폴링으로 시작해, 소켓으로 진화시키기
처음부터 소켓에 기대면 장애 원인을 좁히기 어렵다. 폴링은 예측 가능하고, 재시도 정책이 단순하며, 캐시 계층 뒤에서 움직여 트래픽이 완만하다. 초당 1회 이내 폴링이면 리그 일정 전체를 커버해도 비용이 낮다. 안정화되면 매치가 시작되면 소켓으로 전환하고, 유휴기에는 폴링으로 돌아오는 하이브리드가 효율적이다.
폴링 간격은 리그별로 나눈다. 티어 1 리그는 1초, 티어 2는 2초, 이외는 3초 같은 식으로 운영하면 쓸데없는 부하를 줄인다. 소켓은 연결이 끊어질 때 지수 백오프와 지연 측정값을 함께 로깅한다. 간헐적 끊김에 과민반응하지 않도록 최소 연속 실패 횟수 기준으로 경보를 울린다.
중복 알림과 순서 보장
한 번만 울려야 한다. 키는 리그, 매치, 게임 번호, 드래프트 버전으로 묶는다. 드래프트 버전은 리메이크나 재드래프트 대비용이다. 데이터 소스에서 제공하는 draftId가 있으면 최선이고, 없으면 locked 시각과 픽 조합의 해시로 대체한다. 알림 발행 전에 Redis SET에 키를 넣어 중복을 막고, 만료 시간은 하루 정도로 잡으면 깔끔하다.
순서도 중요하다. 같은 경기에서 밴픽후닫 알림이 나오기 전에 경기 시작 알림이 나가면 혼란스럽다. 큐에서 토픽별로 순서를 유지하거나, 컨슈머 쪽에서 이전 단계 알림 유무를 확인해 순서를 강제한다.
지연, 얼마나 잡을 수 있나
현실적인 p95 목표는 2에서 5초 사이다. 라이브 피드가 잘 작동하면 1초 이내도 어렵지 않다. 화면 보정을 섞으면 3초 내외가 보통이다. 메트릭을 분리해보면 병목은 세 군데에서 나온다.
데이터 소스 지연: 경기 운영의 변동성이 있어서 우리가 통제할 수 없다. 폴백 경로를 준비해 완충한다. 네트워크 왕복: 멀티 리전 배포로 줄일 수 있지만, 과한 최적화는 비용 대비 효과가 낮다. 내부 큐 적체: 발행 폭주를 레이트 리밋과 배치 전송으로 완화한다.
모니터링에는 locked 감지 시각과 발행 완료 시각의 차이를 기록해둔다. 발행 실패 재시도는 지연 통계에 포함시키고, p99에서 뾰족하게 튀는지 본다. 통계가 예쁘면 실제 사용자 체감도 예쁘다.
실패 전략과 폴백
수집 실패가 30초 이상 이어지면 폴백으로 전환한다. 폴백 순서는 소켓에서 폴링, 폴링에서 대체 엔드포인트, 그마저도 실패하면 화면 기반 보정이다. 폴백이 시작되면 알림 메시지에 신뢰도 레벨을 표기한다. 예를 들어 [BETA] 또는 [Unverified] 꼬리표를 붙여 구독자 스스로 리스크를 판단하게 하는 방식이다.
또한 재드래프트나 취소 같은 예외가 발생하면 정정 알림을 허용한다. 너무 자주 정정하면 피로감을 키우니, 최초 발행 이후 3분 이내, 동일 매치 안에서만 허용하는 식으로 제한한다.
포맷팅, 현장에서 먹히는 디테일
문장을 짧게, 팀명과 세트 번호를 앞에 둔다. 숫자는 한글보다 눈에 먼저 들어온다. 팀명 약어가 익숙한 리그라면 약어를 쓰고, 그렇지 않다면 풀네임을 유지한다. 챔피언 리스트는 5개가 넘어가면 줄바꿈으로 가독성을 확보한다. 타임스탬프는 현지 시각과 UTC를 함께 보여주면 글로벌 구독자에게 편하다.
챔피언 이름 표기는 현지화와 버전 차이가 있다. Data Dragon의 ko_KR 이름과 방송 자막이 다를 때가 있다. 사용자 피드백을 받아 매핑 테이블을 따로 관리하는 편이 낫다.
운영 자동화와 비용
규모가 커질수록 사람이 개입하는 지점은 줄여야 한다. 배포는 블루 그린으로, 설정은 리모트 키밸류에서 가져오도록 한다. 레이트 리밋, 타임아웃, 리그 우선순위 같은 조정값을 코드 없이 바꿀 수 있어야 한다. 비용은 대부분 네트워크와 메시지 전송에서 발생한다. 텔레그램과 슬랙은 무료 구간이 넓어 초기에는 부담이 거의 없다. 클라우드 트래픽은 초당 1회 폴링 기준으로도 미미하다. 이미지 임베딩이나 장문 메시지는 트래픽을 급격히 키우니 꼭 밴픽후닫 https://xn--9j1b29om3uymb.isweb.co.kr/ 필요할 때만 쓴다.
법과 윤리, 그리고 맥락
API와 화면 소스를 사용할 때는 약관을 반드시 확인한다. 상업적 이용, 재배포, 캐시 보존 기간 같은 조항을 지키지 않으면 운영 중단 리스크가 크다. 또, 밴픽후닫이나 밴픽후마감 같은 알림은 민감한 맥락을 가진다. 일부 지역에서는 롤배팅 관련 행위가 규제 대상일 수 있다. 서비스 배포 대상 국가의 법률과 플랫폼 정책을 확인하고, 알림의 용도와 책임 범위를 명확히 안내하는 것이 안전하다. 롤배팅 실시간 사이트와의 연동은 기술적으로 간단하지만, 법적 제약과 사용자 보호 원칙을 먼저 세워야 한다.
테스트, 회귀, 회상
실전에서 제일 잘 먹히는 테스트는 회상 테스트다. 지난 시즌의 경기 로그를 가져와 리플레이하듯 파이프를 흘려보내고, 우리가 포착한 locked 시점과 실제 방송의 전환 시점을 비교한다. 지난달, 지난 주, 어제의 데이터를 각각 흘려보내면 회귀 결함을 빨리 건진다. 스키마 변화에 대비해 JSON 스냅샷을 보관하고, 파서가 모르는 필드가 들어오면 즉시 경고를 올리도록 한다.
부하 테스트는 발행 채널 기준으로 한다. 알림이 동시에 수백 건 몰릴 일은 드물지만, 리그가 겹치는 주말 프라임 타임에는 일시적 스파이크가 생긴다. 이때 큐가 버틸 수 있는지, 재시도 폭이 과도하지 않은지 점검한다.
다국어, 접근성, 커뮤니티
해외 리그를 다룰수록 언어 장벽이 있다. 메시지 템플릿을 ICU나 간단한 포맷 스트링으로 빼서 언어팩을 교체할 수 있게 하자. 팀명과 선수명은 고유명사라 번역하지 말고 표기만 통일한다. 접근성 면에서는 이모지 남발을 피하고, 색에 의존하는 표현을 줄이는 것이 낫다. 커뮤니티에서 피드백을 받되, 알림 빈도와 시간대를 조절할 수 있는 개인 설정을 제공하면 만족도가 오른다.
예산이 적을 때의 현실적인 구성
단일 리전, 단일 VM, 단일 프로세스로 시작해도 된다. 파이썬 비동기 폴링, Redis, 텔레그램만으로도 1초대 알림이 가능하다. 로그는 텍스트 파일보다는 관리형 로그 수집기에 보내고, 메트릭은 프로메테우스와 그라파나 조합이면 충분하다. 장애 알림은 운영 채널로 들어오되, 중복을 줄이기 위해 임계치를 꽉 조인다.
간단한 FastAPI 기반의 서비스 뼈대는 아래처럼 시작할 수 있다.
From fastapi import FastAPI Import asyncio Import time App = FastAPI() Running = True @app.on_event("startup") Async def start(): Asyncio.create_task(poller()) @app.get("/health") Def health(): Return "ok": True Async def poller(): While running: Start = time.time() Try: # 1) 일정 업데이트 # 2) 라이브 상태 수집 # 3) 상태 전이 평가 # 4) 알림 발송 Pass Except Exception as e: # 로깅 Pass Finally: Elapsed = time.time() - start Await asyncio.sleep(max(0, 1.0 - elapsed))
여기에 Redis로 중복 억제, 텔레그램 발송, 간단한 상태기계를 붙이면 알림 봇으로 동작한다. 나중에 큐와 소비자 워커를 떼어내면 확장도 쉽다.
마무리의 판단 기준
좋은 밴픽후닫 알림 봇은 세 가지가 분명하다. 첫째, 언제 알리고 언제 알리지 않을지 기준이 선명하다. 둘째, 데이터 소스의 변덕을 폴백과 관측으로 흡수한다. 셋째, 메시지가 짧고 쓰임새가 분명하다. 기술은 도구일 뿐이다. 사용자가 이 알림으로 무엇을 더 잘하게 되는지가 분명하면, 지연을 줄이는 최적화도, 장애에 흔들리지 않는 설계도 방향을 잃지 않는다.
밴픽은 게임의 절반을 정한다. 그 절반이 닫히는 순간을 정확히 포착해 건네주는 일은, 생각보다 많은 사람의 다음 행동을 덜 번거롭게 만든다. 알림이 제때 울리면 운영자가 덜 뛰고, 시청자가 더 똑똑해지며, 시스템이 신뢰를 얻는다. 그 신뢰가 결국 서비스의 수명을 늘린다.