일기 slecs

두 서비스 사이를 오가며 보낸 하루

목차

오늘 커밋 목록을 뒤로 쭉 스크롤하다가 잠깐 멍했다. 이게 정말 하루치 작업인가 싶었다. 두 서비스를 왔다 갔다 하면서도 뭔가 꽤 완성도 있게 마무리된 날이었는데, 그래서 더 이상했다.

오전: kpopdex 컴백 카운트다운

오전은 거의 kpopdex에 쏟았다. 팬들이 자기가 팔로우한 그룹의 컴백 카운트다운을 볼 수 있는 기능인데, 처음엔 그룹 페이지에만 붙이려 했다가 멤버 페이지에도 달아야 한다는 걸 금방 깨달았다.

이런 순간이 컴포넌트 설계의 갈림길이다. 빠르게 가려면 그냥 복붙해서 박아도 되는데, 그렇게 하면 카운트다운 포맷 하나 바꿀 때 두 군데를 동시에 열어야 한다. 결국 나중에 더 느려진다. 컴포넌트 하나로 빼서 그룹·멤버 양쪽에서 공유하도록 구조를 잡았다. 엔티티 타입이랑 ID만 프롭으로 받으면 내부에서 알아서 데이터를 가져오는 방식으로.

<ComebackCountdown
  entityType="group" // 또는 "member"
  entityId={id}
  timezone="Asia/Seoul"
/>

컴포넌트 내부에서 타입에 따라 다른 API 엔드포인트를 호출하도록 처리했다. 뷰 레이어는 완전히 동일하게 재사용하고, 데이터 페칭 로직만 분기하는 구조. 이게 처음부터 설계하면 30분 안에 되는 일인데, 나중에 리팩토링하면 두 배 걸린다.

KST 기준 카운트다운 계산이 겉으로는 단순해 보이는데, 서버 타임존이 UTC면 클라이언트 렌더링 시 시간이 어긋나는 경우가 생긴다. 서버에서는 ISO 8601 문자열만 내려주고, 카운트다운 계산은 클라이언트에서 KST offset 적용해서 처리하는 게 가장 깔끔하다고 판단해서 그렇게 했음. SSR 환경이면 또 다른 고민이 생기지만, 이 경우엔 클라이언트 사이드로 처리하는 게 맞았다.

/following 페이지를 만들면서 사소한 함정에 빠졌다. 컴백 일정이 없는 그룹은 목록 자체에서 빠져버리는 버그. 팔로우한 이상 일정 없어도 '아직 컴백 없음'으로 표시해줘야 맞는 거잖아. 당연한 건데 놓쳤다. 이런 류의 버그는 쿼리 레이어에서 INNER JOIN 대신 LEFT JOIN으로 바꾸거나, 앱 레이어에서 팔로우 목록 기준으로 컴백 데이터를 머지하는 방식 중 하나인데, 이번엔 후자로 처리했음. 고치고 나서야 진짜 '내 그룹 허브' 느낌이 났다. 팔로우한 그룹이 20개면 20개 다 보여야지, 일정 있는 것만 보여주면 그건 허브가 아니라 필터다.

MV 봇: API 쿼터 우회와 데이터 신뢰성

MV 봇도 오늘 큰 전환점을 맞았다. YouTube Data API 쿼터가 발목을 잡아왔는데, 검색 결과 페이지를 직접 긁는 방식으로 바꿨다. 키 없이 돌아가고, 쿼터 걱정도 없다. 137개 그룹 중 122개 공식 MV를 채웠는데 솔직히 예상보다 훨씬 잘 됐다.

YouTube Data API v3의 search.list 하나가 100 유닛이고 하루 한도가 10,000 유닛이다. 그룹이 수백 개 단위면 하루 만에 바닥나고, 다음 날 자정 리셋 전까지 봇이 멈춘다. 이 구조에서 벗어나려면 선택지가 두 가지인데, API 키를 여러 개 로테이션하거나 아예 페이지 스크래핑으로 전환하거나. 키 로테이션은 운영 부담이 생기니까 후자를 택했다. 스크래핑 방식에서 제일 신경 쓴 건 공식 채널 식별이었다. 검색 상위에 뜬다고 공식 채널인 게 아니니까, 채널명 일치도·검증 뱃지·콘텐츠 패턴을 조합해서 점수를 매기는 방식을 썼다.

iTunes로 디스코그래피 채우는 것도 LLM 없이, 키 없이 돌아가도록 짰는데 여기서 조용한 버그가 터졌다. 같은 이름의 전혀 다른 아티스트랑 매칭되는 오판이 몇 번 터졌다. 장르 필터에 시드 앨범 겹침 확인까지 넣고 나서야 안정됐다.

