유튜브 채널을 통째로 위키로 만들기 — 자막 스크래퍼를 만들며 겪은 버그들


들어가며

즐겨보는 AI 자동화 유튜브 채널이 하나 있다. 영상이 쌓일수록 “저 얘기 어느 영상에서 했더라” 하고 다시 찾아 헤매는 일이 잦아져서, 아예 채널 영상을 통째로 자막까지 긁어서 Obsidian 위키에 쌓아두기로 했다. 채널 하나를 RSS/Data API로 훑어 영상을 찾고, 자막을 받아오고, 위키 노트로 정리하는 파이프라인을 Claude Code와 함께 터미널에서 만들었다. 이 글은 그 과정 — 특히 “무료로 돌리기”를 고집하다 마주친 버그들과 마지막에 만난 네트워크 차단 소동 — 을 기록한 것이다.

설계: 채널 1개가 아니라 “분야별 여러 채널”

처음엔 채널 하나만 받는 구조로 짰다. 그런데 나중엔 관심사별로 여러 채널을 따로 관리하고 싶어져서, channels.json에 채널ID·이름·분야(domain)를 배열로 적어두면 그 목록을 전부 순회하도록 바꿨다. 노트는 저장위치/분야명/ 아래에 분야별로 나뉘어 쌓이고, DB 스키마에도 분야 컬럼을 추가해 상태를 채널·분야별로 필터링할 수 있게 했다. 채널 하나가 실패해도 다음 채널로 넘어가게 만들어서, 여러 채널을 한 번에 돌려도 하나의 오류가 전체를 멈추지 않는다.

API 비용 없이 돌리기

원래는 자막을 받은 뒤 Claude API를 호출해 영상마다 요약까지 자동 생성하게 만들었다. 그런데 “굳이 API 키를 따로 발급받고 과금까지 해야 하나, 이미 Claude Code 세션을 쓰고 있는데”라는 생각이 들었다. 그래서 SKIP_SUMMARY라는 옵션을 추가했다 — 이 모드에서는 자막 원본만 받아서 저장하고, 요약과 위키 정리는 지금 쓰고 있는 Claude Code 세션이 직접 맡는다. 스크립트가 별도로 API를 호출할 필요가 없어지니 자막 수집 자체는 완전히 무료로 돌아간다.

채널 전체 이력을 한 번에 받아오는 “백필” 기능도 원래는 유튜브 Data API 키가 필수였는데, yt-dlp의 채널 목록 조회 기능을 활용해 API 키 없이도 채널의 전체 영상 목록(테스트해보니 200여 개)을 무료로 가져오는 경로를 추가했다. 다만 이 방식은 발행일 정보를 못 주는 대신 영상 길이는 알려줘서, 쇼츠 필터링 정도는 여전히 가능하다.

실제로 만난 버그 네 가지

# 증상 원인 심각도
1 설정값이 주석 텍스트로 채워짐 빈 값 + 인라인 주석 조합 낮음
2 폴백이 조용히 실패 가상환경 미activate로 yt-dlp가 PATH에 없음 중간
3 한국어 제목이 영어로 저장 채널 목록 조회 시 자동번역 중간
4 차단된 영상이 영구 제외됨 일시적 차단을 “자막 없음”으로 기록 높음

1. 빈 값 + 인라인 주석을 같이 쓰면 값이 깨진다

.env 파일에서 API_KEY= # 이 키는 선택사항입니다 처럼 값이 비어있는 줄에 인라인 주석을 달았더니, 환경변수 로더가 주석까지 통째로 값으로 읽어버렸다. 값이 채워진 줄은 문제없이 주석을 잘라내는데, 유독 “빈 값 + 주석” 조합에서만 이 문제가 났다. 결과적으로 빈 문자열이어야 할 설정값이 주석 텍스트 그 자체가 되어버려서, 그 값을 쓰는 API 호출이 400 에러를 냈다. 고친 방법은 간단했다 — 값을 비워둘 땐 주석을 아예 별도 줄로 옮기는 것.

2. 가상환경 스크립트를 activate 없이 실행하면 폴백 도구를 못 찾는다

이 프로젝트는 자막 API가 막히면 yt-dlp로 폴백하도록 짜여 있는데, 가상환경을 activate하지 않고 .venv/Scripts/python.exe를 직접 실행하는 방식으로 돌리다 보니 yt-dlp 실행파일이 PATH에서 안 잡혀서 폴백 자체가 조용히 실패하고 있었다. 실행 파일 이름으로 찾는 대신, 지금 실행 중인 파이썬 인터프리터로 모듈을 직접 호출(python -m yt_dlp)하도록 바꾸니 PATH 설정과 무관하게 항상 동작하게 됐다.

