6 min read

강좌신청 봇 개발 완료

강좌신청 봇 개발 완료
Photo by nameless 3791 / Unsplash "울산 방어진"

개발 까지 걸린시간 총 5시간+ 테스트 시간?시간

클로드 - 개발VM 서버 - 도커 스웜 이 3개 조합으로 만 완성

제일 오류가 많이 발생한 구간이 대기열 시스템 이었다. 대기열 동안 찾지 못하면 바로 에러를 발생시켰는데 이 구간이 가장 오류가 많이 났다.

참! 이것은 직접하지 않고 자동으로 해준다는 이점 밖에는 없다.

만든이유....가끔씩 깜빡해서 등록 시간을 놓칠때가 많기 때문이다.

수영 강좌 등록 전쟁, 봇으로 끝냈다 — 울주봇 개발기

매달 21일 오전 10시. 울주군국민체육센터 수영 강좌 신청이 열리는 시간이다.

새로고침을 연타해도 "만석"이 뜨는 건 순식간이다. 손이 느린 게 아니다. 이미 수십 명이 같은 화면 앞에 있었던 것이다. 몇 번 이 경험을 반복하고 나서 생각했다. "이거 자동화할 수 있지 않나?"

그렇게 울주봇이 시작됐다.


왜 Playwright였나

처음엔 단순하게 생각했다. HTTP 요청 분석해서 requests로 직접 때리면 되지 않을까. 근데 해당 사이트는 세션 처리, 폼 토큰, 동적 렌더링이 얽혀 있어서 단순 HTTP 요청으로는 한계가 있었다.

결국 실제 브라우저를 통째로 자동화하는 방향으로 갔다. Playwright + Chromium 조합이다.

Playwright를 선택한 이유는 간단하다:

  • Python 지원이 성숙해 있고
  • async/await 기반이라 타이밍 제어가 편하고
  • headless 모드에서도 안정적으로 동작한다

핵심 구현: DOM 폴링으로 대기열 붙잡기

등록 시간 전에 미리 로그인해서 강좌 페이지에 진입해 놓는다. 그리고 신청 버튼이 활성화되는 순간을 1초 간격 DOM 폴링으로 감지해서 즉시 클릭한다.

핵심은 페이지를 새로고침하지 않는다는 것이다. 새로고침하면 세션이 끊기거나 대기 순서가 밀릴 수 있다. 이미 열려 있는 페이지에서 DOM 변화만 감지하는 방식으로 대기열 이탈 없이 처리했다.

# 개념적 흐름 (실제 코드 단순화)
while True:
    button = await page.query_selector("신청버튼 selector")
    if button and await button.is_enabled():
        await button.click()
        break
    await asyncio.sleep(1)

오픈 시간 PRE_LOGIN_MINUTES분 전에 미리 로그인하고, MAX_WAIT_MINUTES분까지 대기하는 구조다. 두 값 모두 환경변수로 조정 가능하다.


봇 감지는 없었나

결론부터 말하면 없었다. 별도의 User-Agent 조작이나 딜레이 트릭 없이도 정상 동작했다. 공공기관 사이트 특성상 봇 감지 시스템이 없는 것으로 보인다.

다만 폴링 간격은 1초 이상으로 유지하고 있다. 서버에 불필요한 부하를 주지 않기 위해서다.


왜 Flask + SQLite인가

웹 UI가 필요했다. 강좌 등록, 스케줄 설정, 실시간 로그 확인을 브라우저에서 하고 싶었다.

무거운 스택은 필요 없었다. 관리자 혼자 쓰는 도구니까. Flask + SQLite 조합으로 충분했고, 배포도 단순해진다.

비밀번호는 bcrypt로 암호화해서 SQLite에 저장한다. 울주군 사이트 계정 정보가 들어가는 만큼 이 부분은 신경 썼다.


Docker로 컨테이너화

Playwright + Chromium을 포함하면 이미지가 꽤 무겁다. 처음에 인스턴스를 3개 띄워봤는데 서버가 버티질 못했다. 결국 1개 인스턴스로 운영하고 있다. 어차피 신청은 순차적으로 하면 되니까.

Docker Swarm 환경에서 stack.yml로 배포하고, Caddy 리버스 프록시로 도메인 연결했다. SECRET_KEY는 Docker Secret으로 주입한다.

secrets:
  - uljubot_secret_key
environment:
  - MAX_WAIT_MINUTES=20
  - PRE_LOGIN_MINUTES=10

Caddy 쪽은 라벨만 붙여주면 끝이다:

labels:
  caddy: "uljubot.yourdomain.com"
  caddy.reverse_proxy: "{{upstreams 5000}}"

텔레그램 알림 연동

등록 성공/실패 결과를 텔레그램 봇으로 받는다. 봇이 혼자 돌아가는 동안 결과를 실시간으로 확인할 수 있어서 편하다.

지불 대기 상태가 2시간 넘어가면 알림 보내는 기능도 로드맵에 있다 (v1.4).


현재 기능과 앞으로 계획

v1.0 현재 동작하는 것들:

  • 관리자 로그인 및 계정 관리
  • 울주군 사이트 계정 다중 등록
  • 즉시 등록 / 예약 시간 실행 선택
  • 만석 시 자동 재시도
  • 마이페이지 등록 여부 확인
  • 대시보드 실시간 로그
  • DOM 폴링 방식 대기열 유지

앞으로 추가할 것들:

  • v1.1: 미접수 강좌 1분마다 자동 재등록
  • v1.2: 결제 대기 상태 모니터링 + 시간 초과 알림
  • v1.3: 멤버 추가 기능 (관리자 승인 방식)
  • v1.5: 강좌 신청 기간에만 봇 활성화 + 매달 21일 안내 알림

마치며

만들고 나서 첫 달에 바로 수영 강좌 등록에 성공했다. 오전 10시에 커피 마시면서 텔레그램 알림 받은 순간, 꽤 뿌듯했다.

코드 자체는 어렵지 않다. Playwright의 DOM 폴링, Flask의 스케줄러 통합, Docker 배포 — 각각은 단순하지만 조합하면 실생활에서 쓸 수 있는 도구가 된다.

기술 스택 정리하면 이렇다:

역할 기술
웹 서버 Python Flask
DB SQLite
비밀번호 암호화 bcrypt
봇 엔진 Playwright (Chromium)
컨테이너 Docker / Docker Swarm
알림 Telegram Bot API
리버스 프록시 Caddy + caddy-docker-proxy

개인 도구로 만들었지만, 이 구조는 다른 공공 예약 사이트나 소상공인 자동화 작업에도 그대로 응용할 수 있다. 다음에 비슷한 걸 만들게 되면 그것도 정리해볼 생각이다.