이런 데이터 신뢰성 문제가 제일 소리 없이 무서운 버그다. 애플리케이션이 터지거나 빈 화면을 보여주면 바로 티가 나는데, 잘못된 데이터가 정상적인 모양새로 표시되면 한참 뒤에야 누군가 "이 정보 틀린 것 같아요"로 제보가 들어온다. 자동화 파이프라인에서 메타데이터를 뽑을 때 신뢰 점수를 함께 관리해야 한다는 걸 이번에 다시 확인했음. 매칭 결과에 confidence 필드 하나 추가해두면, 임계값 아래 결과는 사람이 검토하거나 스킵하는 식으로 운용할 수 있다.

검증 레이어 방식
아티스트명 정규화 후 퍼지 매칭
장르 K-pop 계열 외 결과 가중치 낮춤
시드 앨범 겹침 기존 DB 앨범과 교집합 확인
신뢰 점수 항목별 가중합, 임계값 미달 시 스킵

오후: 스포츠 사이트와 중첩 앵커 함정

오후엔 스포츠 사이트 쪽으로 넘어갔다. 검색, 스코어 상태 필터, 맞대결 블록, 득점 순위판, F1 레이스 캘린더, 부상자 패널. 항목만 봐도 벅차다. 이 정도 볼륨이면 PR을 잘게 쪼개서 넣는 게 나중에 롤백할 때 편한데, 오늘은 흐름이 끊기기 싫어서 그냥 달렸다. 내일 검증 때 좀 고생할 수 있다는 걸 알면서도.

스코어 상태 필터는 라이브·종료·예정 같은 상태값 기반으로 결과를 걸러주는 거라 로직은 단순한데, 상태값이 소스마다 다른 문자열로 오는 게 문제였다. 중간에 정규화 레이어를 하나 두는 게 맞아서 그렇게 처리했음. 맞대결 블록은 날짜 순 정렬이랑 빈 상태 처리가 생각보다 손이 많이 갔다.

그중 가장 시간 잡아먹은 건 별것 아닌 중첩 앵커 버그였다. 뉴스 행 전체를 <a>로 감쌌는데 그 안에 작성자 링크로 또 <a>가 들어있는 구조. HTML 스펙상 <a> 안에 <a>는 허용되지 않는다. 브라우저 파서가 태그를 임의로 재배치하면서 레이아웃이 와장창 무너진다. 개발자 도구에서 DOM을 뜯어봐야 원인이 보이는 버그라 처음엔 CSS 문제인 줄 알고 한참 헤맸다. 찾고 나면 허탈하지만 그래도 찾아서 다행이다.

<!-- 문제 구조 -->
<a href="/news/123">
  <span>기사 제목</span>
  <a href="/author/kim">김작가</a>  <!-- 파서가 깨뜨림 -->
</a>

<!-- 수정 후 -->
<div class="news-row" role="link" data-href="/news/123">
  <span>기사 제목</span>
  <a href="/author/kim">김작가</a>
</div>

role="link"는 스크린 리더를 위한 거고, 실제 클릭 핸들러는 JS로 붙인다. 접근성까지 챙기면 이게 중첩 앵커 문제를 해결하는 표준 패턴이다.


사이트 여러 개를 하루에 동시에 건드리면 맥락 전환이 제일 피곤하다. kpopdex에서 KST 기준 카운트다운 로직 생각하다가 갑자기 스포츠 14개 언어 i18n으로 넘어가면 머리가 잠깐 멈추는 느낌이 난다. 각 서비스의 도메인 언어 자체가 다르니까. 팬덤 서비스는 감성이 섞인 UX 판단이고, 스포츠는 데이터 밀도랑 레이아웃 정확도가 중심이다. 같은 날 양쪽을 오가면 뇌가 두 개면 좋겠다는 생각을 한 번씩 한다.

이런 날은 저녁에 커밋 메시지 정리하면서 '오늘 이만큼 했구나' 실감하는 맛이 있다. 코드는 쌓이는데 체감은 느린 날들 사이에서, 오늘처럼 확실하게 전진한 날은 나름 좋다.

내일은 일단 오늘 넣은 것들 실제로 잘 돌아가는지 눈으로 확인하고 싶다. 기능 추가보다 검증이 먼저다. 스포츠 쪽 AdSense 승인 상태도 한번 봐야 할 것 같고. 오늘 너무 많이 달렸으니 내일은 좀 조용히.

댓글 0

첫 댓글 달아줘.