CommentIQ 구축하기: YouTube 댓글 분석을 위한 온디바이스 (On-Device) AI
요약
Gemini Nano를 활용하여 YouTube 댓글을 기기 내에서 분석하는 Chrome 확장 프로그램 'CommentIQ'의 개발 과정을 다룹니다. API 없이 Shadow-DOM을 탐색하고 SPA 환경에서 데이터를 안정적으로 추출하는 기술적 해결책을 공유합니다.
핵심 포인트
- Gemini Nano를 이용한 온디바이스 감성 분석 및 토픽 클러스터링 구현
- MutationObserver와 강제 스크롤을 활용한 YouTube 댓글 지연 로딩 대응
- SPA 환경에서의 History API 대응 및 컨텐츠 스크립트 관리 전략
- 확장 프로그램 팝업과 컨텐츠 스크립트 간의 연결 유실 문제 해결
CommentIQ는 Gemini Nano를 사용하여 모든 YouTube 영상의 댓글 섹션을 읽고 감성 분석 (Sentiment Analysis), 토픽 클러스터링 (Topic Clustering), 질문 추출 (Question Extraction)을 완전히 사용자의 기기 내에서 수행하는 Chrome 확장 프로그램입니다. API 호출도 없고, 백엔드도 없으며, 그 어떤 것도 브라우저를 벗어나지 않습니다. 이 글은 실제로 우리를 힘들게 했던 부분들에 대한 기록입니다.
API 없이 댓글 가져오기
YouTube는 수년 전에 댓글에 대한 공개 API 접근을 차단했습니다. 이는 댓글을 읽을 수 있는 유일한 방법이 페이지 자체에서 읽는 것뿐임을 의미합니다.
YouTube는 Polymer와 커스텀 엘리먼트(Custom Elements)인 ytd-comment-renderer, ytd-comment-thread-renderer 등을 기반으로 구축되어 있습니다. 이들은 일반적인 HTML이 아닙니다. 여러분이 원하는 텍스트는 각 렌더러(Renderer)의 여러 Shadow-DOM 레이어 깊숙이 중첩된 #content-text 엘리먼트 안에 위치합니다. 셀렉터(Selector)를 알게 되면 쿼리(Query)하는 것은 간단하지만, 어려운 점은 언제 쿼리하느냐입니다.
페이지가 로드될 때는 DOM에 댓글이 존재하지 않습니다. YouTube는 사용자가 페이지 하단으로 스크롤할 때 댓글을 지연 로딩 (Lazy-loading)합니다. 우리는 처음에 탐색 후 고정된 지연 시간을 기다리는 방식을 시도했으나, 이는 분명 취약한 방식이었기에 ytd-comments를 감시하는 MutationObserver로 전환했습니다:
const observer = new MutationObserver(() => {
const threads = document.querySelectorAll('ytd-comment-thread-renderer');
if (threads.length > 0) {
...
이 방식은 YouTube의 intersection-observer 기반 지연 렌더링 (Lazy Render) 문제에 부딪힐 때까지는 작동했습니다. 스레드(Threads)가 DOM에는 존재하지만, 화면에 보일 때까지 내부 콘텐츠가 채워지지 않는 현상입니다. 결국 우리는 첫 번째 배치를 강제로 렌더링하기 위해 프로그래밍 방식으로 스크롤을 발생시킨 뒤, 즉시 다시 스크롤을 올리는 방식을 사용했습니다. 편법(Hacky)이긴 하지만 신뢰할 수 있는 방법입니다.
YouTube는 SPA이며 여러분을 놀라게 할 것입니다
더 큰 탐색 문제는 YouTube가 영상 사이에서 전체 페이지 로드를 수행하지 않는다는 점입니다. YouTube는 History API를 사용하는 싱글 페이지 애플리케이션 (SPA)입니다. 여러분의 컨텐츠 스크립트 (Content Script)는 탭당 한 번 로드되며, 사용자가 영상에서 영상으로 클릭하며 이동하는 동안 계속 실행됩니다.
chrome.webNavigation.onHistoryStateUpdated는 각 비디오 탐색 시 발생하지만, 새로운 페이지의 DOM이 준비되기 _전(before)_에 발생합니다. 따라서 우리는 각 탐색 이벤트마다 디바운스 (Debounce)를 적용하고 옵저버 (Observer) 설정을 다시 실행해야 했으며, 이 과정에서 이전 비디오에서 진행 중이던 분석이 있다면 반드시 먼저 취소하도록 처리했습니다.
또한, 세션 중간에 확장 프로그램 팝업 (Extension Popup)이 컨텐츠 스크립트 (Content Script)와의 연결을 잃어버리는 문제도 겪었습니다. 이를 해결하기 위해 효과적이었던 패턴은 팝업과 컨텐츠 스크립트 사이에 port를 열어두고, onDisconnect를 감지하여, 조용히 실패하는 대신 "페이지를 새로고침하세요"라는 안내를 보여주는 것이었습니다.
컨텍스트 제한 (Context Limit)에 걸리지 않고 Gemini Nano에 데이터 공급하기
Chrome의 내장 프롬프트 API (Prompt API, window.ai.languageModel을 통해 제공)에는 컨텍스트 윈도우 (Context Window)가 있습니다. 인기 있는 영상은 수천 개의 댓글을 가질 수 있는데, 이를 한꺼번에 보내면 즉시 제한을 초과하게 됩니다.
우리는 각 요청을 보내기 전에 먼저 토큰 용량을 확인합니다:
const session = await window.ai.languageModel.create();
const available = session.tokensLeft;
그 다음, 댓글들을 제한 범위 내에 여유롭게 들어갈 수 있는 청크 (Chunk) 단위로 배치 (Batch) 처리하고, 각 청크에 대해 분석을 실행한 뒤 결과를 병합합니다. 감성 분석 (Sentiment Analysis)의 경우 이는 청크 전체의 점수를 평균 내는 것을 의미합니다. 주제 클러스터링 (Topic Clustering)의 경우, 원본 댓글 대신 청크 출력물에 대해 두 번째 요약 (Summarisation) 단계를 실행합니다.
또한 모델은 처음 사용하기 전에 다운로드되어야 합니다. window.ai.languageModel.capabilities()는 { available: 'readily' | 'after-download' | 'no' }를 반환합니다. 상태가 after-download인 경우 진행 표시기를 보여주고 readily로 바뀔 때까지 폴링 (Polling)합니다. 상태가 no인 경우 — 보통 장치가 하드웨어 요구 사항을 충족하지 못하기 때문입니다 — 조용히 실패하는 대신 명확한 메시지를 표시합니다.
출력 구조화하기
Gemini Nano의 원시 출력은 JSON이 아닙니다. 우리는 구조화된 출력을 명시적으로 요청하는 프롬프트를 작성한 뒤 이를 파싱하며, 모델이 지침을 벗어날 경우를 대비한 폴백 (Fallback) 로직을 갖추고 있습니다:
const prompt = `
이 YouTube 댓글들을 분석하고 다음 형태의 유효한 JSON만 반환하세요:
{ "sentiment": { "positive": 0-100, "neutral": 0-100, "negative": 0-100 },
...
폴백(fallback) 방식은 정규 표현식(regex)을 사용하여 모델이 반환한 어떤 텍스트에서든 숫자와 리스트를 추출합니다. 이는 엄격한 JSON 파싱(parsing)이 실패하는 사례의 약 95%를 커버합니다.
백엔드 없는 배포
Margin과 마찬가지로, 저희는 서버 인프라가 전혀 없는 환경을 원했습니다. 모든 것은 chrome.storage.local에 저장됩니다. 분석 결과는 비디오 ID별로 캐싱(cached)되므로, 동일한 비디오에서 확장 프로그램을 다시 열면 즉시 실행됩니다.
CommentIQ가 외부로 보내는 유일한 요청은 Pro 티어의 키 검증을 위해 Lemon Squeezy의 공개 라이선스 API(License API)로 보내는 요청뿐입니다. 확장 프로그램 내에 비밀 키(secret)가 내장되어 있지 않으며, 해당 API는 클라이언트 측(client-side)에서 호출되도록 설계되었습니다. 라이선스 확인은 활성화 시 한 번 실행되며 그 결과는 로컬에 캐싱됩니다. 따라서 이후 실행 시에는 네트워크를 전혀 사용하지 않습니다.
Chrome 웹 스토어 심사를 위해 이러한 데이터 흐름을 명시적으로 문서화해야 했습니다. 심사관들은 activeTab 및 scripting 권한이 잠재적으로 광범위하다고 지적했습니다. 이에 대해 저희는 개인정보 처리방침(privacy policy)을 통해 youtube.com/*의 ytd-comment-renderer 요소에서만 데이터를 읽으며, 해당 데이터를 절대 전송하지 않는다는 점을 명확히 설명하여 대응했습니다.
개선하고 싶은 점
멀티 패스 청킹(multi-pass chunking)은 댓글 섹션이 밀집된 비디오에서 체감될 정도의 지연 시간(latency)을 발생시킵니다. 전체 병합(merge)을 기다리는 대신 각 청크가 완료될 때마다 결과를 보여주는 스트리밍 UI(streaming UI)를 도입한다면 이를 더 잘 숨길 수 있을 것입니다. 이는 개선 목록에 올라와 있습니다.
또 다른 점은, YouTube의 DOM이 A/B 테스트 변형에 따라 얼마나 자주 변하는지 과소평가했다는 것입니다. YouTube가 서로 다른 레이아웃을 테스트하고 있었기 때문에 개발 도중 댓글 컨테이너 선택자(selector)가 두 번이나 깨졌습니다. 폴백(fallback)을 포함한 더 방어적인 선택자 전략을 사용했다면 디버깅 시간을 절약할 수 있었을 것입니다.
CommentIQ는 Chrome 웹 스토어에 출시되었습니다. 무료로 설치할 수 있으며 계정은 필요하지 않습니다. 질문이나 피드백은 댓글로 환영합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기