Social Fetcher URL Structure Audit — 2026-07-19 관련 코드: social-fetcher.router.ts(classifyTiktok) · fetchers/tiktok.fetcher.ts 관련 데이터: samples/tiktok_profile.json · samples/tiktok_video.json · samples/tiktok_search.json
소스 채택 권장도조건부 채택 — 밈 모멘텀은 여기 사는데(TikTok=주류 바이럴리티의 핵심 확산 채널) 현재 fetcher 매핑이 트레이더가 가장 원하는 두 축(러그리스크의 계정 생성일/인증여부)을 통째로 버리고 있어 반쪽짜리다. playCount/shareCount/authorFans/isAd 등 모멘텀·광고tell 필드는 이미 확보돼 있어 실전 매매 판단에 바로 쓸 수 있다. 하지만 context.md가 명시한 러그리스크 1순위 tell(계정 신선도)과 팀진정성 tell(인증여부)이 raw response엔 존재하는데 tiktok.fetcher.ts가 authorMeta.createTime/verified를 파싱하지 않아 유실되고 있다 — 퀀트 관점의 '있는데 안 씀' 낭비가 아니라 트레이더 관점에선 '가장 쓰고 싶은 것부터 없다'는 문제. 또한 tiktok_profile 라우트가 실제 프로필 집계가 아니라 최신 영상 1건 스냅샷이라 채널 성장추세를 못 보는 구조적 약점, tiktok_search unsupported·단축링크 unresolved로 관측 273+104+49+8+7건 중 일부가 데이터 손실 상태다. 트레이더 관점: TikTok은 밈코인 판에서 X/Telegram 다음으로 실제 바이럴이 시작·증폭되는 채널이라 소스 자체는 채택할 가치가 충분하다(모멘텀 축이 명확히 존재). 다만 지금 코드 그대로면 트레이더는 '얼마나 터졌나'는 알 수 있어도 '이 계정이 진짜인지, 방금 만든 사칭 계정인지'는 알 수 없다 — 러그 판별이라는 트레이더의 1순위 질문에 답을 못 준다는 뜻. authorMeta.createTime/verified 매핑 추가가 field-necessity(퀀트)보다 trader-value 관점에서 더 시급한 우선순위다.
✅ 최종 결정 (수기 audit 반영) 원칙: sourceKey는 unique & immutable & linkable. TikTok handle은 30일마다 변경 가능하고 옛 handle이 타인에게 즉시 재할당되므로, handle을 키로 쓰면 서로 다른 두 계정이 한 키로 조용히 병합된다(분기보다 나쁜 충돌). → handle은 키 불가, 숫자 id가 canonical.
조인 키
객체
키
비고
Creator (프로필)
authorMeta.id (숫자 uid)
유료 실측 3콜 전부에서 content 응답이 authorMeta.id를 추가 쿼리 없이 직접 포함 → 사용자 규칙 충족. handle(authorMeta.name)은 별도 기록하되 조인·dedup에 절대 사용 금지 — 표시용 + URL→handle→id 해석 경로용. 단 handle→id는 시점 종속(as-of): 재할당 때문에 "현재값 1건" 매핑으로 들고 있으면 과거 관측이 새 주인 id에 잘못 붙는다. content 행의 (authorHandle + createTimeISO + authorId)가 곧 handle 이력 로그이므로 별도 alias 테이블 불필요. ⚠ 유료 게이트로 미fetch 엔트리는 id 없이 handle만 남음 → resolved(실측) / inferred(handle 추정) 구분 필요.
Content (영상/포토)
id (postId, 콘텐츠 자체) + authorMeta.id (creator 링크)
postId=dedup 키이며 라우터 sourceKey와 실측 일치. postId가 authoritative — URL의 handle이 완전히 틀려도 apify·oEmbed 모두 정확한 게시물을 반환(실측). photo/video 병합 후 형식 구분은 isSlideshow로 복원.
sourceType 통합 맵
최종 타입
포함 패턴
tiktok_profile
/@{handle}, /@{handle}/live — router sourceKey=handle(alias). canonical=authorMeta.id로 resolve 후 조인
vm.tiktok.com/{code}, tiktok.com/t/{code}, vt.tiktok.com/{code} — 상류 수집 단계에서 HEAD 301로 canonical URL로 펼쳐 저장하기로 결정(실측 6/6 해석 성공, 무료). 정규화가 성공하면 router엔 canonical만 도달하므로 이 타입은 쓰이지 않음. 만료·네트워크 실패로 raw shortlink가 남은 잔여분에 한해 안전망으로만 존치 (특히 vt.tiktok.com은 host whitelist에 없으면 website로 새어 sourceKey='tiktok.com' 오염)
tiktok_search
/search?q, /search/video?q — 분류 유지, fetcher 미구현(unsupported) 상태 유지
tiktok_hashtag / tiktok_music (보류)
/tag/{hashtag}, /music/{title}-{musicId} — 신규 타입 검토만, 우선순위 낮음. 단 사운드 축은 content의 musicMeta.musicId로 먼저 관측
unknown 유지
빈 path(//), oembed?url=(빈 값), /{lang}/trending/detail/{slug} — 식별자 복원 불가하거나 극희소
이 문서에서 쓰는 pill 범례
관측 source-data에서 실제 발견됨웹리서치/실측 검색·curl·실제 호출로 확인정상/유지차단·버그·갭·drop신규/분리 타입fixed / mutableconfidence: highmediumlow
tiktok.com/@{handle} 형태. 크리에이터/브랜드 계정 페이지. 표본 273건 전량 이 단일 템플릿(프로필 URL에 항상 @핸들 포함, 부가 경로 없음). 관측
URL 종류 - 비디오(Video)
tiktok.com/@{handle}/video/{videoId} 형태. 개별 영상 게시물, 표본 115건 중 104건(90%) 차지. 관측
URL 종류 - 포토(Photo/Slideshow)
tiktok.com/@{handle}/photo/{photoId} 형태. TikTok 특유의 이미지 슬라이드쇼 게시물(캐러셀). video와 별도 경로지만 표본 내 fetched.sourceType은 tiktok_video로 통합 취급됨. 11/115건(9.6%). 관측
URL 종류 - 검색(Search)
tiktok.com/search?q={query}&t={timestamp} (49건) 및 tiktok.com/search/video?q={query}&t={timestamp} (8건, 비디오 탭 한정 검색). 특정 계정/게시물이 아닌 쿼리 기반 URL로 status: unsupported 처리됨. 관측
식별자 - handle(@아이디)
프로필/비디오/포토 URL 공통 경로 세그먼트 @{handle}. 표본상 소문자·숫자 조합만 관측(예: @pumbacaracalofficial, @espn, @knowyourmeme). TikTok 정책상 handle은 대소문자 구분 없이 고유(unique, case-insensitive)하며 영문자/숫자/밑줄(_)/마침표(.)만 허용, 최대 24자. 웹리서치/실측
식별자 - videoId/photoId
URL 마지막 세그먼트의 순수 숫자 문자열(예: 7652462228196560159, 7649280049534799137). TikTok 내부적으로 이 값은 스노우플레이크형 64비트 정수 ID이며 생성 시각 정보를 인코딩함. 웹리서치/실측
식별자 - search query(q)
검색 URL의 q 파라미터는 자유 텍스트(공백 포함, URL 인코딩됨. 예: q=flock%20camera). 계정/게시물을 특정하지 못하는 비고유 식별자. 관측
식별자 - t 파라미터
검색 URL에 항상 동반되는 t=epoch밀리초 타임스탬프. 검색이 수행/공유된 시점을 나타내는 TikTok 검색 URL 특유의 트래킹 파라미터로 추정. 관측
대소문자 규칙 - handle
handle 자체는 case-insensitive 고유성을 가지나(대문자로 입력해도 동일 계정 매칭), 표시상 원본 대소문자를 유지하는 경우가 있어 URL 매칭/정규화 시 소문자로 통일해 비교해야 함. 웹리서치/실측
지표 - 팔로워 수(Followers)
프로필 페이지에 노출되는 핵심 소셜 지표. 표본 JSON은 모두 status: skipped_paid로 실제 값(data)이 비어 있어 관측 불가, TikTok 프로필의 표준 지표로만 알려짐. 웹리서치/실측
비디오의 재공유(공유 버튼) 횟수. 댓글 수와 함께 비디오 참여 지표 3종(좋아요/댓글/공유) 중 하나. 웹리서치/실측
지표 - 팔로잉 수(Following)
프로필이 팔로우하는 계정 수. 웹리서치/실측
플랫폼 특유 개념 - 듀엣(Duet)
기존 영상 옆에 나란히 자신의 영상을 나란히 배치해 반응/합주하는 TikTok 고유 리믹스 기능. URL/API 상 원본 videoId를 참조하는 별도 파생 콘텐츠 개념으로, X의 quote와 유사하나 영상 나열 방식이 다름. 웹리서치/실측
플랫폼 특유 개념 - 스티치(Stitch)
타 영상의 클립(최대 5초)을 자신의 영상 도입부에 잘라 붙이는 기능. 원본 크리에이터 크레딧이 자동 표기됨. 웹리서치/실측
플랫폼 특유 개념 - 사운드/오디오 페이지(Sound page)
특정 배경음악·오디오 클립을 사용한 모든 영상을 모아 보여주는 tiktok.com/music/{soundName}-{soundId} 형태 URL. 밈 확산의 핵심 축(동일 사운드로 영상이 파생)이나 이번 표본에는 미등장. 웹리서치/실측
플랫폼 특유 개념 - 해시태그 챌린지 페이지
tiktok.com/tag/{hashtag} 형태로 특정 해시태그를 사용한 영상을 모은 페이지. 브랜드/밈 캠페인 단위 집계에 쓰이나 표본에는 없음. 웹리서치/실측
플랫폼 특유 개념 - 슬라이드쇼/포토모드
영상 대신 여러 장의 사진 카루셀+배경음악을 올리는 게시 형식(/photo/ 경로). 표본에서 관측된 11건 전량 동일 URL(같은 photoId)이 다수 토큰에 재사용됨(콘텐츠 재사용/카피캣 정황). 관측
소스 필드(sourceField) 관측치
동일 TikTok URL이 토큰의 website, discovered, telegram 등 서로 다른 sourceField에서 발견됨 — 즉 TikTok 링크가 웹사이트뿐 아니라 텔레그램 메시지 등 타 채널 본문에서도 발견/수집될 수 있음. 관측
fetch 상태 - skipped_paid
프로필/비디오/포토 URL은 TikTok의 유료(비공식) 스크래핑 API 게이트에 걸려 실제 fetch가 스킵됨(status: skipped_paid, data: null) — 무료 채널로는 팔로워/조회수 등 실측 지표를 얻지 못함. 관측
fetch 상태 - unsupported
검색(search) URL 타입은 애초에 fetch 파이프라인에서 미지원 처리됨(status: unsupported) — 특정 게시물/계정을 가리키지 않는 쿼리형 URL이라 구조적으로 수집 대상에서 제외. 관측
URL 정규화 특이점 - www 유무
동일 search/video 템플릿 내에서도 https://www.tiktok.com/...과 https://tiktok.com/...(www 없음)이 혼재 관측됨(예: search/video 표본 3건 중 1건은 www 없음) — URL 매칭 시 www 유무를 정규화해야 함. 관측
일반 동영상 게시물. classifyTiktok: segments에 'video' 포함+다음 세그먼트 존재 시 tiktok_video, sourceKey=postId(숫자 원형). fetcher는 postURLs:[전체URL]로 apify 호출, 동일 valueNature 스키마.
postId (숫자 게시물 ID, 대소문자/원형 유지)
관측 104건
samples/tiktok_video.json 구조1
tiktok.com/@{handle}/photo/{postId}
포토(슬라이드) 게시물. classifyTiktok에서 'video'와 동일 루프(['video','photo'])로 처리되어 sourceType은 tiktok_video로 통합, sourceKey=postId. router 주석: photo도 post라 video와 동일 취급(fetcher가 postURLs로 직접 조회).
postId (숫자 게시물 ID)
관측 11건
samples/tiktok_video.json 구조2
tiktok.com/search?q={query}
검색 결과 페이지(홈 탭). classifyTiktok: segments[0]==='search' → tiktok_search, sourceKey=q 파라미터(소문자화). tiktok_search는 TiktokFetcher.handles에 없어 라우팅은 되지만 실제 fetcher 미구현 → orchestrator가 status='unsupported' 반환(관측 전부).
query string (검색어, URL-decode 후 소문자)
관측 49건
samples/tiktok_search.json 구조1
tiktok.com/search/video?q={query}
검색 결과의 video 서브탭. classifyTiktok에서 segments[0]==='search'만 체크하므로 segments[1]='video'는 무시되고 동일하게 tiktok_search로 분류, sourceKey=q. fetcher 없음 → unsupported.
TikTok 단축링크(리다이렉트 전용, 실제 프로필/영상으로 302 이동). host는 route()에서 vm.tiktok.com까지 tiktok 분기로 들어가지만 classifyTiktok 내부는 '@'/'search'/'video'/'photo' 셋 중 아무것도 매치 안 해 sourceType='unknown', sourceKey=null. 라우터가 리다이렉트를 따라가지 않아 미해결.
shortCode (리다이렉트 해석 전엔 식별자 아님 — 해석 후 진짜 handle/postId로 대체 필요)
관측 7건
unknown.json: vm.tiktok.com/ZNRTk8BQc(3) + ZNRTtUjSM/ZGd9UfYoM/ZNRTxEvBu/ZNR3pHNHo(각1) + tiktok.com/{value}/{value}(t/ZTSJ7TCua, 54건 그룹 중 일부)
tiktok.com/tag/{hashtag}
해시태그 페이지. classifyTiktok 미대응 경로 → unknown. 웹 지식상 정식 해시태그 채널 URL.
hashtag (문자열, 대소문자 유지)
웹리서치/실측
unknown.json 'tiktok.com/{value}/{value}' 그룹 샘플2(count=54 그룹 내 일부, 정확 서브카운트 미분리)
잘못 구성된/빈 URL. 파싱 시 segments가 빈 배열이라 classifyTiktok의 모든 분기 미스 → unknown, sourceKey=null. 데이터 결손 케이스(실제 프로필/링크 아님).
없음 (식별자 없음)
관측 3건
unknown.json 'tiktok.com/' 템플릿
tiktok.com/@{handle}/live
웹 지식상 존재하는 정식 라이브 방송 URL(미관측). classifyTiktok은 '@'로 시작하면 무조건 tiktok_profile로 분류하므로 /live 서브패스가 있어도 handle만 추출되어 프로필로 오분류될 가능성.
handle (라이브는 세션형이라 handle 외 별도 콘텐츠ID 없음)
웹리서치/실측
미관측, 일반 지식 보완
vt.tiktok.com/{shortCode}
vm.tiktok.com과 동일 성격의 또 다른 TikTok 단축도메인(신규 배포 지역에서 사용). router의 host 화이트리스트(tiktok.com/vm.tiktok.com/m.tiktok.com)에 없어 host 자체가 매칭 안 되면 apex-domain website로 폴백될 수 있음.
shortCode (리다이렉트 해석 필요)
웹리서치/실측
미관측, 일반 지식 보완 — router.ts 60-63행 host whitelist 확인
2. sourceType / sourceKey 판정 👤 판단 반영
2.1 sourceType 적합도 👤 판단 반영
패턴
현재 type
판정/제안
sourceKey
conf
근거
사용자 결정
tiktok.com/@{handle}
tiktok_profile, key=handle(소문자)
변경 없음
handle
high
segments[0].startsWith('@') 분기가 정확히 매치. fetcher가 profiles:[handle]로 clockworks 액터 호출하며 valueNature 스키마도 명확(fixed: text/createTimeISO/hashtags/isAd, mutable: playCount류). 다만 observed 3건 전부 skipped_paid — 라우팅은 옳으나 유료게이트로 실사용 데이터 0건. 이건 router 문제가 아니라 orchestrator 유료게이트 정책 이슈이므로 sourceType 분류 자체는 keep.
✅ tiktok_profile 유지. 단 sourceKey=handle은 alias일 뿐이고 canonical 조인키는 authorMeta.id — fetch 후 id로 resolve해 연결. handle 재할당 때문에 handle 단독 dedup 금지. 기존 제안 유지.
router 주석에 명시된 의도된 병합 — photo도 post이고 fetcher가 postURLs로 게시물을 직접 조회하므로 video와 스키마·엔드포인트가 완전히 동일. 별도 sourceType으로 split할 실익 없음(같은 postId 네임스페이스, 같은 valueNature).
✅ 병합 유지 확정 — 2026-07-29 실측에서 photo 게시물의 canonical URL이 /video/{id}로 정규화돼 돌아옴을 확인(동일 네임스페이스 실증). 병합 후 형식 구분은 isSlideshow로 복원.
tiktok.com/search?q={query}
tiktok_search로 정확히 분류되나 TiktokFetcher.handles에 없어 fetcher 미구현 → orchestrator가 항상 status=unsupported(49건 전부)
router 분류는 유지. fetcher 구현 여부는 별도 결정 필요 — 구현하지 않을 거라면 애초에 이 sourceType을 만들 필요가 있었는지 재검토
query string(소문자)
medium
라우팅 정확도 자체는 문제없음(gap은 router가 아니라 fetcher 커버리지). 다만 trader-value 관점에서 검색결과는 다건 집계 페이지라 '원본 vs 재활용' 판별에 쓸 단일 콘텐츠 식별자가 없고, 항상 unsupported로 끝나 실질 신호 가치가 0 — 투자 우선순위 낮음(퀀트 signal 렌즈에서도 관측 데이터가 전무해 검증 불가).
✅ router 분류 유지, fetcher 미구현(unsupported) 상태도 유지. 기존 제안 유지.
host는 vm.tiktok.com까지 tiktok 분기로 들어오지만 classifyTiktok 내부 4개 분기(search/video/photo/@) 중 아무것도 매치 안 해 unknown, key=null. tiktok.com/t/{shortCode}도 segments[0]='t'가 어떤 분기와도 안 맞아 동일하게 unknown
신규 sourceType 'tiktok_shortlink' (key=shortCode) 도입 후, router는 pure/네트워크-없음 함수이므로 classify 단계에서 리다이렉트를 못 따라감 — fetcher 또는 orchestrator 앞단에 HEAD 리다이렉트 리졸버를 추가해 실제 handle/postId로 재라우팅하거나, 최소한 shortCode를 key로 저장해 추후 배치 리졸브 가능하게
shortCode (해석 전엔 진짜 식별자 아님)
high
observed 7건이 전부 데이터 손실(unknown, key=null) — 실제로는 프로필/영상 콘텐츠인데 완전히 유실됨. trader-value 관점에서 shortlink는 밈코인 공식 계정이 자주 쓰는 형태(bio 링크 단축)라 무시하면 신호 누락 폭이 작지 않음. 다만 router가 순수함수라 리다이렉트 해석은 아키텍처 변경(네트워크 호출)이 필요해 즉시 처리는 어려움 — 우선 sourceType만 분리해 유실을 가시화하는 것이 1차 조치.
✅ 결정 변경 — 상류 수집 단계에서 canonical 정규화. 실측: vm/t 링크 6건 전부 HEAD 301로 /@handle/video/{postId} 해석 성공(무료, 즉시). 왜 fetcher가 아니라 상류인가: TikTok은 유료 게이트라 fetch가 스킵된 엔트리는 fetcher 해석 방식에선 영원히 shortlink로 남고 → 같은 영상이 canonical 관측분과 별개 엔티티로 분기(조인키에서 금지한 실패 모드). 리다이렉트 해석은 무료 HEAD 1홉이라 유료 여부와 무관하게 항상 가능. ⚠ shortlink를 tiktok_video로 고정하지 않음 — vm 링크는 프로필 공유에도 쓰여 /@handle로 떨어질 수 있음(관측 6건이 전부 video인 건 표본 편향) → 해석 결과에 따라 재분류. ⚠ Location에 ?_r=1&_t=... 추적 파라미터가 붙어오므로 제거 후 저장.
tiktok.com/tag/{hashtag}
미대응 → unknown
신규 sourceType 'tiktok_hashtag' (key=hashtag, 대소문자 유지) 검토. 단 fetcher 지원 여부(clockworks 액터가 hashtag 검색을 지원하는지) 확인 전에는 우선순위 낮음
hashtag
medium
search와 동일하게 다건 집계 페이지라 단일 콘텐츠 신호는 아니지만, 해시태그 자체가 밈 확산(크로스포스트) 폭을 나타내는 모멘텀 지표가 될 수 있어 trader-value에서 완전 noise는 아님. 다만 정확 서브카운트가 unknown 그룹(54건)에 섞여 있어 실제 관측량 확인이 먼저 필요.
✅ 신규 타입 검토만 유지(우선순위 낮음). 기존 제안 유지.
tiktok.com/music/{musicTitle}-{musicId}
미대응 → unknown
신규 sourceType 'tiktok_music' (key=musicId, URL 끝 숫자 파싱 필요 — title은 slug라 비고유) 검토
musicId
low
사운드 트렌드는 밈 확산의 간접 지표가 될 수 있으나 특정 토큰과의 연결고리가 약하고(사운드는 재사용성이 전제라 '원본 판별' 신호와 상충), 관측량도 미상(observed=null, unknown 그룹 내 일부 추정)이라 신규 sourceType 투자 우선순위는 낮음.
✅ 신규 타입은 보류 유지. 다만 사운드 확산 축은 content의 musicMeta.musicId를 keep해 먼저 관측하기로 결정 — URL 타입 없이도 데이터는 쌓임.
tiktok.com/oembed?url= (빈 값)
unknown (segments=['oembed'], 어떤 분기도 매치 안 함)
router 변경 불필요 — unknown 유지가 맞음. 근본 수정은 상류(discovered 필드 추출 파이프라인)에서 url= 파라미터가 빈 oEmbed API 호출 흔적을 애초에 소셜 URL로 저장하지 않도록 하는 것
없음(url= 파라미터 자체가 비어 식별 불가)
high
이건 router의 분류 실패가 아니라 상류 데이터 결손 — url 파라미터가 비어 있어 어떤 라우팅 로직을 추가해도 식별자를 복원할 수 없음. sourceType/router 레이어에서 손댈 지점이 아님.
✅ unknown 유지(상류 데이터 결손). 기존 제안 유지.
tiktok.com/embed/v2/{postId}
미대응 → unknown (loop이 'video'/'photo' 세그먼트만 찾고 'embed'/'v2'는 안 봄)
tiktok_video로 병합, key=postId (마지막 숫자 세그먼트). 단 fetcher의 postURLs 입력이 raw embed URL(iframe 형식)을 그대로 받아도 apify clockworks 액터가 파싱 가능한지 미검증 — 필요시 postId만 뽑아 표준 워치 URL로 재구성해서 넘기는 보정 필요
postId
medium
postId는 video/photo와 동일 네임스페이스로 추정되므로 새 sourceType을 만들 이유가 없고 기존 tiktok_video에 합류시키는 게 맞음. 다만 라우터에서 sourceKey만 옳게 뽑고 sourceUrl(임베드 형식)을 그대로 fetcher에 넘기면 액터가 실패할 위험이 있어 confidence는 medium.
✅ tiktok_video 병합 확정 + 미해결 리스크 실측으로 해소. "handle을 못 찾아 못 쓴다"는 우려는 조인키를 id로 확정하면 사라짐(creator 링크는 URL이 아니라 fetch 응답의 authorMeta.id에서 옴). 실측: embed URL 자체는 oEmbed·apify 둘 다 거부(400/FAILED)했으나, /@아무handle/video/{postId}로 감싸면 틀린 handle로도 정확한 게시물+작성자를 반환 → router는 postId만 뽑고 fetcher가 canonical 형태로 정규화하면 완전 해결.
tiktok.com/{lang}/trending/detail/{slug}
미대응 → unknown
현재는 조치 보류 권장 — slug는 안정적 콘텐츠 ID가 아니라 트렌드 항목명이라 postId처럼 재현 가능한 키가 아님. 관측 1건뿐이라 신규 sourceType 투자 대비 효용 낮음
slug (약한 식별자)
low
약한 식별자 + 극희소 관측(1건) 조합이라 signal/redundancy 렌즈에서 검증 자체가 불가능. 향후 관측량이 늘면 재검토.
✅ 조치 보류 유지(약한 식별자 + 관측 1건). 기존 제안 유지.
tiktok.com// (빈 path, 중복 슬래시)
unknown, key=null (segments 빈 배열)
변경 불필요
없음
high
잘못 구성된/빈 URL로 실제 콘텐츠가 아닌 데이터 결손 케이스 — unknown 처리가 정확한 동작. router 로직 수정 대상 아님.
현행 유지. 단 라이브 세션 특유의 메트릭(동시 시청자수, 방송중 여부)이 필요해지면 그때 별도 tiktok_live sourceType 분리 검토
handle
medium
우려와 달리 '오분류'는 아님 — 라이브는 handle 외 별도 콘텐츠ID가 없는 세션형이라 프로필 키로 수렴하는 게 자연스럽고, 어차피 별도 라이브 전용 fetcher/액터가 없어 지금 분리해도 실질 이득이 없음. 미관측(observed=null)이라 우선순위도 낮음.
✅ tiktok_profile 수렴 유지. 기존 제안 유지.
vt.tiktok.com/{shortCode}
router.classify()의 host whitelist(56-90행)가 tiktok.com/vm.tiktok.com/m.tiktok.com만 포함 — vt.tiktok.com은 어떤 if도 안 걸려 최종 폴백인 website로 떨어지고 apexDomain('vt.tiktok.com')이 'tiktok.com'을 반환해 잘못된 website 키로 오염될 위험
host whitelist에 'vt.tiktok.com' 추가해 classifyTiktok으로 라우팅. 단 vm.tiktok.com과 동일하게 shortCode 리다이렉트 해석 필요(위 vm.tiktok.com 항목과 동일한 리졸버 설계에 편입)
shortCode (리다이렉트 해석 필요)
high
host 커버리지 갭이 명확 — 매치 실패 시 unknown이 아니라 website로 새는 게 더 나쁨(다른 정상 website 엔트리와 sourceKey='tiktok.com'으로 충돌 가능). 미관측(observed=null)이지만 신규 지역 배포 도메인이라 향후 유입 가능성 있어 whitelist 추가는 저비용·고안전 조치.
✅ host whitelist에 vt.tiktok.com 추가는 그대로 유지. 상류 정규화가 기본 경로가 되더라도 만료·네트워크 실패로 raw shortlink가 남을 수 있고, 그때 whitelist가 없으면 unknown이 아니라 website로 새어 apexDomain이 'tiktok.com'을 반환해 다른 정상 website 엔트리와 키 충돌 — 실패 모드가 더 나쁨. 안전망으로 필수.
2.2 id 추출 적합도 👤 판단 반영
패턴
추출 id
resolve?
판정
conf
근거
사용자 결정
tiktok.com/@{handle}
sourceKey = handle (소문자)
✅
ok
high
live-test에서 profiles:["knowyourmeme"] 호출 결과 authorMeta.name='knowyourmeme'로 handle이 정확히 그 계정을 가리킴을 확인. 단, Apify profiles 경로는 '프로필 정보'가 아니라 해당 계정의 '최신 영상 1건'을 반환하고 authorMeta에 프로필 필드가 동봉되는 구조(fetcher는 authorMeta.fans/name만 매핑) — sourceKey 자체는 계정을 유일하게 특정하는 진짜 식별자이나, 이 식별자로 얻는 콘텐츠가 '안정적 프로필 스냅샷'이 아니라 '조회 시점 최신 영상'이라는 점은 mutable 데이터로서 look-ahead 주의 대상(별도 field-necessity 판정 사안).
✅ handle 추출 자체는 정확. 단 handle은 키가 아니라 alias로 격하 — 30일마다 변경 + 옛 handle 타인 재할당(unique 위반). canonical 조인키는 authorMeta.id.
tiktok.com/@{handle}/video/{postId}
sourceKey = postId (숫자 원형)
✅
ok
high
live-test에서 postURLs:[...] 호출 결과 raw item.id='7652462228196560159'가 라우터 추출 sourceKey와 완전 일치 확인. postId는 게시물을 유일하게 특정하는 진짜 식별자이며 profiles 경로와 달리 '최신영상'이 아닌 해당 게시물 자체를 정확히 반환.
✅ postId가 authoritative임을 실측 확인 — URL의 handle이 틀려도 정확한 게시물 반환. key=postId 유지 + content 응답의 authorMeta.id로 creator 연결.
tiktok.com/@{handle}/photo/{postId}
sourceKey = postId (숫자, video와 동일 분기 처리)
✅
ok
medium
classifyTiktok의 ['video','photo'] 동일 루프를 타고 fetcher도 postURLs로 동일하게 호출되므로 코드상 video 케이스와 완전히 동형. 다만 photo 게시물 자체를 대상으로 한 live-test는 수행되지 않아 postId가 photo 콘텐츠에도 동일하게 유효한 식별자로 resolve됨은 직접 관측이 아닌 코드 동형성에 근거한 추정.
✅ postId가 authoritative임을 실측 확인 — URL의 handle이 틀려도 정확한 게시물 반환. key=postId 유지 + content 응답의 authorMeta.id로 creator 연결.
tiktok.com/search?q={query}
sourceKey = q (URL-decode 후 소문자)
❌
wrong
high
live-test(코드 확인)로 TiktokFetcher.handles에 'tiktok_search'가 없어 서비스가 디스패치 전에 unsupported로 조기 반환함을 확인 — 실제 네트워크 호출 자체가 발생하지 않아 sourceKey가 resolve되는 경로가 아예 없음. 설령 fetcher가 구현되더라도 검색어는 시간에 따라 결과가 바뀌는 쿼리이지 단일 콘텐츠를 유일하게 가리키는 식별자가 아니라 id-fit 관점에서도 근본적으로 약함(우리 목적상 '진위/원본 재활용 판별'에 쓸 고정 콘텐츠 ID가 아님).
✅ 쿼리는 콘텐츠 식별자 아님(다건 집계). 기존 제안 유지.
tiktok.com/search/video?q={query}
sourceKey = q (segments[1]='video' 무시)
❌
wrong
high
search 패턴과 동일하게 fetcher 미구현으로 unsupported 확정(코드 확인, 관측 status와 일치). 추가로 서브탭 정보(video 필터)가 sourceKey 추출 시 아예 버려져 원 URL이 담고 있던 필터 정보 손실도 발생 — 이중으로 부적합.
현재 sourceType=unknown, sourceKey=null (host는 whitelist에 있으나 path가 @/search/video/photo 중 아무것도 매치 안 함)
❌
wrong
high
classifyTiktok 내부 4개 분기(search/video/photo/@) 중 어느 것도 shortCode 형태 path와 매치되지 않아 sourceKey=null로 완전 유실 확인(코드 Read). live-test에서도 표본에 vm.tiktok.com 0건이라 실제 리다이렉트 해석 시도조차 없었음. shortCode 자체는 리다이렉트를 따라가지 않는 한 진짜 콘텐츠를 가리키는 식별자가 아니므로(302 대상이 진짜 handle/postId) 현재 구현으로는 원천적으로 resolve 불가.
✅ shortCode는 그 자체로 콘텐츠 식별자가 아님이 맞음. 다만 상류에서 canonical로 정규화하면 postId(또는 handle)가 확보되므로 식별 문제 해소 — 실측 6/6 해석 성공. 저장되는 최종 키는 shortCode가 아니라 해석된 postId/handle.
tiktok.com/tag/{hashtag}
현재 sourceType=unknown, sourceKey=null
❌
wrong
medium
classifyTiktok에 'tag' 분기가 없어 unknown으로 빠짐(코드 확인). live-test에서 별도 호출 시도 없이 구조적으로 fetcher 도달 불가 확정. 관측 건수 자체도 unknown.json 그룹 내 정확 서브카운트 미분리라 실사용 빈도 불확실.
✅ hashtag는 집계 페이지 키 — 단일 콘텐츠 식별 불가. 기존 제안 유지.
tiktok.com/music/{musicTitle}-{musicId}
현재 sourceType=unknown, sourceKey=null (musicId가 title에 붙은 slug 안에 매장)
❌
wrong
medium
classifyTiktok에 'music' 분기 없어 unknown. 설령 파싱을 추가해도 slug에서 musicId만 분리 추출하는 별도 로직이 필요(title은 비고유 slug). 현재는 어떤 형태로도 resolve되지 않음을 코드로 확인.
✅ musicId 추출(끝 숫자)은 유효하나 타입 신설은 보류. musicMeta.musicId로 먼저 관측.
tiktok.com/oembed?url=
현재 sourceType=unknown, sourceKey=null (url 파라미터가 비어있음)
❌
wrong
high
url= 값 자체가 빈 데이터 결손 케이스(코드/샘플 모두 동일 URL 40건). 식별자 후보가 근본적으로 존재하지 않아 id-fit 판정 대상이 아니라 데이터 정제(제거) 대상.
✅ url 파라미터가 비어 복원 불가 — 상류 수정 사안. 기존 제안 유지.
tiktok.com/embed/v2/{postId}
현재 sourceType=unknown, sourceKey=null (postId가 문자열에 있음에도 미추출)
❌
wrong
medium
classifyTiktok의 video/photo 루프는 segments 중 'video' 또는 'photo' 토큰이 있어야 매치되는데 embed 경로는 'embed'/'v2'/postId 세그먼트라 루프에 걸리지 않아 unknown으로 누락(코드 확인). postId 자체는 video/photo와 동일 네임스페이스로 추정되어 잠재적으로 진짜 식별자이지만 현재 라우터가 뽑지 않아 resolve 불가.
classifyTiktok 미대응이라 unknown(코드 확인). 관측 1건뿐이라 실사용 빈도 낮고, slug 자체도 트렌드 항목명이라 게시물 고유 ID가 아닌 약한 식별자 — 설령 파싱을 추가해도 진짜 고유 식별자로 보기 어려움.
✅ slug는 재현 가능한 키가 아님 — 보류 유지. 기존 제안 유지.
tiktok.com// (빈 path)
sourceType=unknown, sourceKey=null
❌
wrong
high
segments가 빈 배열이라 모든 분기 미스로 unknown 확정(코드 확인). 식별자 후보 자체가 없는 데이터 결손 케이스.
✅ 기존 제안 유지(확정).
tiktok.com/@{handle}/live
sourceKey = handle (segments[0].startsWith('@') 분기가 /live 서브패스 무시하고 무조건 매치)
✅
ok
low
코드상 '@'로 시작하면 뒤 세그먼트('live')와 무관하게 무조건 tiktok_profile+handle로 분류됨을 확인(라우터 149-163행). handle 자체는 계정을 유일하게 특정하는 진짜 식별자이므로 id-fit 관점에서는 통과하나, 실제 라이브 테스트는 미수행(관측 0건)이라 profiles:[handle] 호출 시 프로필 판정과 동일하게 최신 영상을 반환할 뿐 '라이브 세션'과는 무관한 콘텐츠가 나올 가능성 — 이는 sourceType 오분류 문제(별도 판정관 사안)이지 sourceKey 자체의 고유성 문제는 아님.
✅ handle 추출 자체는 정확. 단 handle은 키가 아니라 alias로 격하 — 30일마다 변경 + 옛 handle 타인 재할당(unique 위반). canonical 조인키는 authorMeta.id.
vt.tiktok.com/{shortCode}
host가 whitelist(tiktok.com/vm.tiktok.com/m.tiktok.com)에 없어 classify()의 마지막 폴백으로 떨어져 sourceType='website', sourceKey=apexDomain('vt.tiktok.com')='tiktok.com'
❌
wrong
medium
라우터 classify() 62-68행 host whitelist에 vt.tiktok.com이 없음을 코드로 확인 — 89행 최종 폴백(website+apexDomain)으로 떨어져 shortCode가 완전히 유실되고, 여러 서로 다른 vt.tiktok.com 링크가 전부 동일한 sourceKey='tiktok.com'으로 뭉개져 고유 식별은커녕 다른 소스와 충돌 위험까지 있음. 미관측이라 실사용 빈도는 불확실.
✅ shortCode는 그 자체로 콘텐츠 식별자가 아님이 맞음. 다만 상류에서 canonical로 정규화하면 postId(또는 handle)가 확보되므로 식별 문제 해소 — 실측 6/6 해석 성공. 저장되는 최종 키는 shortCode가 아니라 해석된 postId/handle.
커버리지 갭 — vm.tiktok.com/{shortCode} 및 tiktok.com/t/{shortCode} — 7건 전부 unknown으로 유실, 리다이렉트 해석 로직 부재(router가 순수함수라 네트워크 호출 불가)
커버리지 갭 — tiktok.com/tag/{hashtag} — router 미대응, unknown 그룹(54건)에 섞여 정확한 관측량 미분리
커버리지 갭 — tiktok.com/music/{musicTitle}-{musicId} — router 미대응, unknown 그룹에 섞임
커버리지 갭 — vt.tiktok.com host 전체가 whitelist 밖 — 매치 실패 시 unknown이 아니라 website 폴백으로 새어 apexDomain 오염 위험
커버리지 갭 — tiktok_search sourceType은 router 분류는 정확하나 TiktokFetcher.handles에 없어 fetcher 자체가 미구현(49+8=57건 전부 unsupported) — router 문제는 아니지만 sourceType 존재 이유 재검토 필요
3. Fetch 테스트 + 획득 방법
3.1 획득 사다리 (공짜 → 유료)
방법
비용
설명
트레이드오프
conf
확정 ✅ (2026-07-21)
사용자 선택 완료 — tiktok_profile→apify:apidojo ($0.30 / 1,000 results (profile-scraper 단독은 $2.50/1,000)) · tiktok_video→apify:apidojo ($0.30 / 1,000 results) · tiktok_search→apify:apidojo-search ($0.30 / 1,000 results (프로필/비디오와 동일 액터·가격)) adopted — ⚠️ 전환 보류(2026-07-31 실측)
Phase A 후보 사다리에서 사람이 선택한 확정 source. §4 통합 필드 뷰는 이 source의 실제 스키마 기준.
🔴 전환 시도했으나 막혔다 — apidojo 가 무료 플랜의 API 호출을 거부한다(The developer of this actor doesn't allow the use of API in the Free Plan). 4회 실행 전부 SUCCEEDED 로 기록되나 데이터 0건 + 과금만 $0.012. 그리고 README 출력 예시에 authorMeta.createTime(계정 개설일 = 러그리스크 1순위 tell) · isSlideshow(photo/video 유일 구분자) · slideshowImageLinks 등 7개 필드가 없다 — 단 API 가 막혀 확증하지 못했다. 비용 실측: clockworks $0.0047/run vs apidojo $0.0003/건 → 절감 월 약 $5로 Starter $29 를 정당화하지 못한다. 상세: [apify-cost-model.html](./apify-cost-model.html)
낮음 — 실측으로 전환 조건이 미충족임이 확인됨
0 (현재)
Apify actor clockworks~tiktok-scraper 실제 사용 중 (tiktok.fetcher.ts, 유일한 경로)
$1.70 / 1,000 results (PPE, Apify 공식 pricing 페이지 확인)
profiles:[handle] 또는 postURLs:[url]로 프로필/게시물 조회. 반환: text/createTimeISO/hashtags/isAd(fixed), playCount/diggCount/commentCount/shareCount/authorFans(mutable). follower total은 authorMeta.fans로 확보되나 생성일(계정)/인증여부는 미매핑(item에 있어도 fetcher가 파싱 안 함).
검증됨(운영중)이지만 현재 사용 가능한 actor 중 가장 비쌈. 저볼륨 온디맨드엔 비용 자체는 부담 적으나 대안 대비 5배 이상 비효율.
높음 (코드 Read로 직접 확인, 추정 없음)
1 (무료, 제한적)
TikTok oEmbed (/oembed, 인증 불필요) 미사용, 즉시 사용 가능
완전 무료, rate limit 공개 안 됨(가벼운 사용 전제)
author_name, title, thumbnail_url만 반환. follower/생성일/인증여부/참여수(playCount 등) 전혀 없음.
우리 필요 필드(팔로워·인증·참여수)를 거의 커버 못 해 실질적으로 게시물 존재확인/미리보기용 그 이상 아님. 백업 fallback 용도로만 가치.
높음 — TikTok 공식 문서(developers.tiktok.com/doc/embed-videos) 직접 확인
2 (공식이나 구조적 불가)
TikTok Display API (/v2/user/info/, /v2/video/list/) 우리 유스케이스에 사용 불가
무료
follower/videoList 등 필요 필드 스펙상 존재.
OAuth로 대상 계정 소유자 본인이 로그인/동의해야만 접근 가능한 구조 — 우리는 토큰과 연관된 임의 제3자 계정을 관찰해야 하므로 원천적으로 부적합.
높음 — TikTok 공식 문서 직접 확인
3 (공식·무료지만 자격 없음)
TikTok Research API 신청 자격 자체 없음
무료 (1,000 req/day, follower/following 엔드포인트는 최대 2M records/day)
팔로워·생성일·인증·게시물 참여수 등 스펙상 우리 필요를 거의 다 커버.
공식 FAQ에 상업적 사용자(creator/advertiser/commercial user)는 명시적으로 'No' — 학술기관·비영리 연구 전용. 우리 백엔드는 자격 미달로 원천 배제.
높음 — TikTok Research API FAQ 페이지 직접 fetch, 문구 인용 확인
4 (유료, 추천 대안)
Apify actor apidojo/tiktok-scraper 미사용, 교체 후보
$0.30 / 1,000 results (PPE) — 현재 clockworks 대비 약 82% 저렴
프로필: username/bio/verified/follower·following/video수. 게시물: views/likes/comments/shares/bookmarks + 업로드시각/해시태그. 우리 필요 필드(팔로워·인증·게시일·참여수) 구조적으로 모두 존재.
현재 fetcher 인터페이스(buildResult/valueNature 매핑)만 바꾸면 되는 저비용 전환. 단, 필드명이 clockworks와 다를 가능성 높아 실제 매핑 검증(actor 출력 스키마 재확인) 필요 — 광고성 요약이라 100% 신뢰 금지.
중상 — Apify 공식 상품페이지 직접 fetch, 단 필드 상세는 요약이라 실사용 전 실제 output 샘플 확인 권장
5 (유료, 비추천)
TikAPI.io (비공식 관리형 API, 구독형) 미사용, 저볼륨엔 부적합
월 구독 $29(300req/day)~$189(2,000req/day), pay-per-use 아님
프로필/팔로워/게시물 등 REST 엔드포인트 제공.
고정 월정액 + 요청수 tier 구조라 '저볼륨 온디맨드'와 과금모델 자체가 안 맞음(안 쓰는 달에도 최소 $29). PPE형 Apify 대비 비효율적.
중 — 3rd party 블로그(Blotato) 요약 기반, 가격 페이지 직접 미확인이라 벤더 편향 가능성 있음. 채택 전 tikapi.io/pricing 직접 재확인 권장
3.2 공식 API 상세
확신도 높음 (공식 문서 1차 확인) — 두 공식 API 모두 우리 유스케이스(임의 제3자 토큰 관련 TikTok 계정을 저볼륨으로 온디맨드 조회)에는 사용 불가: Display API는 대상 계정 소유자 OAuth 동의가 전제라 구조적으로 불가하고, Research API는 필드는 충분하지만 상업적 사용자는 자격 자체가 없다고 FAQ에 명시됨.
tiktok_profile — Apify clockworks~tiktok-scraper, profiles:["knowyourmeme"], resultsPerPage:1 (fetcher가 실사용하는 정확한 엔드포인트/입력 그대로 curl 재현)
POST https://api.apify.com/v2/acts/clockworks~tiktok-scraper/run-sync-get-dataset-items?token=***&timeout=90
유료
success (HTTP 201, 1 item)
profiles:[handle] 는 '프로필 정보'가 아니라 해당 계정의 최신 영상 1건을 반환하며 authorMeta 안에 프로필 정보가 동봉됨 — fetcher.buildResult는 item.text/createTimeISO/playCount/diggCount/commentCount/shareCount/hashtags/authorMeta.fans/authorMeta.name/isAd만 매핑. authorMeta.verified(true 확인됨)/authorMeta.createTime(계정생성일 1556120323=2019-04-24)/collectCount/repostCount는 raw에 존재하나 fetcher가 파싱하지 않음(acquisition 요약과 일치).
POST https://api.apify.com/v2/acts/clockworks~tiktok-scraper/run-sync-get-dataset-items?token=***&timeout=90
유료
success (HTTP 201, 1 item)
postURLs 경로는 정확히 해당 게시물 자체를 반환(profiles 경로와 달리 '최신영상' 아님). isAd=true(광고성 게시물) 확인 — fetcher.isAd 매핑 정상 동작 검증됨. sourceKey(라우터 추출 '7652462228196560159')와 raw item.id 완전 일치 확인.
2건 확정: ①photo/video 형식 구분자 = isSlideshow ②photo 게시물의 canonical URL이 /video/로 정규화돼 돌아옴 → 동일 네임스페이스 실증(병합 결정 정당화). 더불어 문서 objectFields에 없던 필드 다수 발견(mentions/textLanguage/isPinned/webVideoUrl/videoMeta.transcriptionLink/musicMeta.musicId 등) — IG에서 alt를 뒤늦게 발견한 것과 같은 패턴.
embed/v2 URL을 postURLs에 그대로 투입 (https://www.tiktok.com/embed/v2/6646191924438895877)
POST clockworks~tiktok-scraper, postURLs:[embed URL] / 및 무료 oEmbed·HEAD 대조
유료
FAILED (apify run-failed, HTTP 400) · oEmbed 400 · 직접 HEAD 503
교란요인 분리함: 이 예시 postId(6646...)는 삭제된 게시물이라 정상 형식(/@espn/video/)으로도 400. 살아있는 postId를 /embed/v2/ 형식으로 oEmbed 조회해도 400 → embed URL 형식 자체가 미지원임이 별도로 확인됨(게시물 사망과 무관).
postId authoritative 검증 — 일부러 틀린 handle + 살아있는 postId (/@zzzwronghandlezzz/video/7649280049534799137)
POST clockworks~tiktok-scraper, postURLs:[틀린 handle URL] · 무료 oEmbed 대조
유료
success (HTTP 201) — apify·oEmbed 둘 다 정확한 게시물 반환
URL의 handle은 완전히 무시되고 postId만으로 해석됨 → embed/v2·shortlink 등 handle 없는 URL도 postId만 확보하면 /@x/video/{postId}로 감싸 fetch 가능. "handle을 못 찾아 쓰기 어렵다"는 audit의 우려에 대한 확정 답.
상류 정규화 결정의 근거. 6건 전부 즉시 해석되고 무료 → 유료 게이트와 무관하게 항상 수행 가능. ⚠ 6건이 전부 /video/ 로 떨어졌으나 프로필 공유 shortlink도 존재하므로 "shortlink=video" 고정은 금지, 해석 결과로 재분류. ⚠ ?_r=1&_t= 추적 파라미터 제거 필요.
{"version":"1.0","type":"rich","title":"Know Your Meme's Creator Profile","author_name":"Know Your Meme","embed_product_id":"knowyourmeme","embed_type":"profile"}
tiktok_profile — Apify clockworks~tiktok-scraper, profiles:["knowyourmeme"], resultsPerPage:1 (fetcher가 실사용하는 정확한 엔드포인트/입력 그대로 curl 재현) 실측 응답 원문:
{"id":"7658394009320099085","text":"What actually is Tung Tung Tung Sahur?...","createTimeISO":"2026-07-03T20:00:04.000Z","isAd":false,"playCount":3400000,"diggCount":340900,"commentCount":3276,"shareCount":95200,"collectCount":...,"hashtags":[{"name":"ramadan"},{"name":"indonesia"},...],"authorMeta":{"id":"6612606270220451846","name":"knowyourmeme","nickName":"Know Your Meme","verified":true,"signature":"Documenting memes...","bioLink":"https://linktr.ee/knowyourmeme","privateAccount":false,"createTime":1556120323,"following":127,"friends":47,"fans":2300000,"heart":42800000,"video":1486}}
social-fetcher.service.ts:255-256 case 'tiktok_search': ... return buildResult(routed, 'unsupported'); (TiktokFetcher.handles=['tiktok_profile','tiktok_video']에 tiktok_search 없음, 디스패치 전에 서비스가 unsupported로 조기 반환)
id-probe(shortlink/opaque URL redirect) 실측 응답 원문:
grep 'vm.tiktok.com' tiktok_*.json → 매치 없음. 3개 샘플 파일 전부 tiktok.com/@handle[...] 형태의 canonical URL만 존재.
vm.tiktok.com/ZNRTtUjSM → Location: https://www.tiktok.com/@solana/video/7655366815324867854?_r=1&_t=ZN-97Vf97clmEg
vm.tiktok.com/ZGd9UfYoM → Location: https://www.tiktok.com/@thejaneguy/video/7655071949407800590?_r=1&_t=...
tiktok.com/t/ZTSJ7TCua → Location: https://www.tiktok.com/@dakota.mcmurray.w/video/7653960203552967943?_r=1&_t=...
(나머지 3건도 동일 형태로 /@handle/video/{postId} 해석)
현재 fetcher(tiktok.fetcher.ts)는 Apify clockworks~tiktok-scraper를 정확히 코드 그대로 재현한 curl로 profile/video 양쪽 모두 실제 200/201 성공 확인. sourceKey(라우터가 뽑은 '7652462228196560159')와 raw item.id가 완전 일치해 라우터↔fetcher id 정합성 문제 없음.
acquisition 요약이 지적한 '팔로워는 매핑되나 계정생성일/인증여부는 raw에 있어도 fetcher가 파싱 안 함'을 실제 raw JSON에서 authorMeta.verified=true, authorMeta.createTime=1556120323 존재로 직접 재확인(추정 아님).
profiles:[handle] 입력은 '프로필 정보' 자체가 아니라 해당 계정의 최신 영상 1건을 반환하고 그 안에 authorMeta로 프로필 정보가 동봉되는 구조 — playCount/diggCount 등 fetcher가 담는 값은 엄밀히는 '최신 영상의 참여수'이지 '계정 누적 참여수'가 아님. 코드 주석에는 명시 안 돼 있어 문서화 시 유의 필요.
tiktok_search는 라우터가 sourceType은 분류하지만 social-fetcher.service.ts에서 디스패치 전에 'unsupported'로 조기 반환되어 fetcher에 도달하지 않음(fetcher.handles에도 tiktok_search 없음) — 샘플의 status:'unsupported'와 코드가 정확히 일치, 실제 네트워크 호출 자체가 발생하지 않는 구조임을 코드 Read로 확인.
TikTok oEmbed(무료)는 video/profile 양쪽 모두 HTTP 200을 반환하나 팔로워/참여수/인증/생성일 등 acquisition이 필요하다고 정의한 필드가 전혀 없어 실질적으로 백업 fallback(존재확인/타이틀·썸네일) 이상의 가치가 없음을 실측으로 재확인.
4. 객체별 필드 통합 (변동성 · 파싱상태 · 트레이딩 유용성) 👤 판단 반영
Creator/Content 객체별로 필드를 한 표에 통합. 값 변동성 = 값이 시간에 따라 변하나(immutable 역사적 사실 / mutable 드리프트 / derivable 계산값) → look-ahead 안전성이 여기서 도출(immutable=safe · mutable=conditional/as-of · derivable=unsafe). 파싱 상태 = 현재 파이프라인이 실제로 저장하나.
source: 확정 source: apify apidojo/tiktok-scraper ($0.30/1k) — fieldsDiffer=false, 현재 clockworks~tiktok-scraper 응답과 필드셋 사실상 동일. profiles:[handle] 호출은 실제로는 해당 계정의 '최신 영상 1건'을 반환하고 authorMeta 서브객체에 프로필 정보가 동봉되는 구조(계정 단위 집계 endpoint 아님) · fans/heart/video(count)/following/friends는 스크랩 시점 스냅샷 — scrapedAt과 페어링 없이 백테스트에 쓰면 look-ahead. createTime/verified는 저빈도 변경값이라 관측 윈도우 내 사실상 고정으로 취급 가능하나, age는 raw createTime만 저장하고 매 시점 as-of로 재계산해야 안전(미리 계산한 ageHours 저장은 unsafe)
필드
설명
값 변동성
파싱 상태(현재)
최종 결정
트레이딩 유용성
id
authorMeta.id — 계정 고유 숫자 ID (예: '6612606270220451846')
immutable
⚠️ actor제공·미파싱
🔑 key
context — 조인키 용도, 그 자체로 매매결정 안 바꿈
name (handle)
authorMeta.name — @핸들. 변경 가능(30일마다 1회) + 옛 핸들 타인 재할당이라 키 부적격 → 표시/해석용 속성으로만 보관(조인·dedup 금지)
mutable
✅ 캡처
keep
context — 사칭 판별 텍스트 비교 + URL→id 해석 경로. 조인·dedup 사용 금지(재할당 충돌)
nickName
authorMeta.nickName — 표시이름(display name), 예: 'Know Your Meme'
mutable
⚠️ actor제공·미파싱
skip
noise — 자유 변경 가능한 표시텍스트, 사칭 판별 보조 이상 아님
verified
authorMeta.verified — 인증 배지 여부(true/false)
immutable
⚠️ actor제공·미파싱
remove
noise — 검증된 원칙(verified 배지류는 구매가능이라 noise)에 따라 팀진정성 단독 결정 신호로 쓰지 않음, context 보조로만
createTime
authorMeta.createTime — 계정 생성 unix timestamp (예: 1556120323 → 2019-04-24)
immutable
⚠️ actor제공·미파싱
keep
actionable — 러그리스크 1순위 tell(신선 계정=사칭/스캠 확률↑), raw 저장 후 as-of age 재계산 시 look-ahead safe
signature (bio)
authorMeta.signature — 프로필 자기소개 텍스트
mutable
⚠️ actor제공·미파싱
keep
context — 팀진정성 보조 근거(공식 링크·소속 명시 여부), 단독 actionable 아님
authorMeta 하위 — 2026-07-29 실측에서 새로 확인된 필드들(문서 미기재였음). profileUrl=handle 파생, avatar류=이미지 URL, commerceUserInfo/ttSeller=커머스 계정 플래그, roomId=라이브 방 id(비방송 시 빈 문자열), digg=계정이 누른 좋아요 수
⚠️ apidojo 전환 보류 — 실측 근거 — 무료 플랜 API 차단 + 필드 7개 손실 미확증 + 절감액 월 ~$5(구독료 $29 미달). 전환 조건 = ①Apify Starter 가입 ②authorMeta.createTime 유무 확증. ①이 막혀 ②를 검증할 수 없다. 전체 비용 모델: [apify-cost-model.html](./apify-cost-model.html)
authorMeta.createTime/verified 매핑 추가 — raw 응답엔 있는데 tiktok.fetcher.ts가 파싱 안 함 — 러그리스크(계정 신선도)·팀진정성(인증) tell 유실. 트레이더 1순위라 field보다 시급
shortlink → canonical 정규화 (상류 수집 단계) — 실측 6/6 HEAD 301 해석 성공(무료). fetcher가 아니라 상류에서 정규화 — TikTok 유료 게이트 때문에 fetcher 해석은 스킵 엔트리를 영원히 shortlink로 남겨 dedup이 분기됨. 추적 파라미터(?_r&_t) 제거 + 해석 결과로 재분류(프로필 shortlink 가능성). vt.tiktok.com host whitelist 추가는 정규화 실패 안전망으로 유지. ⚠ 상류 파이프라인 수정은 이번 audit(router/fetcher) 범위 밖 — 별도 후속
tiktok_profile 스냅샷 한계 — 프로필 라우트가 집계 아닌 최신 영상 1건 스냅샷 — 채널 성장추세 못 봄. tag/music 페이지도 미분류(unknown)