Social Fetcher URL Structure Audit — 2026-07-19 관련 코드: social-fetcher.router.ts(classifyYoutube) · fetchers/youtube.fetcher.ts 관련 데이터: samples/youtube.json · samples/unknown.json
소스 채택 권장도low YouTube은 밈코인 모멘텀이 실시간으로 형성되는 채널이 아니다(제작~업로드 지연이 밈코인 분/시간 단위 변동성과 안 맞음). 현재 구현(무료 oEmbed)은 title/author_name/author_url/thumbnail_url만 반환하는데 트레이더 렌즈에서 전부 context 이하(actionable 0건) — 매매 판단을 바꾸는 필드가 하나도 없다. 유료 Apify 티어를 붙여야 처음으로 actionable 필드(numberOfSubscribers, channelJoinedDate, isChannelVerified)가 생기지만 이는 '모멘텀 발굴용'이 아니라 '이미 발견한 언급의 사칭/러그 판별용' 좁은 용도다. 트레이더 관점: 밈코인 트레이더는 유튜브를 보고 진입 타이밍을 잡지 않는다 — X/Telegram이 초 단위 하이프의 무대이고 유튜브는 그보다 몇 시간~며칠 늦게 따라오는 후행 지표다. 이 소스가 쓸모 있는 유일한 순간은 '유튜버가 이 토큰을 홍보하는데, 이 채널 진짜냐 방금 만든 가짜 계정이냐'를 확인할 때뿐이다 — 그때도 무료 oEmbed로는 판단 불가하고 유료 Apify의 구독자수+개설일+인증배지 조합이 있어야 한다. 즉 1차 신호 소스가 아니라 러그/사칭 검증용 2차 보조 소스로만 조건부 채택할 가치가 있다.
✅ 최종 결정 (수기 audit 반영) 원칙: sourceKey는 unique & immutable & linkable — content에도 그 키가 있어야 creator로 분기 없이 연결. 같은 채널이 handle/UCxxx/옛핸들로 분기되면 안 됨.
조인 키
객체
키
비고
Creator (channel)
channelId (UCxxx)
video.snippet.channelId가 영상 응답에 직접 포함 → channelId를 canonical 조인/dedup 키로. handle/customUrl/user-name은 alias(router sourceKey)이고 channels.list?forHandle/forUsername으로 channelId 해석. ⚠ 현재 fetcher는 oEmbed라 channelId 미제공(handle만) → Data API v3 전환 후 확보, 그전엔 handle 폴백.
youtube.com/watch?v={videoId} — 표준 단일 영상 URL. videoId는 쿼리 파라미터 v로 전달됨. 관측
videoId
11자 base64url-like 문자열(A-Za-z0-9_-). 대소문자 구분(case-sensitive) — 예: rkJAgXQYWAI, 5MaWkuCz-G8. 다른 케이스는 다른 영상을 가리키므로 정규화 시 소문자 변환 금지. 관측
youtu.be 단축 URL
youtu.be/{videoId} — watch URL의 축약형, 리다이렉트로 동일 영상 도달. si= 등 공유 트래킹 쿼리 파라미터가 흔히 붙음(핵심값 아님, 무시 가능). 관측
si 쿼리 파라미터
공유 링크에 붙는 트래킹 식별자(예: ?si=XhdPNKpzampT0k-6). 영상 식별과 무관하므로 videoId 추출 시 제거 대상. 관측
channel handle (@handle)
youtube.com/@{handle} — 2022년 이후 채널의 사람이 읽을 수 있는 고유 식별자(핸들). @ 접두사 필수, 대소문자 무관하게 매칭되나 표기는 원문 유지(예: @dexerto, @FourMeasure). 트레일링 슬래시(/@dexerto/)는 동일 채널을 가리킴. 관측
legacy /user/{username} URL
youtube.com/user/{username} — 구버전 커스텀 사용자명 기반 채널 URL. 신규 발급 중단, 기존 채널만 유지. 관측 표본에서 fetch 시 not_found 다수(oEmbed 등 API가 handle/channelId 미지원 형태로 처리 못함 가능성). 관측
channelId (/channel/{UC...})
youtube.com/channel/{channelId} — 불변의 내부 채널 고유 ID, UC로 시작하는 24자 문자열. handle과 달리 채널명 변경에도 불변. 관측
channel sub-path (/videos, /featured 등)
채널 URL 뒤에 붙는 탭 경로. /videos는 해당 채널의 업로드 영상 목록 탭. 채널 자체 식별에는 불필요하며 스킴상 {handle 또는 user}/{탭} 형태로 나타남. 관측
video vs shorts
YouTube는 일반 영상(watch?v=)과 짧은 세로형 영상인 Shorts(youtube.com/shorts/{videoId})를 구분 제공. 관측 표본에는 shorts URL이 없었으나 실제 존재하는 URL 스킴이므로 파서 설계 시 고려 필요. 웹리서치/실측
playlist
youtube.com/playlist?list={playlistId} 또는 watch?v={videoId}&list={playlistId} — 영상 모음. playlistId는 videoId와 별개 네임스페이스(보통 PL/UU/OL 등 접두사). 웹리서치/실측
channel vs handle
channel은 내부적으로 channelId(UC...)로 식별되는 불변 엔티티이고, handle(@name)은 그 채널에 부여된 변경 가능한 사람이 읽는 별칭. 하나의 channelId는 시점에 따라 다른 handle을 가질 수 있음. 웹리서치/실측
authorName / authorUrl (oEmbed)
YouTube oEmbed API가 반환하는 필드로 영상 업로더의 채널명(authorName)과 채널 URL(authorUrl, 보통 @handle 형태). 관측 표본에서 fixed(고정값) 성격으로 분류됨 — 영상 자체와 함께 불변. 관측
thumbnailUrl (i.ytimg.com)
i.ytimg.com/vi/{videoId}/hqdefault.jpg 형태의 영상 썸네일 CDN URL. videoId로부터 결정적으로 파생 가능. 관측
subscriber count
채널을 구독한 사용자 수. 채널 단위 지표이며 영상 단위 지표(view/like)와 별개 — YouTube Data API의 channels.statistics에서 조회. 웹리서치/실측
view count
영상이 재생된 횟수. 영상 단위 지표, videos.statistics에서 조회 가능. 웹리서치/실측
like count
영상에 좋아요를 누른 수. dislike count는 2021년 이후 공개 API에서 비공개 처리됨. 웹리서치/실측
www 접두사 유무
youtube.com과 www.youtube.com은 동일 서비스를 가리키며 관측 표본 대부분 www 포함이나 도메인 정규화 시 www 유무는 무시 가능. 관측
1b. URL 패턴
https://www.youtube.com/watch?v=rkJAgXQYWAI
표준 시청 URL — videoId가 유일 식별자, 쿼리 파라미터(si, list 등)는 부가정보로 무시 가능
https://youtu.be/audhiM3Ur1I?si=XhdPNKpzampT0k-6
단축 URL — path 전체가 videoId, si= 트래킹 쿼리는 버림(관측 샘플 중 개행+텍스트 오염 사례 존재, 파싱 시 첫 세그먼트만 취해야 함)
https://www.youtube.com/@dexerto/videos
핸들 프로필 — @ 포함 handle이 식별자, 대소문자는 원문 보존되나 router는 key를 소문자화; 뒤에 /videos 같은 탭 세그먼트가 붙을 수 있음(무시 가능)
Shorts 재생 URL — 미관측이나 router classifyYoutube가 이미 처리 대상으로 분기시킴(watch/shorts/live/embed/v 동일 취급)
videoId
관측 0건
unobserved in sample; 코드 로직(classifyYoutube)상 지원 확인됨
youtube.com/live/{videoId}
라이브 스트림 재생 URL — 미관측, 코드상 지원
videoId
관측 0건
unobserved in sample; router 지원
youtube.com/embed/{videoId}
임베드 플레이어 URL — 미관측, 코드상 지원
videoId
관측 0건
unobserved in sample; router 지원
youtube.com/v/{videoId}
구형 Flash 임베드 URL — 미관측, 코드상 지원(레거시)
videoId
관측 0건
unobserved in sample; router 지원
youtube.com/c/{name}
커스텀 URL(레거시, 2022년경 @handle로 대체 진행 중) — 미관측이나 router가 명시적으로 처리
customName (소문자)
관측 0건
unobserved in sample; router 명시 지원. 웹지식: 신규 채널은 발급 중단, 기존 채널은 유지
youtube.com/playlist?list={playlistId}
재생목록 URL — 완전 미관측 + router 미지원(sourceType='youtube' 단일분류 로직에 playlist 케이스 없음). 현재 코드에서 실제로 어떻게 떨어지는지 별도 확인 필요
playlistId (PL/UU/FL/RD 등 prefix)
관측 0건
unobserved in both samples; 웹지식만 근거. classifyYoutube 요약에 playlist 언급 없음 — unknown 또는 오분류 가능성, 코드 재확인 권장
2. sourceType / sourceKey 판정 👤 판단 반영
2.1 sourceType 적합도 👤 판단 반영
패턴
현재 type
판정/제안
sourceKey
conf
근거
사용자 결정
youtube.com/watch?v={videoId}
youtube (병합)
youtube_video
yt-video-watch
high
YoutubeFetcher는 routed.sourceUrl 원본을 그대로 youtube.com/oembed 에 넘긴다. YouTube 공식 oEmbed 는 video URL에서만 title/author/thumbnail 을 안정적으로 반환하고, 채널류 URL은 fetcher 주석에도 '401/403 가능 → blocked' 로 명시돼 있다. 즉 video와 channel은 fetch 성공률·회수 데이터가 근본적으로 다른데 지금은 통계·모니터링에서 구분이 안 된다. trader-value 관점에서도 video=콘텐츠/모멘텀(펌핑 영상 존재 여부), channel=팀 정체성·도달(rug-risk·팔로워 규모류)로 판단축이 다르다.
✅ youtube_video 확정(videoId=콘텐츠 키). 기존 제안 유지.
youtube.com/user/{name}
youtube (channel/{id} 와 합쳐서 집계)
youtube_channel
yt-channel-user
high
채널류(user/channel/c/@handle) 전부 oEmbed가 사실상 실패하는 동일 카테고리이므로 이들끼리는 병합이 맞다. video 그룹과는 split. observed=159로 video(246) 대비 채널류 합계(159+159+104+3+2=427)가 오히려 더 크므로, 현재 단일 'youtube' 타입 아래에서는 fetch 실패(error)가 다수를 차지할 가능성이 높다는 게 중요한 커버리지 시사점.
✅ youtube_channel로 통합. canonical 키=channelId(user-name은 alias→forUsername resolve). 기존 제안 유지.
youtube.com/channel/UC{22 chars}
youtube (user/{name} 와 합쳐서 집계, key만 원형 보존)
youtube_channel
yt-channel-id
high
채널 정준 ID. handle/username과 표현만 다를 뿐 트레이더 관점에서 동일 카테고리(팀 정체성·도달)이며, oEmbed 미지원이라는 fetch 특성도 동일. video 그룹과는 분리(merge into channel bucket, split from video).
✅ youtube_channel. /channel/은 UCxxx=channelId 직접. 기존 제안 유지.
youtube.com/@{handle}
youtube
youtube_channel
yt-handle
high
2022년 이후 표준 핸들. channel/user/c 와 동일 카테고리(채널 정체성). observed=104로 무시 못할 볼륨.
✅ youtube_channel. handle=alias, canonical=channelId(forHandle resolve). 분기 방지 통일.
youtu.be/{videoId}
youtube
youtube_video
yt-video-short
medium
단축 URL, video 그룹에 속함(watch?v= 와 동일 fetch 경로). 다만 observed 표본에 si= 뒤 개행/텍스트 오염 사례가 있어 — 이건 sourceType 분류 이슈가 아니라 상류 스크레이핑 데이터 정제 이슈. 오염된 URL은 router.normalize()에서 파싱 실패로 unknown 처리될지 확인 필요(정규화 로직 직접 추적은 안 함, 표본 근거로만 플래그).
✅ youtube_video. 기존 제안 유지.
youtube.com/@{handle}/{tab}
youtube (tab 무시, key=handle)
youtube_channel
yt-handle-tab
high
router가 이미 segments[0]만 보고 tab을 버리므로 @handle과 동일 key로 정규화됨. 채널 그룹에 합류.
✅ youtube_channel(tab 무시). canonical=channelId. 기존 제안 유지.
youtube.com/user/{name}/videos
youtube (tab 무시, key=username)
youtube_channel
yt-user-videos-tab
high
router가 이미 segments[1]만 보고 /videos tab을 무시. user/{name}과 동일 취급 — 채널 그룹.
✅ youtube_channel. canonical=channelId. 기존 제안 유지.
youtube.com/shorts/{videoId}
youtube (코드상 지원, 미관측)
youtube_video
yt-shorts
medium
router가 이미 videoId를 원형 보존해 뽑아내고 있음. video 그룹에 속함. 미관측이라 실사용 빈도는 확인 안 됨.
✅ youtube_video. 기존 제안 유지.
youtube.com/live/{videoId}
youtube (코드상 지원, 미관측)
youtube_video
yt-live
medium
라이브 스트림도 video 그룹. 다만 라이브는 oEmbed 응답이 진행 중 상태에 따라 title이 변하는(스트림 제목 변경 가능) mutable 가능성 — video인데도 valueNature가 watch류와 다를 수 있어 field-necessity 판정관에 별도 플래그 권장.
✅ youtube_video. 기존 제안 유지.
youtube.com/embed/{videoId}
youtube (코드상 지원, 미관측)
youtube_video
yt-embed
medium
video 그룹. 실제 트윗/사이트 임베드에서 흔한 패턴이라 미관측이어도 향후 유입 가능성 있음.
✅ youtube_video. 기존 제안 유지.
youtube.com/v/{videoId}
youtube (코드상 지원, 미관측)
youtube_video
yt-v-legacy
low
구형 Flash 임베드, 사실상 사멸한 패턴. video 그룹에 합류시키되 실사용 가능성 낮음 — 유지보수 우선순위 낮음.
✅ youtube_video. 기존 제안 유지.
youtube.com/c/{name}
youtube (코드상 지원, 미관측)
youtube_channel
yt-c-legacy
medium
레거시 커스텀 URL, 채널 그룹. 신규 채널 발급은 중단됐지만 기존 채널은 계속 노출되므로 완전 죽은 패턴은 아님.
✅ youtube_channel. customName=alias, canonical=channelId. 기존 제안 유지.
youtube.com/playlist?list={playlistId}
youtube (실질적으로 오분류 — 코드 확인 결과 sourceKey='playlist' 리터럴 문자열로 떨어짐, 실제 playlistId 유실)
youtube_playlist (또는 최소한 key 추출 버그 수정)
yt-playlist
high
router 코드 직접 확인: classifyYoutube에서 segments[0]==='playlist' 는 shorts/live/embed/v/watch/@/channel/c/user 어느 분기에도 안 걸리고 최종 catch-all { sourceType: 'youtube', sourceKey: this.lc(first) || null } 로 떨어져 sourceKey가 실제 playlistId(list= 쿼리파라미터)가 아니라 리터럴 'playlist' 문자열이 된다. 이는 순수 라우팅 버그. 다만 trader-value 관점에서 playlist는 개별 영상/채널 정체성보다 우선순위 낮은 컨텐츠 묶음이라 fetcher 신규 구현까지는 낮은 ROI — 최소한 오분류(key 유실)만은 수정 필요.
unknown (host 매칭 목록에 없음 — youtube.com/m.youtube.com/youtu.be만 포함)
youtube_video (host 목록에 추가)
yt-host-music
medium
router의 host 분기(social-fetcher.router.ts classify)는 youtube.com·m.youtube.com·youtu.be만 처리. music.youtube.com은 다른 host라 전부 'unknown' 또는 apex-domain 기반 'website' 로 새 버킷. 밈코인 테마곡/음원 홍보 URL이 존재할 수 있어 완전 무가치는 아니나, 실측 표본에 미관측이라 우선순위는 낮음.
✅ youtube_video로 흡수(host 확장 music.youtube.com). 기존 제안 유지.
프라이버시 강화 임베드용 별도 도메인. 웹사이트 임베드에서 흔히 쓰이나 밈코인 소셜 URL 필드에 직접 등장할 확률은 낮음(주로 iframe src). 실측 미관측 + 신호가치 낮아 우선순위 최하.
✅ youtube_video로 흡수(host 확장 youtube-nocookie.com). 기존 제안 유지.
2.2 id 추출 적합도 👤 판단 반영
패턴
추출 id
resolve?
판정
conf
근거
사용자 결정
youtube.com/watch?v={videoId}
videoId (대소문자 보존)
✅
ok
high
live-test 직접 확인: oEmbed(youtube.com/oembed?url=...watch?v=rkJAgXQYWAI)가 200으로 title/authorName/authorUrl/thumbnailUrl 반환. videoId는 YouTube 전역 불변 식별자이며 sourceKey와 정확히 일치 — 진짜 고유 식별자.
✅ videoId=콘텐츠 키 확정.
youtube.com/user/{name}
username (소문자화)
❌
wrong
high
live-test 직접 확인: oEmbed(/user/DaveThomasFoundation)은 404 not_found — 무료 경로로는 아예 이 sourceKey를 소비 못 함. 더 근본적으로 username은 채널의 '진짜' 식별자가 아니라 레거시 별칭(alias)이고, 실제 고유키는 channelId(UC...)임(id-probe: curl -I가 200/no-redirect만 반환, canonical id는 노출 안 됨). 같은 채널이 /channel/UCxxx 형태로도 등장하면 서로 다른 sourceKey가 나와 dedup 실패 — Social-field loss filter 연구의 sameHandle 판별 로직이 이 alias 불일치로 같은 계정을 다른 계정으로 오판할 위험.
✅ router키=alias(소문자). canonical=channelId(forHandle/forUsername resolve)로 통일 — 분기 방지.
youtube.com/channel/UC{22 chars}
channelId (대소문자 보존)
✅
ok
high
id-probe 확인: /channel/UCbrPqq29C9Q_TQP7OFFRzcw는 curl -I에서 200/no-redirect — 이미 canonical 형태. 별도 Apify 라이브 테스트에서도 @dexerto가 실제로 resolve되는 진짜 채널 식별자가 channelId(UCWWCHrEqzME9N6JTuL6YUaw)임을 재확인 — channelId 자체는 진짜 고유·불변 식별자. 다만 현재 무료 oEmbed 경로는 이 형태를 404 not_found로 소비 못함(별도 fetcher 이슈, id-fit 문제 아님).
✅ UCxxx=channelId 직접, canonical 키.
youtube.com/@{handle}
handle (소문자화)
❌
wrong
high
live-test 직접 확인: oEmbed(/@dexerto) 404 not_found — 무료 경로 소비 불가. 결정적으로 유료 Apify 테스트가 sourceKey=dexerto(handle)와 실제 채널 식별자(channelId=UCWWCHrEqzME9N6JTuL6YUaw)가 다르다는 'id-fit gap'을 직접 확인함. handle은 채널 소유자가 변경 가능한 mutable alias라 시간에 따라 같은 channelId가 다른 handle로 바뀔 수 있음 — 장기 dedup/재식별에 부적합. sameHandle 기반 사칭탐지 로직이 이 alias 위에서 동작 중이면 handle 변경 시 오탐/누락 위험.
✅ router키=alias(소문자). canonical=channelId(forHandle/forUsername resolve)로 통일 — 분기 방지.
youtu.be/{videoId}
videoId
✅
ok
high
live-test 이중 확인: (1) oEmbed(youtu.be/audhiM3Ur1I)가 200으로 정상 필드 반환, (2) id-probe(curl -I)에서 youtu.be가 303으로 watch?v=audhiM3Ur1I(동일 id)로 HTTP 리다이렉트 — sourceKey로 추출한 videoId가 canonical id와 정확히 일치함을 별도 채널로 재확인. 진짜 고유 식별자 확정.
✅ videoId=콘텐츠 키 확정.
youtube.com/@{handle}/{tab}
handle (tab 제거)
❌
wrong
high
live-test 직접 확인: oEmbed(/@techbullion/videos) 404 not_found — bare @handle과 동일한 실패. 근본 원인도 동일(handle=mutable alias, 진짜 식별자는 channelId). #4의 id-fit gap이 그대로 적용됨.
✅ router키=alias(소문자). canonical=channelId(forHandle/forUsername resolve)로 통일 — 분기 방지.
youtube.com/user/{name}/videos
username
❌
wrong
high
live-test 직접 확인: oEmbed(/user/kabosumama/videos) 404 not_found. #2(user/{name})와 동일한 근본 이슈(username alias, 진짜 식별자는 channelId) — tab suffix 유무는 무관.
✅ router키=alias(소문자). canonical=channelId(forHandle/forUsername resolve)로 통일 — 분기 방지.
youtube.com/shorts/{videoId}
videoId
✅
ok
medium
샘플 미관측(observed=0)이라 직접 live-test 대상은 아니었으나, 코드상 classifyYoutube가 watch?v/youtu.be와 동일 경로(videoId 추출, 대소문자 보존)로 처리하도록 확인됨. watch?v·youtu.be 둘 다 live-test에서 oEmbed 성공이 실증됐으므로 동일 로직인 shorts도 유사 확률 높음 — 다만 shorts 자체는 직접 fetch 테스트하지 않아 확신도는 medium.
✅ videoId=콘텐츠 키 확정.
youtube.com/live/{videoId}
videoId
✅
ok
medium
shorts와 동일 근거: 미관측(observed=0)이나 router가 watch?v과 동일 videoId 추출 경로로 분기. oEmbed가 videoId 기반으로 동작함이 이미 실증됐으므로 analogically 안전할 가능성 높지만 직접 테스트는 없어 medium.
✅ videoId=콘텐츠 키 확정.
youtube.com/embed/{videoId}
videoId
✅
ok
medium
shorts/live와 동일 근거(코드 경로 동일, 직접 테스트는 없음) — medium confidence.
✅ videoId=콘텐츠 키 확정.
youtube.com/v/{videoId}
videoId
✅
ok
medium
레거시 Flash embed 형태. 코드상 videoId 추출 경로는 동일하나 관측 0건 + 직접 테스트 없음 — medium confidence.
✅ videoId=콘텐츠 키 확정.
youtube.com/c/{name}
customName (소문자화)
❌
wrong
medium
직접 테스트 대상은 아니었으나(observed=0), /user/{name}·/@{handle} 두 유사 alias 패턴이 이번 세션에서 모두 '진짜 식별자 아님(channelId 별도 존재)'으로 확인됨. /c/{name}도 동일하게 사용자가 설정하는 레거시 커스텀 URL이라 같은 alias 구조 — 동일 id-fit gap 예상, 다만 직접 검증은 안 해 medium.
✅ router키=alias(소문자). canonical=channelId(forHandle/forUsername resolve)로 통일 — 분기 방지.
youtube.com/playlist?list={playlistId}
playlistId (미확인)
—
wrong
low
관측 0건 + classifyYoutube 로직 요약에 playlist 케이스 자체가 언급되지 않음 — 현재 router가 이 URL을 어떤 sourceType/sourceKey로 분류하는지조차 불확실(unknown 처리 또는 다른 youtube 케이스로 오분류 가능). id-probe/live-test 모두 미수행. sourceKey가 진짜 고유 식별자인지 판정할 근거 자체가 없어 '확인 안 됨' 상태로 wrong 처리(안전 쪽) — 코드 재확인이 판정 전제조건.
✅ playlistId 키(youtube_playlist, fetch 보류).
커버리지 갭 — playlist URL(youtube.com/playlist?list=)이 최종 catch-all 분기로 떨어져 sourceKey가 실제 playlistId가 아닌 리터럴 'playlist' 문자열로 오염됨 — 코드 확인된 버그
커버리지 갭 — music.youtube.com, youtube-nocookie.com, gaming.youtube.com(레거시) 등 host가 classify() 의 host 매칭 목록(youtube.com/m.youtube.com/youtu.be)에 없어 전부 미분류(website/unknown)로 샘. 관측 표본엔 없으나 잠재 갭
커버리지 갭 — channel류(user/channel/c/@handle, 합계 observed 427)와 video류(watch/youtu.be, 합계 observed 246)가 동일 sourceType 'youtube' 로 묶여 있는데, YoutubeFetcher는 oEmbed만 호출하고 oEmbed는 video URL 전용이라 채널류는 다수 error/blocked 로 떨어질 가능성이 높음(fetcher 자체 주석이 이를 인정). 두 그룹의 fetch 성공률·데이터 품질을 구분 통계로 볼 수 없는 게 실질적 커버리지 갭
3. Fetch 테스트 + 획득 방법
3.1 획득 사다리 (공짜 → 유료)
방법
비용
설명
트레이드오프
conf
확정 ✅ (2026-07-21)
사용자 선택 완료 — youtube_channel→official:youtube-data-api-v3 ($0 (10,000 quota units/day 무료 티어, channels.list 1 unit/call → 최대 1만 채널 조회/일)) · youtube_video→official:youtube-data-api-v3 ($0 (10,000 quota units/day 무료 티어, videos.list 1 unit/call)) adopted
$0 (10,000 quota units/day 무료 티어, channels.list 1 unit/call → 최대 1만 채널 조회/일) / $0 (10,000 quota units/day 무료 티어, videos.list 1 unit/call)
Phase A 후보 사다리에서 사람이 선택한 확정 source. §4 통합 필드 뷰는 이 source의 실제 스키마 기준.
신규 채택 source는 현행 코드 미구현분이 §4에서 parseStatus=dropped(구현 필요)로 표시됨. 실제 코드 전환·스키마 검증은 반영 단계에서.
Returns title, author_name (channel display name), author_url, thumbnail_url, html embed. No numeric counters at all — no subscriber count, no view count, no channel/video creation date. Works for both video URLs and (undocumented, less reliable) channel-ish inputs but is really designed for videos.
No statistics of any kind, so it cannot satisfy 'subscriber count', 'creation date', or 'view count' requirements. Endpoint is undocumented/unofficial for anything beyond basic oEmbed spec — no published SLA, no rate-limit doc, can change or throttle without notice. Fine as a free first-pass metadata source, dead end for the numeric fields this task actually needs.
high — response shape confirmed by oEmbed spec + multiple independent third-party writeups (oembed.com, Google's own IFrame docs ecosystem); matches what the fetcher in this repo already observes (title/authorName/authorUrl only).
2
YouTube Data API v3 (official, Google Cloud) gated — free tier but requires Google Cloud project + API key + quota budgeting
$0 for the standard 10,000 quota units/day per GCP project (as of the Dec 2025 pricing simplification, YouTube Data API usage itself carries no request pricing — quota is the only constraint). channels.list and videos.list both cost 1 quota unit per call regardless of which 'part' fields are requested (snippet+statistics together still = 1 unit), so 10,000 units/day ≈ 10,000 channel or video lookups/day, comfortably covering a 'low-volume on-demand' backend.
channels.list(part=snippet,statistics) returns subscriberCount, publishedAt (channel creation date), title. videos.list(part=snippet,statistics) returns title, channelTitle/channelId (author), publishedAt, viewCount. This is the only free source in the ladder that actually has the counters/dates required.
Requires a Google Cloud project + API key (5-minute self-serve setup, no credit card, no manual review for read-only key usage under the default quota — review/audit only kicks in if you request a quota increase beyond 10k/day or use OAuth scopes, neither of which applies here). subscriberCount can be hidden by channel owners (returns as hidden/approximate in that case) — a real, documented data gap, not an integration bug. search.list is separately capped at 100 calls/day and costs 100 units/call, but this task doesn't need search — only channels.list/videos.list by known ID, both cheap.
high on quota-cost figures — cross-checked against Google's own quota-cost documentation page directly (not just secondary blog posts). Medium-high on the 'no request pricing since Dec 2025' claim — that specific detail came from secondary SEO/blog sources (getphyllo, blotato, socialcrawl) rather than a Google changelog page fetched directly; treat as directionally right but verify against the live Google Cloud console pricing page before relying on it for a cost model, since these vendor-adjacent blogs have an incentive to overstate 'free-ness' to sell their own tooling around the API.
3
Apify YouTube actors (e.g. streamers/youtube-scraper, apidojo/youtube-scraper) rejected/blocked for this use case — unauthorized-scraper tier, only relevant if official API becomes insufficient
Pay-per-result, priced per 1,000 items and varies a lot by actor: apidojo/youtube-scraper ≈ $0.50/1,000 videos; streamers/youtube-scraper ≈ $2.40/1,000 videos — on top of Apify platform subscription/compute-unit costs (free plan gives ~$5/month credit, cheapest paid plan $29/month). No per-channel pricing was found separately from per-video pricing in what I could verify.
These scrape YouTube's web UI rather than calling the official API, so they can return fields the API also provides (subscriber count, view count, title, publish date) plus some the API doesn't cleanly expose (e.g. monetization status). Field completeness is inconsistent between actors — streamers/youtube-scraper's own listing claims subscriberCount + channelJoinedDate for channels; apidojo/youtube-scraper's listing does not clearly document subscriberCount/join-date in its schema even though it's a similarly-named product from a different publisher.
medium — pricing figures came directly from each actor's own Apify store page (first-party for that vendor, but Apify actor marketplace listings are self-reported by third-party publishers, not Apify itself, so treat exact field lists as unverified marketing copy until tested against a live run). Given YouTube Data API v3 already covers 100% of the stated requirements (subscriber count, channel creation date, title, video title/author/publish date/view count) at zero request cost within a low-volume on-demand quota, there is no functional reason to pay for a scraper here — this tier is dominated by tier 2 and should only be revisited if daily volume were to exceed the ~10,000 units/day free quota, which is not the stated scenario (low-volume, on-demand).
3.2 공식 API 상세
확신도 high for quota mechanics (verified against Google's own quota-cost page); medium for the broader 'no cost at all' pricing narrative (secondary-source only) — subscriberCount can come back hidden/omitted for channels whose owner has disabled public subscriber count display — this is a real data gap in the official API itself, not a fetcher bug, and needs a null-handling path (e.g. fall back to null/unknown rather than treating it as fetch failure). Also: the widely-repeated 'API usage is now fully free / December 2025 pricing change' claim is sourced from secondary SEO blogs (getphyllo, blotato, socialcrawl) in this research pass, not a primary Google announcement I fetched directly — recommend a direct check of Google's official YouTube API terms/pricing page before finalizing any cost writeup, since these are exactly the kind of vendor-adjacent sources that tend to editorialize 'free' claims to drive their own SEO/affiliate traffic.
단계
내용
conf
Create/select a Google Cloud project
console.cloud.google.com → New Project. No billing account required for this quota tier.
high
Enable 'YouTube Data API v3'
APIs & Services → Library → search 'YouTube Data API v3' → Enable.
high
Create an API key
APIs & Services → Credentials → Create Credentials → API Key. Optionally restrict the key to the YouTube Data API and to your server's IP for security. No OAuth/user-consent flow needed since channel/video public statistics are read with a plain API key, not user-authorized data.
high
Call channels.list and videos.list
channels.list(part=snippet,statistics,id={channelId}) → title, publishedAt (creation date), subscriberCount. videos.list(part=snippet,statistics,id={videoId}) → title, channelTitle/channelId, publishedAt, viewCount. Each call = 1 quota unit regardless of how many parts/fields requested in that single call.
high
Budget quota
Default 10,000 units/day per project resets at midnight Pacific Time. At 1 unit/call for channels.list+videos.list, that's up to 10,000 lookups/day — request a quota increase only if this low-volume on-demand use ever grows past that (requires a compliance/use-case audit through Google's quota extension form).
Matches existing implementation exactly: title/authorName/authorUrl/thumbnailUrl all present. oEmbed additionally returns type/height/width/html/provider_name/provider_url/version which the fetcher currently discards.
youtu.be short-link resolves in oEmbed exactly like the watch?v= form; same field set returned. Confirms videoId key is directly usable without needing to expand the short link first for the free path.
Confirms current implementation's status:'not_found' for channel-id URLs — oEmbed genuinely does not support channel-shaped inputs, this is not a fetcher bug.
303 See Other -> location: https://www.youtube.com/watch?v=audhiM3Ur1I&feature=youtu.be
id-probe: youtu.be short link HTTP-redirects to watch?v=<same id>. Canonical videoId == sourceKey extracted by router; no id-fit mismatch for this template.
https://www.youtube.com/@dexerto
curl -sIL (HEAD, follow redirects)
무료
200 OK, no Location header (no HTTP redirect)
id-probe: @handle URLs resolve directly at HTTP layer — YouTube does NOT redirect /@handle to a /channel/{UC...} canonical URL via HTTP. The true channelId (UCWWCHrEqzME9N6JTuL6YUaw for dexerto) is only obtainable by parsing the rendered page or calling an API/scraper (confirmed via the Apify test below), not via curl -I alone.
id-probe: /channel/{id} already is the canonical form and correctly serves 200 without redirecting further.
https://www.youtube.com/user/DaveThomasFoundation
curl -sIL (HEAD, follow redirects)
무료
200 OK, no Location header
id-probe: legacy /user/{name} form also resolves 200 with no HTTP redirect to a canonical /channel/{UC...} id — same limitation as @handle; canonical channelId not recoverable via curl -I.
grep for YOUTUBE_API_KEY / GOOGLE_API_KEY / YT_API in .env.dev and .env.prod found no matching variable (only HELIUS_*, ALCHEMY_*, BINANCE_API_KEY, EXTERNAL_GEMINI_API_KEY, TWITTER_API_KEY, APIFY_TOKEN, URLSCAN_API_KEY are present). Per instructions, no live call attempted; subscriberCount/publishedAt/viewCount fields for this route remain estimates from acquisition research only, not verified in this session.
Confirms acquisition's rank-3 pick works and actually returns the numeric fields oEmbed/legacy sources lack: numberOfSubscribers=880000, channelJoinedDate='Jan 5, 2021', channelTotalVideos=4425, channelTotalViews present, channelId=UCWWCHrEqzME9N6JTuL6YUaw (this IS the canonical id that @dexerto silently resolves to — confirms the id-fit gap noted above). Also returns per-video fields (title, viewCount, likes, commentsCount, duration, date, hashtags, isMonetized, isAgeRestricted) bundled in the same item since the actor is video-centric, not a dedicated channel-only endpoint.
{"title":"This Is My Room — Marisol 60 | Dave Thomas Foundation for Adoption","author_name":"DaveThomasFoundation","author_url":"https://www.youtube.com/@DaveThomasFoundation","type":"video","height":113,"width":200,"version":"1.0","provider_name":"YouTube","provider_url":"https://www.youtube.com/","thumbnail_height":360,"thumbnail_width":480,"thumbnail_url":"https://i.ytimg.com/vi/rkJAgXQYWAI/hqdefault.jpg","html":"<iframe ... src=\"https://www.youtube.com/embed/rkJAgXQYWAI?feature=oembed\" ...></iframe>"}
{"title":"Danny Gonzalez Breaks Down His YouTube Watch History","channelName":"Dexerto","channelUrl":"https://www.youtube.com/channel/UCWWCHrEqzME9N6JTuL6YUaw","channelId":"UCWWCHrEqzME9N6JTuL6YUaw","numberOfSubscribers":880000,"channelTotalVideos":4425,"channelJoinedDate":"Jan 5, 2021","viewCount":39730,"date":"2026-07-16T16:00:14.000Z","isChannelVerified":true, ...}
oEmbed 결과는 기존 구현·샘플 fetched 데이터와 100% 일치 — 6개 템플릿 중 video형(watch?v=, youtu.be/{id}) 2개만 ok, 나머지 channel/user/@handle 계열 4개 템플릿은 전부 404 not_found로 라이브 재현됨. 구현 버그 아님, oEmbed 스펙상 video 전용 한계.
YouTube Data API v3 키가 .env.dev/.env.prod 어디에도 없음(YOUTUBE_API_KEY, GOOGLE_API_KEY, YT_API 패턴으로 grep, 매치 없음). acquisition 문서가 제시한 subscriberCount/publishedAt/viewCount 무료 경로는 이번 세션에서 미검증 상태로 남음 — 키 발급 전까지 '추정'으로만 취급해야 함.
id-probe 결과 youtu.be/{id}만 HTTP 303으로 watch?v={id} canonical로 리다이렉트됨(무료 확인). @handle, /channel/{id}, /user/{name}은 curl -I에서 전부 200을 반환하고 Location 헤더가 없음 — 즉 @handle→UC... 실제 channelId 매핑은 HTTP 리다이렉트로는 절대 얻을 수 없고, 페이지 렌더링/API/스크래퍼로만 확인 가능함(Apify 테스트로 dexerto의 실제 channelId=UCWWCHrEqzME9N6JTuL6YUaw 확인됨).
Apify streamers/youtube-scraper 1회 호출($0.004)로 acquisition의 rank-3 주장(구독자수+channelJoinedDate 제공)이 실측 확인됨. 단, 이 actor는 video-centric이라 채널 단독 조회가 아니라 최소 1개 video item에 채널 필드가 동봉되는 구조 — 순수 채널 통계만 필요해도 최소 1 video의 크레딧이 소모됨.
4. 객체별 필드 통합 (변동성 · 파싱상태 · 트레이딩 유용성) 👤 판단 반영
Creator/Content 객체별로 필드를 한 표에 통합. 값 변동성 = 값이 시간에 따라 변하나(immutable 역사적 사실 / mutable 드리프트 / derivable 계산값) → look-ahead 안전성이 여기서 도출(immutable=safe · mutable=conditional/as-of · derivable=unsafe). 파싱 상태 = 현재 파이프라인이 실제로 저장하나.
4.1 · Creator (채널) — youtube_channel
source: official:youtube-data-api-v3 (channels.list, part=snippet,statistics,contentDetails) · subscriberCount/videoCount/viewCount는 조회 시점 스냅샷(mutable) — as-of 시점 고정 필요, look-ahead 시 진입 이후 값 섞임 주의. publishedAt(채널 개설일)만 immutable이라 진입시점 무관하게 안전하게 재사용 가능. 현재 코드는 v3 API 자체를 호출하지 않음(oEmbed는 채널 URL에서 전부 404) — 아래 필드 전부 신규 구현 필요.
필드
설명
값 변동성
파싱 상태(현재)
최종 결정
트레이딩 유용성
channelId
채널 고유 불변 ID (UC로 시작 24자). channels.list id 또는 snippet.channelId. 예: UCWWCHrEqzME9N6JTuL6YUaw
immutable
⚠️ actor제공·미파싱
🔑 key
actionable — @handle/username은 mutable alias라 dedup/사칭탐지의 진짜 join key는 channelId뿐(현재 라우터는 handle을 key로 써서 alias 불일치 시 동일계정 오분리 위험)
snippet.title
채널 표시명. 예: 'Dexerto'. 소유자가 언제든 변경 가능
mutable
⚠️ actor제공·미파싱
keep
context — 브랜드/사칭 텍스트 매칭(sameHandle 판별)의 identity 키, 값 자체가 매매 트리거는 아님
snippet.customUrl
채널 @handle(정규화된 커스텀 URL). 예: '@dexerto'. 소유자가 변경 가능한 mutable alias
noise — 이미지 자체는 파이프라인에서 분석 안 함, channelId로 언제든 재구성 가능한 파생 성격
snippet.country
채널이 설정한 국가 코드(선택 입력이라 비어있는 채널 다수)
mutable
⚠️ actor제공·미파싱
remove
noise — 러그/모멘텀/팀진정성/타이밍 어느 축과도 무관
statistics.subscriberCount
구독자 수. 채널 소유자가 공개를 끄면 hiddenSubscriberCount=true와 함께 값이 생략됨(정상 데이터 갭, API 결함 아님)
mutable
⚠️ actor제공·미파싱
keep
actionable — 도달 규모(viability) 판단축, 단 절대 도달만 예측하고 진입상대 수익은 별개(project_af_viability_vs_entry_relative와 동일 구조)
statistics.hiddenSubscriberCount
구독자 수 비공개 여부 boolean 플래그
mutable
⚠️ actor제공·미파싱
skip
noise — subscriberCount 결측 원인 설명용 메타플래그일 뿐 그 자체로 매매 신호 아님
statistics.videoCount
채널 누적 업로드 영상 수
mutable
⚠️ actor제공·미파싱
keep
context — 채널 활성도/신생 여부 보조 지표, publishedAt만큼 강한 tell은 아님
statistics.viewCount
채널 전체 영상 누적 조회수 합
mutable
⚠️ actor제공·미파싱
keep
context — 채널 규모감(브랜드 파워) 정도, 개별 홍보 영상의 즉시 확산력과는 별개
verified badge (checkmark)
YouTube 공식 인증 배지 — v3 API의 snippet/statistics/contentDetails part 어디에도 공개 필드로 노출되지 않음(구독자수 임계치 기반 자동 부여이나 API 응답에 boolean으로 없음)
mutable
❌ 없음
—
noise — 애초에 구독자수 임계치로 자동 부여되는 배지라 별도 구매 불가하지만, API로 확보 자체가 불가해 활용 불가
channelAgeDays (파생)
publishedAt 기준 경과일수 — 우리가 raw publishedAt 저장 후 계산해야 하는 파생값
derivable
❌ 없음
skip
actionable이나 look-ahead unsafe — 조회 시점마다 값이 달라지므로 raw publishedAt만 저장하고 진입 as-of 시점 기준으로 재계산 필수
4.2 · Content (영상) — youtube_video
source: official:youtube-data-api-v3 (videos.list, part=snippet,statistics,contentDetails) · viewCount/likeCount/commentCount는 조회 시점 스냅샷(mutable, 영상 공개 이후 계속 누적) — 진입시점 as-of 값과 이후 값 혼동 주의(look-ahead). publishedAt(업로드일)·videoId는 immutable이라 즉시 확정. 현재 코드는 무료 oEmbed로 title/author_name/author_url/thumbnail_url만 받고 통계(view/like/comment)·publishedAt·channelId는 전혀 파싱 안 함 — v3 신규 구현 필요.
필드
설명
값 변동성
파싱 상태(현재)
최종 결정
트레이딩 유용성
videoId
영상 고유 불변 ID(11자). 라우터가 URL에서 이미 원형 보존해 sourceKey로 추출 중(watch?v=, youtu.be 둘 다 live-test로 canonical 일치 확인됨)
immutable
✅ 캡처
🔑 key
actionable — 콘텐츠 재사용/카피캣 탐지의 진짜 dedup 키(project_content_reuse_direction과 동일 축)
snippet.publishedAt
영상 업로드 일시(ISO8601). 한번 게시되면 불변
immutable
⚠️ actor제공·미파싱
keep
actionable — 콘텐츠 신선도(신규 vs 재탕) 판별, look-ahead 안전. 현재 oEmbed는 이 필드를 아예 안 줌
snippet.channelId
업로드한 채널의 불변 ID — Creator 객체의 channelId와 동일 값, join key
immutable
⚠️ actor제공·미파싱
🔑 key
actionable — 사칭/동일계정 재사용 판별의 join key. 현재 oEmbed의 author_url(@handle)은 mutable alias라 대체 불가
per-video flags/metadata not exposed by oEmbed or Data API snippet/statistics parts
다음 액션
sourceType 분리(video/channel) — 단일 youtube를 youtube_video(watch/youtu.be/shorts/live/embed/v)와 youtube_channel(@handle/channel/user/c)로 분리 — 판정 15건 중 다수 merge→channel, split→video로 수렴
playlist 커버리지 갭 수정 — youtube.com/playlist?list= 가 catch-all로 떨어져 sourceKey=리터럴 "playlist" — playlistId 추출 분기 추가. music.youtube.com/youtube-nocookie.com host 갭도 함께
채널 데이터 획득 경로 결정 — 현재 무료 oEmbed는 @handle/user 채널 URL에 404(id-fit 전부 wrong) — 채널급 데이터(구독자/개설일/인증)는 공식 YouTube Data API(무료 quota+키) 또는 Apify 필요. adoption=low이나 러그/사칭 검증용 2차 소스로 조건부
필드 keep 확정 + valueNature 매핑 — oEmbed 상수/중복 필드 drop, 트레이더 actionable(numberOfSubscribers·channelJoinedDate·isChannelVerified·channelTotalVideos)은 채널 소스에서만 확보 가능 — keep 확정 후 SocialFetchResult.valueNature 매핑