3. 채널 전체 목록을 가져오면 제목이 자동번역된다

yt-dlp로 채널의 영상 목록 전체를 한 번에 가져오는 기능을 쓰니, 원래 한국어인 제목들이 전부 영어로 자동번역되어 나왔다. 언어를 명시적으로 고정하는 옵션을 추가하고 나서야 원어 그대로의 제목을 받을 수 있었다. 이대로 뒀으면 위키 노트 제목이 전부 번역투 영어로 저장될 뻔했다.

4. “차단당함”과 “애초에 없음”을 구분하지 못한 버그 (제일 심각했다)

가장 뼈아팠던 버그다. 유튜브 쪽에서 자막 요청이 일시적으로 차단됐을 때(과다 요청으로 인한 429 응답), 그 실패를 **“이 영상엔 자막이 아예 없다”**는 영구적인 상태로 잘못 기록하고 있었다. 문제는 이 두 상태를 다루는 방식이 완전히 달랐다는 것 — “자막 없음”은 재시도 대상에서 영구 제외되는 반면, “일시적 오류”는 나중에 다시 시도할 수 있는 상태로 남아야 했다. 즉 한 번 차단에 걸리면, 나중에 차단이 풀려도 그 영상들은 영원히 재시도되지 않는 구조였던 셈이다.

고친 방법은 자막을 받아오는 함수가 실패 이유까지 같이 반환하도록 바꾸는 것이었다. 진짜로 자막이 없는 경우(자막 기능 자체가 꺼진 영상 등)와, 일시적 차단·오류로 실패한 경우를 구분해서, 후자는 재시도 가능한 상태로 따로 남기게 했다. 다행히 실제 서비스 배포 전에 발견해서, 이미 잘못 기록된 십여 건을 다시 대기 상태로 되돌려놓는 것으로 정리됐다.

그리고 만난 진짜 차단

버그를 다 고친 뒤에도 여전히 요청이 막히길래 짧은 간격으로 몇 차례 다시 시도해봤는데, 계속 막혔다. 처음엔 “스크립트의 요청 패턴만 걸렸나 보다” 싶었는데, 브라우저에서 같은 영상의 자막 버튼을 눌러봤더니 거기서도 자막이 안 떴다. 이걸로 스크립트만의 문제가 아니라 네트워크(IP) 단위로 더 넓게 제한된 상태라는 걸 확인했다.

확실히 하려고 휴대폰 핫스팟으로 네트워크를 바꿔서 같은 영상을 다시 열어봤는데, 거기선 정상적으로 자막이 떴다. 집 네트워크의 IP가 문제였던 게 확정된 셈이다. 프록시까지 갈 필요 없이, 그냥 네트워크를 바꾸거나 시간을 두고 기다리면 풀릴 문제로 결론짓고 잠시 손을 뗐다.

지금까지 세운 원칙

  • 실패를 뭉뚱그려 하나의 상태로 기록하지 않는다. “영구적으로 안 됨”과 “지금은 안 되지만 나중엔 될 수 있음”은 반드시 구분해서 저장한다.
  • 이미 쓰고 있는 도구(이 경우 Claude Code 세션)로 대체할 수 있는 API 호출은 굳이 따로 만들지 않는다.
  • 뭔가 계속 막히면, 내 코드만 의심하지 말고 브라우저 등 다른 경로로도 같은 증상이 나는지 먼저 확인한다 — 원인의 범위(스크립트 vs 네트워크)를 좁히는 데 훨씬 빠르다.

마치며

거창한 크롤러를 만든 건 아니고, 그냥 좋아하는 채널을 자막까지 통째로 검색 가능한 개인 위키에 쌓아두고 싶었을 뿐이다. 그 단순한 목표를 무료로, 그것도 안정적으로 이루려다 보니 환경변수 파싱, 가상환경 PATH, 실패 상태 설계, 네트워크 차단 진단까지 — 작은 스크래퍼 하나에도 의외로 많은 디테일이 숨어 있다는 걸 다시 확인했다.

이 글은 개인 위키에 정리해둔 ytwiki 파이프라인 빌드 로그를 블로그용으로 재구성한 것입니다.

댓글