YouTube에 폴더 기능을 추가하는 브라우저 확장 프로그램 · 수시로 바뀌는 SPA에서 Shadow DOM 속 Svelte로 동작 · 공개 RSS로 영상을 수집해 API 키와 로그인이 필요 없음 · Sync Code를 이용한 선택적 기기 간 동기화
해결할 문제
2014년에는 '구독' 페이지에서 주로 볼 영상을 찾았습니다. 지금은 구독한 모든 채널이 한데 뒤섞인 복잡한 목록이 됐습니다.
YouTube를 오래 쓰면서 구독 목록은 걷잡을 수 없이 늘었습니다. 추천 알고리즘도 구독한 채널보다 그때그때 관심을 끄는 인기 영상을 더 자주 보여줬고, 제 시청 방식은 크게 달라졌습니다.
추천 기능은 홈 화면을 여는 순간 시선을 끄는 데 매우 효과적입니다. 하지만 관심 분야와 무관한 영상을 연달아 보다 보면 정작 구독한 채널의 새 영상을 자주 놓치게 됩니다. 이런 일이 반복될수록 해당 채널과 점점 멀어졌습니다.
그래서 YouTube 구독 페이지의 쓰임을 되살리고, 다른 영상에 방해받지 않고 원하는 분야의 영상만 골라 보고 싶었습니다. 기술 소식, 클라이밍, 특정 게임처럼 비슷한 주제의 채널을 하나의 피드로 묶을 방법이 필요했습니다.
아이디어
폴더를 만들고 YouTube 채널을 추가하면 해당 채널의 영상만 모은 피드가 생깁니다. 피드를 복잡하게 만드는 Shorts도 필요에 따라 걸러낼 수 있습니다.
제가 만든 다른 Chrome 확장 프로그램처럼 별도 로그인을 요구하지 않습니다. 구현 복잡도와 제약이 늘어나더라도 사용자가 안심하고 쓸 수 있고, YouTube 계정에 종속되지 않는 편이 더 중요했습니다. 어떤 YouTube 계정으로 전환해도 폴더는 그대로 유지됩니다.
아키텍처
- 폴더 목록, 프런트엔드 · 사이드바 삽입, entrypoints/youtube.content
- 폴더 피드, 프런트엔드 · 전체 화면, youtube.content · overlay host
- 폴더에 추가 버튼, 프런트엔드 · 페이지 버튼, beside the Subscribe · bell cluster
- 백그라운드 워커, 핵심 · MV3 서비스 워커, entrypoints/background.ts, @handle → UCid resolver, 정중한 RSS 폴링 · ETag · backoff, 새 영상 계산, 동기화 push / pull
- YouTube RSS, 외부 · 공개 RSS, 외부 서비스, /feeds/videos.xml?channel_id=…
- chrome.storage.local, 저장소 · 로컬 우선, folders · resolver cache · unread · link
- Supabase Postgres, 저장소 · 동기화 백엔드, 외부 서비스, public.synced_folders
- 폴더 목록에서 폴더 피드 방향으로: 폴더 열기
- 폴더 피드에서 백그라운드 워커 방향으로: 피드 가져오기
- 폴더에 추가 버튼에서 백그라운드 워커 방향으로: @HANDLE 해석
- 백그라운드 워커에서 YouTube RSS 방향으로: VIDEOS.XML · ETAG
- 폴더 목록에서 chrome.storage.local 방향으로: 폴더 읽기 / 쓰기
- 백그라운드 워커에서 chrome.storage.local 방향으로: 새 영상 · 캐시
- 백그라운드 워커에서 Supabase Postgres 방향으로: PUSH / PULL · REST
폴더 목록에서 항목을 누르면 브라우저 방문 기록에 상태를 추가하고 YouTube 콘텐츠 영역에 폴더 피드를 엽니다. 폴더 ID를 URL 해시에 담아 뒤로 가기와 새로고침 후에도 같은 피드를 복원합니다. 피드는 런타임 메시지로 백그라운드 워커에 요청을 보내 각 채널의 videos.xml을 YouTube RSS에서 가져옵니다. 콘텐츠 스크립트에서는 교차 출처 요청을 보낼 수 없기 때문입니다. 수집한 항목은 최신순으로 합치고 약 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만 저장합니다
- ADR 10개 · 모든 의사결정 변경 기록 · 기존 결정 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는 사용자 수가 조금만 늘어도 쿼터가 부족하고 계정을 요구하므로 제외했습니다
- 채널당 항목이 약 15개뿐이고 Shorts와 라이브 방송을 확실히 구분할 수 없다는 RSS의 한계는 감수했습니다
- 비용과 자격 증명이 필요 없고 쿼터 정책 변경에도 영향을 받지 않습니다
▤ 계속 다시 그려지는 YouTube DOM에서 살아남기
문제: YouTube는 헤더와 콘텐츠를 계속 다시 그리는 SPA입니다. 삽입한 버튼은 사용 중에도 사라질 수 있고, 별다른 격리 없이 추가한 UI에는 페이지 CSS가 그대로 적용됩니다.
모든 UI는 스타일이 격리된 Shadow DOM 속 Svelte로 만들었습니다. 헤더 하위 트리의 MutationObserver가 기준 요소가 다시 나타나는 즉시 폴더 추가 버튼을 재삽입합니다.
- 고정 주기로 재시도하면 계속 폴링하거나 기준 요소가 나타나기 직전에 포기할 수 있어 제외했습니다
- 팝오버는 버튼의 shadow root가 아니라 body에 마운트된 오버레이 호스트에 그려 YouTube 헤더의 변형이나 잘림을 피합니다
- YouTube DOM 기준 요소에 의존하는 한계는 사이드바가 없을 때 표시되는 플로팅 버튼으로 보완했습니다
▒ 계정 없이 Sync Code로 동기화
문제: 기기 간 동기화는 보통 인증을 의미합니다: 이메일 흐름, OAuth, 비밀번호 재설정. 폴더 목록을 동기화하기에는 엄청난 표면적입니다.
256비트 CSPRNG Sync Code가 사용자 식별과 인증을 모두 담당합니다. 서버는 코드의 SHA-256 해시를 키로 폴더를 저장하므로 데이터베이스에 실제 코드를 보관하지 않습니다. 처음 연결할 때는 양쪽 기기의 데이터를 합치고, 이후에는 폴더별로 last-write-wins와 tombstone 삭제 규칙을 적용합니다.
- v1에서는 계정을 도입하지 않았지만 나중에 더 강력한 인증 방식으로 코드를 대체할 수 있게 스키마를 설계했습니다
- 레코드를 바로 지우지 않고 tombstone을 남겨 다른 기기의 오래된 사본이 삭제한 폴더를 되살리지 못하게 했습니다
- 코드를 잃으면 클라우드 연결도 잃는다는 한계가 있습니다. 향후 서버 기능을 위해 폴더 데이터는 서버에 평문으로 저장합니다
▓ 사용자의 요청 제한을 지키는 폴링
문제: 새 영상 표시를 갱신하려면 각 사용자의 IP에서 백그라운드로 RSS를 확인해야 합니다. 요청 간격을 조절하지 않으면 YouTube에서 429 응답을 받을 수 있습니다. MV3 서비스 워커에서는 Chrome이 몇 초 뒤 프로세스를 종료하므로 일반적인 반복 루프도 사용할 수 없습니다.
약 30분마다 chrome.alarms가 워커를 깨웁니다. 모든 폴더에서 중복 채널을 제거한 뒤 ETag 조건부 요청, 제한된 동시성, jitter, 429 backoff를 적용해 피드를 가져옵니다.
- 서버 폴러를 추가하면 비용과 자격 증명 문제가 다시 생기므로 제외했습니다
- 알람은 워커가 종료돼도 유지되지만 타이머는 함께 사라지므로
setInterval도 사용하지 않았습니다 - 요청 횟수를 줄이는 대신 새 영상 표시가 최대 30분 늦어질 수 있습니다
▞ 뒤로 가기가 자연스럽게 작동하는 전체 화면 피드
문제: 처음에는 피드를 사이드 패널로 만들었지만 공간이 좁고 페이지 이동 때마다 사라졌습니다. 영상을 본 뒤 뒤로 가기를 누르면 피드가 아니라 그 아래의 원래 페이지로 돌아갔습니다.
이제 피드는 일반 페이지처럼 YouTube 콘텐츠 영역 전체를 사용합니다. 폴더 ID를 URL 해시에 넣고 열 때 방문 기록을 추가해 뒤로 가기, 앞으로 가기, 새로고침 후에도 피드를 복원합니다. 뒤쪽 콘텐츠는 단순히 덮지 않고 숨기며 스크롤도 잠급니다.
- 실제 사용에서 사이드 패널의 문제가 드러나 기존 결정을 교체했습니다(ADR 0004 → 0007 / 0010)
- 기존 영역을 숨겨 백그라운드 렌더링과 의도치 않은 스크롤을 막습니다
- 상단 바와 사이드바는 유지돼 특정 폴더만 보여주는 YouTube 구독 페이지처럼 보입니다
░ 운영체제가 아닌 YouTube 테마 따르기
문제: YouTube에서는 사용자가 운영체제와 별개로 사이트 테마를 고를 수 있습니다. prefers-color-scheme만 따르면 어두운 페이지 위에 밝은 확장 프로그램 패널이 표시될 수 있습니다.
확장 프로그램은 YouTube 루트 요소의 dark 속성을 관찰해 현재 페이지에 적용된 테마를 그대로 반영합니다. 사용자가 사이트 테마를 바꾸면 즉시 함께 전환됩니다.
- 운영체제가 아닌 호스트 페이지의 테마가 기준이므로 표준 미디어 쿼리는 사용하지 않았습니다
- 속성 하나만 MutationObserver로 관찰하므로 성능 부담은 거의 없습니다
- 운영체제와 사이트 테마 조합에 관계없이 페이지와 자연스럽게 어울립니다