API 없이도 데이터를 채우는 파이프라인, 그 가능성을 검증한 오후
목차
이날 오후 세 시간은 두 개의 서비스를 오가며 흘렀다. 케이팝 팬 서비스(kpopdex)와 멀티스포츠 스코어 사이트, 얼핏 보면 도메인이 완전히 달라 보이지만 공통된 문제의식을 가지고 움직였다. 바로 외부 API 의존도를 낮추면서도 데이터 정합성을 높이는 것. 한쪽은 YouTube Data API v3 쿼터에 치여 있었고, 다른 쪽은 12개 종목 동기화가 특정 종목에만 편중된 상태였다. 데이터 파이프라인을 손보고, 그 사이사이 프론트 버그를 잡고, SEO 게이트를 조정하면서 작업이 쌓여갔다.
YouTube MV 봇: 쿼터 장벽을 스크레이핑으로 우회하다
kpopdex의 MV 봇은 원래 YouTube Data API v3 기반이었다. 검색 한 번에 100유닛이 나가는 구조라 137개 그룹 전부를 하루 안에 처리하는 게 애초에 불가능했다. 쿼터 초기화를 기다리며 여러 날에 나눠 돌리는 게 현실이었고, 새 그룹이 추가될 때마다 그 제약이 발목을 잡았다.
전환한 방식은 YouTube 검색 결과 페이지를 직접 파싱하는 것이다. 응답 HTML 안에 박혀있는 ytInitialData JSON을 추출하면 API 없이도 검색 결과를 가져올 수 있다. 쿼터 개념 자체가 사라진다. 이 방식으로 137개 그룹 중 122개 공식 MV를 단일 세션에서 채웠다.
그런데 문제는 "공식 MV"라는 키워드만으로는 원하지 않는 영상이 섞인다는 점이었다. 처음 돌려보니 commentary, clip, shoot, teaser, behind-the-scenes 영상들이 결과에 들어와 있었다. 제목 기반 exclude 패턴을 만들고 실제 데이터를 보면서 세 번에 걸쳐 확장했다. 완벽하진 않지만 현실적으로 "공식 뮤직비디오"에 가장 가까운 결과를 걸러내는 수준까지는 왔다.
추가로 기존 로직에서 YouTube MV를 추가할 때 Apple Music 임베드를 그대로 남겨두는 케이스가 있었는데, MV 전용 페이지에서는 Apple Music 임베드를 제거하는 게 UX 일관성상 맞다고 판단해서 정리했다. 사소해 보이지만 "MV인데 왜 음원 플레이어가 같이 있지?"라는 혼선을 없애는 작업이었다.
디스코그래피 봇: 동명이인 함정과 장르 필터의 이중 방어
enrich_discography.py는 iTunes/Apple Music에서 그룹별 앨범을 가져오는 봇이다. iTunes Search API는 별도 키 없이 쓸 수 있어서 LLM도, API 키 발급도 필요 없다. 이 방향성 자체는 좋았는데, 현실 데이터를 만나니까 생각보다 복잡한 엣지 케이스들이 나왔다.
가장 골치 아팠던 건 동명이인 문제였다. "LATENCY"라는 K-Pop 그룹을 찾는데 iTunes에서 전혀 다른 아티스트 "LatenCy"가 상위에 올라온다. 그냥 매칭하면 완전히 엉뚱한 디스코그래피가 DB에 들어가버린다. 방어 로직을 두 단계로 구성했다.
| 케이스 | 검증 방식 | 비고 |
|---|---|---|
| wiki 시드 앨범이 있는 그룹 | iTunes 결과 디스코그래피와 시드 앨범 오버랩 확인 | 겹치면 동일 아티스트 |
| 신인 등 시드 없는 그룹 | K-Pop 고유 장르 조건 + unique 체크 | 동명이인 제거 |
| 공통 | 장르 K-Pop/J-Pop strict 필터 | 비장르 아티스트 사전 차단 |
처음엔 장르 필터를 느슨하게 적용했다가 generic한 영문 이름의 그룹들이 서양 아티스트와 매칭되는 걸 목격하고 강화했다. 한 번 틀린 데이터가 들어가면 나중에 찾아서 정정하는 비용이 크기 때문에 입구에서 확실히 걸러야 한다.
현실 데이터 특유의 문제들도 있었다. OST 제목처럼 긴 앨범명이 varchar(80) 슬러그 컬럼을 오버플로우하거나, 타이틀이 160자를 넘는 케이스들. 슬러그는 80자 캡에 해시 서픽스를 붙이는 방식으로, 타이틀은 160자로 잘라서 처리했다. 그리고 배치 중 하나 실패해도 전체 롤백되지 않도록 per-group commit에 try/except를 감싸서 resilient 구조로 만들었다.
# per-group commit 구조 — 하나 실패해도 나머지는 계속 진행
for group in groups:
try:
enrich_one_group(group, conn)
conn.commit()
except Exception as e:
conn.rollback()
log_error(group, e)
continue
스포츠 사이트: 멀티 스포츠 자동화와 F1 추가
스포츠 쪽은 방향이 달랐다. 기존에는 축구 위주로만 동기화가 돌아갔는데, 이를 12개 종목 전체로 확장하는 게 목표였다. MODE=refresh를 설정하면 오늘 경기 + 라이브 경기를 종목별로 순회하는 로직을 sync_sports.py에 구현하고, 2시간 간격 크론으로 전 종목이 자동 갱신되도록 붙였다.
작업 중에 종목별로 팀 로고가 누락된 케이스들이 보여서 미디어 URL 패턴 기반으로 백필하는 작업도 함께 진행했다. NBA는 v2 API의 실제 로고 URL 구조를 분석해서 별도로 처리했다.
F1은 이번에 새로 추가한 종목인데, 레이스 캘린더를 fixtures 형식으로 가져와서 저장하고 GP명·서킷·날짜·상태를 시즌별로 보여주는 뷰를 구성했다. 축구 쪽에서는 리그 페이지에 top scorers 패널을 추가하면서 시즌 불일치 문제(스탠딩은 2024, 득점왕은 2023)를 발견해서 맞췄고, 크론 타이밍도 UTC 00:05 — 쿼터 초기화 직후 — 로 잡았다. 부상자 데이터를 위한 sg_injury 테이블도 새로 만들어 팀 페이지에 패널로 노출했다.
프론트 잡일들: 작지만 쌓이면 품질 지표를 깎는 것들
데이터 작업들 사이사이에 프론트엔드 버그도 여러 개 잡았다. 각각은 작아 보이지만 방치하면 실사용자나 검색엔진 모두에게 나쁜 신호가 된다.
- nested anchor 버그: 뉴스/아티클 리스트 행이 바깥
<a>로 감싸여 있는데 내부에도<a>가 있는 구조. stretched-link 패턴이 중첩 앵커로 깨지는 케이스였다. 바깥 wrapper를 제거하고 내부 링크만 남기는 방식으로 수정. - 카드 그리드 빈 칸: 홈 카드 그리드 마지막 행 빈 셀이 회색 블록으로 남는 문제. 컨테이너 배경을 투명으로 처리해 페이지 배경과 자연스럽게 융합.
- WCAG AA 불통: 캘린더 요일/more 라벨 색 대비가 4.44로 기준(4.5:1)을 아슬아슬하게 못 넘겼다.
--muted토큰으로 교체해 통과. - thin 뉴스 noindex 게이트 조정: 200자 → 600자로 기준을 올리고, 해당 페이지를 sitemap에서도 제외. 얇은 콘텐츠가 검색 인덱스에 쌓이는 걸 막기 위해서.
- 홈 콘텐츠 수 조정: 최신 뉴스 6개 → 12개, 릴리즈 10개 → 12개. 6개는 스크롤도 없이 끝나는 빈 느낌이라 늘렸다.
이 세 시간을 돌아보면, 공통된 맥락이 하나 있었다. "외부 종속성을 줄이면서 데이터 정합성은 높인다"는 방향. YouTube API 쿼터를 스크레이핑으로 우회하고, iTunes로 LLM 없이 디스코그래피를 채우고, 멀티스포츠 동기화를 단일 크론으로 통합한 것 모두 같은 맥락이다. 쿼터나 키 관리 부담 없이 안정적으로 돌아가는 파이프라인은 장기적으로 운영 비용이 확연히 낮다. 동명이인 함정처럼 현실 데이터에서 나오는 예외들을 한 번 제대로 막아두면 나중에 잘못된 데이터를 찾아 정정하는 비용을 아낄 수 있다. 결국 오늘 오후는 빠른 기능 추가보다 "다음 번에도 믿을 수 있는 파이프라인"을 만드는 데 시간을 더 썼던 날이었다.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.