YouTube 자체에 폴더 시스템을 주입하는 브라우저 확장 프로그램 · Shadow DOM 속 Svelte가 적대적인 SPA 위에서 동작 · 공개 RSS로 업로드 수집, API 키도 로그인도 없음 · bearer Sync Code 뒤의 선택적 기기 간 동기화
해결할 문제
2014년에는 '구독' 페이지를 볼 영상의 주된 출처로 썼지만, 지금은 그저 제가 구독한 모든 채널이 뒤엉킨 거대한 난장판일 뿐입니다.
저는 YouTube를 아주 많이 사용하는데, 산더미 같은 구독 목록과 제 구독보다는 그때그때의 관심사 주변의 바이럴 영상에 더 신경 쓰는 알고리즘 탓에, 사용하는 방식이 수년간 크게 바뀌었습니다.
나쁘기만 한 것은 아닙니다. 홈 화면을 여는 순간 사람을 확 끌어들이는 데는 꽤 능숙하니까요. 하지만 거기서 따라오는 문제는, 제가 구독한 채널의 영상을 자주 놓친다는 것입니다. 대개는 그 채널의 니치와 상관없는 영상의 굴레에 빠져들고, 그것이 눈덩이 효과를 일으켜 저를 그 채널들에서 멀어지게 하기 때문입니다.
그래서 저는 이 YouTube 구독 페이지의 쓰임을 되살리면서, 동시에 다른 영상에 방해받지 않고 특정 니치의 영상을 골라 볼 수 있기를 원했습니다. 그러려면 같은 니치의 YouTube 채널들을 하나의 공통 피드에 모아 분류할 방법이 필요했습니다. 예를 들어 테크 소식이나 클라이밍 소식, 혹은 특정 비디오 게임 콘텐츠를 따라가고 싶을 때처럼 말이죠.
나의 아이디어
폴더를 만들고 거기에 YouTube 채널을 추가하면, 그 폴더 안에 해당 채널들만의 영상 피드가 생기는 것. 그리고 대부분의 경우 피드를 심하게 오염시키는 shorts를 걸러낼 수 있는 것.
제 모든 Chrome 확장 프로그램이 그렇듯, 저는 사용자가 확장 프로그램 안에서 로그인해야 하는 것을 원치 않습니다. 그만큼 복잡도와 제약이 늘어나지만, 저에게는 사용자가 안전하다고 느끼는 것, 그리고 사용자 계정과 독립적인 것이 중요합니다. 즉 현재 어떤 YouTube 계정을 쓰든 폴더는 그대로 유지됩니다.
아키텍처
폴더 목록에서의 클릭은 히스토리 항목을 추가하고 YouTube의 콘텐츠 영역 위로 폴더 피드를 여는데, 폴더 id가 URL 해시에 담겨 뒤로 가기와 새로고침이 이를 복원합니다. 피드는 런타임 메시지를 통해 백그라운드 워커에게 각 채널의 videos.xml을 YouTube RSS에서 가져오라고 요청하고(콘텐츠 스크립트는 cross-origin fetch를 할 수 없습니다) 항목을 최신순으로 병합하며 약 100개 근처에서 제한합니다. 폴더 자체는 chrome.storage.local에 있으며 UI가 직접 읽고 씁니다. 약 30분마다 chrome.alarms 틱이 워커를 깨워 중복 제거된 채널을 다시 폴링하고 읽지 않음 점을 켭니다. Sync Code가 연결되면 변경 사항이 코드의 SHA-256으로 키가 지정된 Supabase Postgres로 푸시됩니다.
배포: 확장 프로그램 → Chrome Web Store + Edge Add-ons · MV3, WXT로 빌드 · 동기화 백엔드 → Supabase 무료 티어 · 주 2회 launchd keep-alive 핑
성과
- 0 · API 키, 쿼터, 로그인 · 공개 RSS로 업로드 수집
- 30분 · 읽지 않음 폴링 주기 · 워커는 알람 사이에 잠듭니다
- 256비트 · Sync Code · 서버는 그 SHA-256만 저장합니다
- 10개 ADR · 모든 번복 기록 · 2개 결정 대체됨
보여준 역량: MV3 서비스 워커 생명주기(타이머가 아닌 알람) · 조건부 HTTP 폴링(ETag / 304, jitter, backoff) · union 병합 + last-write-wins + tombstone 동기화 · 호스트 페이지 내부의 Shadow DOM 스타일 격리 · MutationObserver 기반 DOM 주입 · Vitest + happy-dom으로 DOM 로직 단위 테스트
문제와 해결책
모든 것은 하나의 제약에서 비롯됩니다: YouTube의 허가나 API 없이, YouTube의 데이터를 대상으로, YouTube의 페이지 내부에서 살아남기.
█ API도 로그인도 없이: 유일한 데이터 소스로서의 RSS
문제: 폴더 피드는 모든 채널의 업로드 데이터가 필요합니다. 공식 Data API는 API 키, 쿼터, Google 로그인을 요구하며, 사용자당 쿼터를 소모하는 것은 무료 확장 프로그램을 죽입니다.
모든 채널은 공개 RSS 피드(/feeds/videos.xml)를 노출합니다. 확장 프로그램은 이를 클라이언트 측에서, 사용자 자신의 브라우저에서 가져오므로 서버도, 키도, 로그인도 어디에도 없습니다.
- YouTube Data API 거부: 쿼터 계산은 적당한 사용자 수에서도 실패하고 모두에게 계정을 강요합니다
- RSS의 한계를 알면서 수용: 채널당 약 15개 항목, Shorts와 라이브 스트림은 안정적으로 구분되지 않습니다
- 어떤 쿼터 변경으로도 취소할 수 없는, 비용 제로 자격 증명 제로 제품을 확보
▤ 끊임없이 다시 렌더링되는 YouTube의 DOM에서 살아남기
문제: YouTube는 헤더와 콘텐츠를 끊임없이 재구성하는 SPA입니다. 주입된 버튼은 세션 도중에 사라지고, 페이지 CSS는 순진하게 마운트된 모든 것에 스며듭니다.
모든 UI는 Shadow DOM 속 Svelte(밀봉된 스타일 경계)이며, 폴더에 추가 버튼은 헤더 하위 트리의 MutationObserver가 앵커가 다시 나타나는 순간 재주입하여 살아있게 유지합니다.
- 고정된 재시도 스케줄 거부: 영원히 폴링하거나 앵커가 마운트되기 직전에 포기합니다
- 팝오버는 버튼 자체의 shadow root가 아니라 body에 마운트된 오버레이 호스트에 렌더링되어, YouTube의 변형된 헤더가 이를 잘라낼 수 없습니다
- 트레이드오프: YouTube의 DOM 앵커에 대한 결합, 사이드바 앵커가 없을 때 플로팅 런처 폴백으로 완화
▒ 계정 없는 동기화: bearer Sync Code
문제: 기기 간 동기화는 보통 인증을 의미합니다: 이메일 흐름, OAuth, 비밀번호 재설정. 폴더 목록을 동기화하기에는 엄청난 표면적입니다.
256비트 CSPRNG Sync Code가 신원 전체입니다. 서버는 폴더를 코드의 SHA-256으로 키를 지정하므로 데이터베이스는 자격 증명을 결코 보유하지 않습니다. 첫 연결은 기기를 union 병합하고, 그다음 폴더별로 last-write-wins에 tombstone 삭제를 적용합니다.
- v1에서는 실제 계정을 거부했지만, 나중에 더 강력한 신원이 코드를 대체할 수 있도록 스키마를 설계했습니다
- 행 삭제 대신 tombstone을 써서, 한 기기의 삭제가 조용히 되살아나는 대신 오래된 사본을 이깁니다
- 트레이드오프를 처음부터 명시: 코드를 잃으면 클라우드 연결을 잃습니다. 폴더는 향후 서버 기능을 열어두기 위해 서버 측에서 평문입니다
▓ 사용자가 rate-limit에 걸리지 않게 하는 정중한 폴링
문제: 읽지 않음 점은 각 사용자 자신의 IP에서 백그라운드 RSS 확인을 필요로 합니다. 순진한 루프는 YouTube로부터 429를 유발하고, MV3 서비스 워커는 루프를 아예 돌릴 수 없습니다: Chrome이 몇 초 안에 죽입니다.
약 30분마다 단일 chrome.alarms 틱이 워커를 깨웁니다. 채널은 폴더 전반에서 중복 제거되고 ETag 조건부 요청, 제한된 동시성, jitter, 429 backoff로 가져옵니다.
- 서버 측 폴러 거부: 서버가 존재하지 않으며, 하나를 추가하면 비용과 자격 증명 문제가 되살아납니다
- setInterval 방식의 타이머 거부: 알람은 워커 수면을 견디고, 타이머는 워커와 함께 죽습니다
- 정중함의 정직한 대가로 최대 30분의 읽지 않음 지연을 수용
▞ 뒤로 가기 버튼을 존중하는 전체 페이지 피드
문제: 피드는 사이드 드로어로 시작했습니다: 비좁고, 어떤 네비게이션으로도 파괴되어, 영상을 본 뒤 뒤로 가기를 누르면 사용자를 피드 대신 그 아래 페이지에 떨어뜨렸습니다.
이제 피드는 네이티브 페이지처럼 YouTube의 콘텐츠 영역을 차지합니다. 폴더 id는 URL 해시에 있고, 열 때 히스토리 항목이 추가되며, 뒤로 / 앞으로 / 새로고침이 피드를 복원합니다. 그 뒤의 콘텐츠 영역은 단지 덮이는 게 아니라 숨겨지고 스크롤이 잠깁니다.
- 실제 사용에서 드로어가 사이트와 싸운다는 것이 드러나자 내 자신의 이전 드로어 결정을 대체(ADR 0004 → 0007 / 0010)
- 영역을 덮는 대신 숨기면 백그라운드 렌더링과 스크롤 새어나옴을 막습니다
- 마스트헤드와 사이드바는 살아있어, 하나의 폴더로 좁혀진 YouTube 자체의 구독 페이지처럼 읽힙니다
░ OS가 아니라 YouTube의 테마에 맞추기
문제: 여기서 prefers-color-scheme는 거짓말을 합니다: YouTube는 사용자가 OS와 독립적으로 사이트 테마를 고르게 하므로, OS 기반 확장 프로그램은 어두운 페이지에 밝은 패널을 렌더링합니다.
확장 프로그램은 YouTube 루트 요소의 dark 속성을 관찰하고 페이지가 실제로 표시하는 것을 그대로 반영하며, 사용자가 사이트 테마를 전환할 때 실시간으로 뒤집힙니다.
- OS가 아니라 호스트 페이지가 진실의 원천이기에 표준 미디어 쿼리를 거부
- 단일 속성에 대한 하나의 MutationObserver라, 비용은 무시할 만합니다
- 사용자가 OS와 사이트 테마를 어떻게 섞든 픽셀 단위로 일관된 임베딩을 확보