주말에 무슨 경기가 있는지 확인하려고 탭을 다섯 개씩 열고 있었다. K리그는 프로축구연맹, K3·K4는 축구협회, 야구는 또 따로. 그래서 한국에서 볼 수 있는 경기를 한 화면에 날짜별로 모으는 도구를 직접 만들었다. 축구 7개 대회와 KBO까지 1,496경기가 들어갔고, 하루 두 번 알아서 갱신된다. 이 글은 그걸 만들면서 막혔던 지점들과, 코딩 에이전트에게 어떻게 시켰는지를 남긴 기록이다.
목차
- 출발점 — 탭을 다섯 개 열고 있었다
- 도구 선정과 계획
- 만든 과정 — 막힌 곳들
- 검사기가 통과시킨 버그
- 자동 갱신 — 하루 두 번
- 예매 링크 — 되는 것과 안 되는 것
- 어떻게 시켰나 — 프롬프트로 진행한 과정
- 한계와 아쉬움
- 결과물과 다음
출발점 — 탭을 다섯 개 열고 있었다
이번 주말에 볼 만한 경기가 뭐가 있나 확인하려면 매번 이랬다. K리그1·2는 한국프로축구연맹 사이트, K3·K4는 대한축구협회 사이트, 야구는 KBO 사이트. 게다가 각 사이트는 자기 리그만 보여준다. “이번 토요일 저녁에 뭐 있지”라는 질문에 답하려면 세 곳을 왔다 갔다 해야 했다.
특히 K3·K4는 정보 찾기가 유독 어렵다. 집 근처에 K4 팀이 있는데도 언제 홈경기를 하는지 알기가 번거로웠다. 그래서 만들기로 한 건 딱 하나였다. 날짜를 고르면 그날 있는 경기가 리그 구분 없이 전부 나오는 화면. 거기에 경기장까지 같이 보이면 끝이다.
도구 선정과 계획
어디서 데이터를 가져올까
제일 먼저 확인한 건 “데이터를 긁어올 수 있느냐”였다. 여기서 갈렸다.
- K리그1·2 — 연맹 사이트가 일정을 JSON으로 주는 내부 API가 있었다. 경기장, 중계사, 관중수, 심판까지 들어 있는 넉넉한 응답이었다.
- K3·K4 — 축구협회 통합 시스템인
joinkfa.com이 Nexacro라는 옛날 기업용 프레임워크로 만들어져 있었다. 화면이 전부 JS로 그려지고 통신도 자체 규격이라 사실상 뚫기 어려웠다. 대신kfa.or.kr이 라운드별 HTML 조각을 그대로 돌려준다는 걸 발견해서 그쪽으로 우회했다. - KBO — 공식 사이트에 월별 일정을 주는 엔드포인트가 있었다.
A안으로 먼저, B안은 나중에
처음에 에이전트가 제안한 선택지는 세 개였다. ①정적 웹페이지 ②수집기+자동갱신 풀세트 ③로컬 앱. 셋의 차이를 물어보니 ①과 ②는 갈림길이 아니라 같은 길의 1단계·2단계라는 답이 왔다. 그래서 먼저 눈에 보이는 걸 만들고, 수집이 안정적인 걸 확인한 뒤 자동화를 얹기로 했다. 결과적으로 이 순서가 맞았다. 1단계에서 파싱 함정을 다 걸러낸 뒤에 자동화를 붙였더니 자동화 단계에서는 데이터 문제가 하나도 안 나왔다.
만든 과정 — 막힌 곳들
415 — 형식이 틀렸다
연맹 API를 처음 찔렀을 때 돌아온 건 HTTP 415 Unsupported Media Type이었다. 흔히 하듯 form 형식으로 보냈는데, 이 API는 JSON 본문만 받았다. 형식만 바꾸니 바로 열렸다.
form-urlencoded → 415 Unsupported Media Type
application/json → 200 성공
1라운드가 7경기인 이유
K리그1은 12팀이니 한 라운드에 6경기여야 한다. 그런데 1라운드만 7경기로 잡혔다. 열어보니 전북이 두 번 나왔다. 알고 보니 응답에 슈퍼컵이 섞여 있었다. 리그 일정을 달라고 했는데 컵대회가 같이 온 것이다. 응답 안의 대회 이름으로 걸러내서 해결했다.
이건 운이 좋았다. “12팀이면 6경기”라는 걸 알고 있었으니 숫자가 안 맞는 걸 눈치챈 거지, 모르고 지나갔으면 그대로 배포됐을 버그다.
같은 번호인데 다른 대회
코리아컵과 AFC 챔피언스리그를 붙일 때가 제일 헷갈렸다. 컵대회는 meetSeq라는 번호로 지정하는데, 2번으로 요청했더니 “포항 vs 감바 오사카”가 코리아컵으로 들어왔다. 딱 봐도 아니다.
파보니 meetSeq는 고정 ID가 아니라 “그 달 대회 목록에서의 순번”이었다. 2월의 2번과 8월의 2번이 아예 다른 대회다. 그래서 매달 대회 목록을 먼저 읽고, 번호가 아니라 대회 종류(meetType)로 식별하도록 고쳤다. 그제서야 포항-감바 경기가 제자리인 ACL 2로 들어갔다.
팀 이름 자리에 점수가 들어왔다
야구를 붙일 때 제일 크게 깨졌다. 수집은 675경기가 멀쩡히 됐는데, 화면을 열어보니 이랬다.
원인은 KBO 응답의 구조였다. 팀 이름과 점수가 둘 다 같은 태그고, 점수 쪽만 한 겹 더 감싸져 있었다.
<span>원정팀</span>
<em><span>4</span><span>vs</span><span>7</span></em>
<span>홈팀</span>
그래서 태그를 통째로 훑으면 [원정팀, 4, vs, 7, 홈팀]이 나온다. 앞에서 두 개를 팀으로 쓰면 “원정팀”과 “4”가 팀이 되는 것이다. 바깥쪽 태그만 팀으로, 점수는 안쪽에서 숫자만 꺼내도록 고쳤다.
KBO에는 함정이 하나 더 있었다. 날짜가 그날 첫 경기 줄에만 붙는다. 표에서 날짜 칸을 세로로 합쳐놨기 때문인데, 그래서 줄마다 칸 개수가 9개였다 8개였다 한다. 칸을 앞에서 세면 어긋나므로, 앞쪽은 칸에 붙은 이름표로 찾고 뒤쪽(경기장·비고)은 끝에서부터 세도록 했다.
검사기가 통과시킨 버그
이번 작업에서 가장 배운 게 이 부분이다. 위의 “팀 이름이 2” 버그를 발견한 건 화면을 눈으로 봤기 때문이지, 검사기가 잡아준 게 아니었다. 데이터를 점검하는 check.py를 미리 만들어 뒀는데, 그 검사기가 675건을 전부 통과시켰다.
이유는 단순했다. 검사기가 보던 건 구조였다.
- 날짜 형식이 맞는가 → 맞음
- 팀 이름이 비어 있지 않은가 → 비어 있지 않음(“2″도 글자다)
- 점수와 종료 상태가 어긋나지 않는가 → 안 어긋남
전부 통과다. 구조는 봤는데 “말이 되는 값인가”는 안 본 것이다. 그래서 파서를 고치기 전에 검사기부터 고쳤다. 추가한 규칙은 이렇다.
- 팀 이름이 숫자이면 실패
- 팀 이름이
vs,-같은 값이면 실패 - 한 리그의 팀이 40개를 넘으면 실패 — 파싱이 깨지면 팀 수가 폭발한다. 실제로 10팀짜리 KBO가 30팀으로 잡혀 있었다.
- 홈경기나 원정경기가 하나도 없는 팀이 있으면 경고 — 홈·원정 판별이 뒤집힌 경우를 잡는다
고친 검사기로 깨진 데이터를 다시 돌리니 675건을 전부 잡아냈다. 그다음에 파서를 고쳤다. 새 데이터 소스를 붙일 때는 파서보다 검사기를 먼저 손보는 게 맞다는 걸 이번에 확실히 배웠다.
자동 갱신 — 하루 두 번
일정은 계속 바뀌고 결과도 쌓인다. 손으로 돌리면 금방 낡는다. 그래서 GitHub Actions로 한국시간 매일 06:00과 23:30에 수집 → 점검 → 빌드 → 배포까지 돌게 했다. 밤 경기 결과를 그날 안에 반영하고, 아침에 일정 변경을 반영하는 구성이다.
여기서 고민한 게 “종목마다 갱신을 따로 걸어야 하나”였다. 결론은 아니오. 작업 하나가 수집기를 전부 돌리면 되고, 종목별로 쪼개면 관리할 게 늘 뿐이다. 대신 “아직 시즌이 시작 안 한 종목”은 각 대회에 활동 기간을 적어두고 비시즌이면 요청 자체를 건너뛴다. 겨울 종목처럼 해를 넘기는 기간도 그대로 쓸 수 있게 했다.
60일 지나면 꺼진다
GitHub은 저장소에 60일간 활동이 없으면 예약 작업을 자동으로 꺼버린다. 우리는 결과물을 커밋하지 않는 구조라 딱 걸릴 뻔했다. 그래서 아침 실행만 데이터 파일을 하루 한 번 커밋하게 했다. 이게 활동으로 잡혀서 예약이 계속 살아 있다. 밤 실행은 커밋하지 않는다. 히스토리가 두 배로 불어나니까.
실행 두 개가 겹쳐서 밀려났다
첫날 바로 사고가 났다. 워크플로 파일을 올리자 자동 실행이 걸렸는데, 그걸 모르고 1분 뒤에 손으로 한 번 더 돌렸다. 결과는 이랬다.
#1 (자동, 23:29) → 성공. 데이터 커밋 + 배포 완료
#2 (수동, 23:30) → 실패. Process completed with exit code 1
에러 메시지가 exit code 1 한 줄뿐이라 원인이 안 보였다. 저장소에 커밋된 파일의 시각과 배포본 시각을 대조해서야 알았다. #2가 밀어넣으려는 순간 #1이 이미 커밋을 해버려서 거절당한 것이다. 손으로 돌린 실행은 버튼 누른 시점의 상태에 고정되기 때문에, 그 사이에 올라간 커밋을 모른다.
고칠 때 한 가지를 알았다. 흔히 쓰는 rebase로 붙이면 반드시 충돌한다. 데이터 파일은 매 실행이 통째로 새로 쓰기 때문이다. 우리 쪽이 항상 최신이니, 원격 위로 올라타서 우리 파일을 다시 얹는 방식으로 바꿨다. 실제 충돌 상황을 만들어 시험해보니 두 번째 시도에서 깔끔하게 통과했다. 덤으로 커밋 단계를 배포 뒤로 옮기고, 커밋이 실패해도 배포는 살아남게 했다.
예매 링크 — 되는 것과 안 되는 것
처음부터 넣고 싶었던 기능인데 가장 나중에 붙였다. 이유가 있다. 경기별 예매 주소는 티켓이 풀리기 전에는 아예 존재하지 않는다. 보통 경기 1주 전쯤 생긴다.
연맹 응답을 뜯어보니 구단마다 예매처가 다르게 적혀 있었다. 인터파크, 티켓링크, 구단 자체. 그리고 티켓이 풀린 경기에만 상품 코드가 들어와 있었다. 그래서 두 예매처의 링크를 실제 브라우저로 열어봤다.
- 티켓링크 — 연맹 페이지가 여는 주소를 그대로 열면
오류가 발생했습니다. (crypto)가 떴다. 파트너 인증값이 세션에 묶여 있어서 밖에서는 안 열린다. 포기. - 인터파크 — 상품 코드를 붙인 주소가 그 구단의 예매 페이지로 알아서 넘어갔다. 성공.
그래서 링크가 어디로 가는지를 정직하게 나눴다. 그 경기 예매 페이지로 바로 가는 건 “예매”, 구단 홈페이지로만 가는 건 “구단 홈”. 눌러보니 엉뚱한 데로 가는 것보다는 낫다고 봤다. 끝난 경기에는 아예 안 붙인다.
여기서도 한 번 헤맸다. 링크를 붙였는데 310경기 중 6건에만 달렸다. 원인은 어이없게도 같은 API 안에서 구단 이름 표기가 다른 것이었다. 구단 목록은 “강원FC”, 일정은 “강원”. 이름 표기를 전부 열쇠로 넣어서 해결했고, 그러자 208건으로 늘었다.
어떻게 시켰나 — 프롬프트로 진행한 과정
이번에도 코드를 직접 치지 않았다. 코딩 에이전트(Claude)에게 말로 시켜서 진행했다. 실제로 넣은 프롬프트를 순서대로 옮기면 이렇다.
- 처음 요청 — “한국 축구 K1리그부터 K4리그까지 경기 일정과 경기장소를 한눈에 정리해서 볼 수 있는 프로그램을 만들고 싶어. 날짜별로 확인할 수 있었으면 좋겠고, 가능하면 각 경기별 예매 사이트도 링크로 첨부해줬으면 좋겠어.”
- 확장 가능성 타진 — “코리아컵 같은 경기와 야구, 농구, 배구 경기 등 한국에서 볼 수 있는 경기들을 더 추가하면 시스템이 너무 복잡해질까?” → 여기서 “화면은 안 복잡해지고 수집기만 복잡해진다”는 답이 나왔고, 종목을 붙일 수 있는 구조로 먼저 갈아엎기로 했다.
- 순서 조정 — “종목별로 자동갱신을 따로 적용해야 하는거지? 그럼 자동갱신을 먼저 적용하는게 낫겠는데?” → 확장보다 자동화를 먼저 하는 쪽으로 순서를 바꿨다.
- 에러 그대로 붙여넣기 — “깃허브에서 액션으로 실행했더니 이런 오류가 발생했어.” 하고 로그를 그대로 복사해 넣었다. 위에서 말한 실행 충돌은 이렇게 잡혔다.
이번에 특히 효과가 좋았던 건 “추측하지 말고 확인해”에 해당하는 요청들이었다. 예매 링크를 붙일 때 “이 주소가 진짜 열리는지 실제 브라우저로 확인해봐”라고 시켰더니, 티켓링크가 안 된다는 걸 배포 전에 알아냈다. 안 그랬으면 눌러도 에러가 뜨는 버튼을 그대로 올릴 뻔했다.
반대로 에이전트가 잘못 짚은 적도 있었다. 데이터 커밋이 안 올라간 줄 알고 권한 설정을 의심했는데, 알고 보니 GitHub의 파일 캐시가 옛날 걸 보여주고 있었을 뿐이었다. 나중에 커밋 기록을 직접 열어보니 전부 정상이었다. 에이전트가 내린 진단도 근거를 같이 봐야 한다.
한계와 아쉬움
- 티켓링크 구단은 경기별 링크가 안 된다. 구단 홈페이지로만 보낸다. 티켓링크의 공개 구단 페이지 주소를 찾아내면 개선할 수 있다.
- K3·K4는 예매 링크가 없다. 대부분 무료입장이거나 현장 판매라 없는 게 맞긴 하다.
- 농구·배구를 아직 못 붙였다. KBL과 KOVO는 봇 차단이 걸려 있어 페이지 껍데기만 온다. 헤드리스 브라우저가 필요해 보이는데, 10월 개막이라 검증도 아직 못 했다. 겨울 종목이 들어오면 12~2월 빈 구간이 메워져서 1년 내내 쓰는 도구가 된다.
- 비어 있는 구간이 있다. 코리아컵은 대진이 안 나왔고, KBO는 9월 6일 이후 일정이 원본에 아직 없다. 숨기지 않고 화면 아래에 이유를 적어뒀다.
- 원본 오류를 그대로 보여준다. K4리그 10월 31일 경기는 홈·원정이 둘 다 같은 팀으로 올라와 있다. 협회 쪽 표기 오류인데, 임의로 고치지 않고 “원본 표기 오류”라고 표시만 한다.
결과물과 다음
결과물은 GitHub Pages에 올려뒀다. 설치할 것 없이 주소만 열면 된다.
- 👉 경기 있는 날 (nerdlog87.github.io/k-matchboard)
- 데이터도 열어뒀다 — matches.json
- 소스 — github.com/nerdlog87/k-matchboard
쓰면서 제일 만족스러운 건 주말 저녁에 뭘 볼지 고르는 시간이 짧아졌다는 점이다. 토요일 칸을 누르면 K리그1 4경기, K리그2 4경기, K3 4경기, KBO 5경기가 시간순으로 한 번에 나온다. 예전 같으면 사이트 세 곳을 돌아야 알 수 있던 정보다.
따라 만들 분들께
- 데이터가 나오는지부터 확인한다. 화면부터 만들면 나중에 데이터가 안 나올 때 전부 헛수고가 된다. 사이트가 JS로만 그려지는지, 내부 API가 있는지를 먼저 본다.
- 검사기를 파서보다 먼저 만든다. 그리고 “구조가 맞는가”가 아니라 “말이 되는 값인가”를 검사한다. 팀이 40개인 리그는 없다.
- 숫자로 검산한다. 12팀 리그는 한 라운드 6경기, 10팀 야구는 팀당 홈경기가 고르게 나뉜다. 이런 걸로 대조하면 이상한 데이터가 바로 보인다.
- 수집이 실패했을 때를 먼저 정한다. 원본이 하루 죽었다고 화면이 비면 안 된다. 그 대회만 직전 데이터를 유지하고, 화면에 “언제 자료인지”를 밝히도록 했다.
- 링크는 실제로 열어본다. 눌러도 에러가 나는 버튼은 없느니만 못하다.
다음에는 농구·배구를 붙여볼 생각이다. 10월 개막에 맞춰서 봇 차단을 어떻게 넘을지 시험해봐야 한다. 그다음엔 내 팀 알림 — 응원하는 팀 홈경기 티켓이 풀리면 알려주는 것까지 가면 진짜 쓸 만해질 것 같다. GitHub Pages 배포 과정 자체는 멀티뷰 크롬 확장 3편에 정리해뒀으니 참고하시면 된다.