Instagram 최종 결정 반영(2026-07-23) · 코드 반영 전

Social Fetcher URL Structure Audit — 2026-07-23
관련 코드: social-fetcher.router.ts(classify instagram 분기) · fetchers/instagram.fetcher.ts
관련 데이터: samples/instagram.json
소스 채택 권장도 low-medium (보조 소스로만, 게시물형 URL은 현재 구현상 폐기 수준)
프로필형 URL(390건, 유료 apify 게이트 필요)에서만 followersCount/biography가 살아있는 정보를 주고, 이마저 팀진정성·초기 러그체크 보조 근거 정도. 밈코인 모멘텀 발굴의 핵심인 '지금 얼마나 빨리 퍼지나'(engagement velocity)는 전체 IG URL의 약 30%를 차지하는 게시물/릴스형 URL에서 원본 Apify 응답엔 존재하지만 현재 InstagramFetcher 코드가 전혀 매핑하지 않아 6개 필드 모두 null 반환 — 밈모멘텀이 실제로 사는 지점(바이럴 릴스)에서 정작 아무것도 못 건지는 구조적 공백. 계정 생성일(핵심 러그 tell)도 IG 어떤 경로로도 확보 불가.
트레이더 관점: 밈 모멘텀은 X/Telegram에 압도적으로 더 많이 산다. Instagram은 실제 트레이더 커뮤니티에서 참고빈도 자체가 낮고, 이미지/릴스 위주 특성상 '빠른 매수 판단용 텍스트 tell'(리스크 공지, 청산 루머, 커뮤니티 반응)이 X/텔레그램만큼 빠르게 안 돈다. 프로필 URL이 걸려있으면 followersCount+biography로 '진짜 팀인가' 정도의 보조 확인용으로만 쓰고, 게시물/릴스 링크는 현재 상태로는 그냥 버리는 게 낫다(값이 안 채워짐) — 고칠 가치는 있지만(engagement velocity가 apify 응답에 이미 있음) 고쳐도 IG는 여전히 2군 소스.
✅ 최종 결정 (수기 audit 반영)
원칙: sourceKey는 unique & immutable & linkable — content에도 그 키가 있어야 creator로 분기 없이 연결. 같은 profile/content가 분기되면 안 됨.
조인 키
객체비고
Creator (profile)id (숫자 계정 id)content가 ownerId를 추가 쿼리 없이 제공(실측) → 숫자 id를 canonical 조인/dedup 키로. username은 표시속성+router sourceKey. 단 IG는 유료 게이트로 미fetch 다수 → 미fetch 엔트리는 id 없어 handle 폴백 병행 상시.
Content (post/reel)ownerId (creator 링크) + shortCode (콘텐츠 자체 id)creator 링크는 ownerId(숫자). 콘텐츠 고유 식별은 shortCode. ownerUsername은 표시속성.
sourceType 통합 맵
최종 타입포함 패턴
instagram (profile)/{handle}, html_fragment_polluted(정제 후 handle 살림)
instagram_post/p, /reel, /tv, /p/../embed, /{handle}/reel/{code}(라우터 shortcode 추출 수정) — 형식은 type/productType로 구분, split 안 함
instagram_story/stories/{handle}/{storyId} — key=storyId, fetch는 보류(24h 소멸)
공통 searchexplore_search — 콘텐츠 아님, 추후 소셜 공통 통합
공통 emptyempty_path(instagram.com//) — 건질 handle 없음
unknown / 보류highlights(안 씀), share_link(미관측 gap)
이 문서에서 쓰는 pill 범례
관측 source-data에서 실제 발견됨 웹리서치/실측 검색·curl·실제 호출로 확인 정상/유지 차단·버그·갭·drop 신규/분리 타입 fixed / mutable confidence: high medium low
목차   1a. 개념 글로서리 · 1b. URL 패턴 · 2. sourceType/sourceKey 판정 · 3. Fetch 테스트 + 획득 방법 · 4. 필드 통합 (변동성·파싱·판정) · 다음 액션

1a. 플랫폼 개념 글로서리

이 플랫폼을 이해하는 데 필요한 도메인 개념. URL/필드 설명에서 참조.

개념설명
프로필 URL (instagram.com/{username}/)표본의 68%(390/576)를 차지하는 최다 패턴. 계정 홈을 가리키며 트레일링 슬래시가 관례. discovered/website 필드 모두에서 관측됨 관측
게시물(post) URL — /p/{shortcode}/일반 피드 포스트(이미지/캐러셀/비디오). shortcode는 base64 유사 알파벳(A-Za-z0-9-_) 11자 내외 고유 식별자로 대소문자 구분함. 예: Bjnx75lFFQp, CJzBXNWj9_o, B6FZF-gBg7H 관측
릴스(reel) URL — /reel/{shortcode}/짧은 세로형 비디오 콘텐츠. shortcode 네임스페이스는 /p/ post와 동일 체계(같은 길이·문자셋)이나 경로 세그먼트가 다름. utm_source=ig_embed&utm_campaign=loading 같은 임베드 추적 파라미터가 흔히 붙음 관측
프로필+콘텐츠 결합 경로 — /{username}/reel/{shortcode}/관측 표본에서 username과 shortcode가 한 URL에 동시에 등장하는 3-세그먼트 패턴(petermsilveira/reel/DZ8NYQ8xnRp/). 파서가 sourceKey로 username(petermsilveira)만 추출해 shortcode 정보를 잃는 구조적 손실 지점 관측
임베드(embed) URL — /p/{shortcode}/embed/외부 사이트에 게시물을 iframe으로 삽입할 때 쓰는 렌더링 전용 경로. 콘텐츠 식별자는 post와 동일 shortcode 관측
스토리(story) URL — /stories/{username}/{story_id}/24시간 후 만료되는 임시 콘텐츠. story_id는 19자리 숫자(스노우플레이크 유사) 고유값. 표본에서 파서가 sourceKey를 'stories'(고정 세그먼트)로 오추출해 실제 username/story_id를 놓치는 버그성 패턴 발견(druski/3927059123933463192, wall/3923786242147689438) 관측
탐색/검색 URL — /explore/search/keyword/?q={query}해시태그·키워드 검색 결과 페이지. 프로필/콘텐츠가 아닌 검색 질의(q 파라미터, URL 인코딩된 공백 %20 포함)를 가리킴. sourceKey가 'explore'로 잘못 추출되는 동일 계열 파싱 이슈 관측
빈 프로필/루트 URL — instagram.com//username 세그먼트가 비어 sourceKey가 null이 되는 깨진/불완전 링크. discovered 필드에서만 발생하며 유효한 프로필을 가리키지 않음(3건) 관측
HTML 잔여물이 섞인 오염 URL임베드 코드를 그대로 긁어와 /u003c/a/u003e 같은 HTML 엔티티 유니코드 이스케이프가 경로에 섞인 파싱 실패 사례. username(whutzhizphace)만 유효하고 나머지는 노이즈 관측
sourceKey ≠ 콘텐츠 식별자 문제URL 템플릿이 /{value}/{value}/... 형태로 깊어질수록 현재 파서는 첫 번째 세그먼트(보통 username 또는 고정 키워드 'stories'/'explore')만 sourceKey로 취하고, 실제 구분력 있는 shortcode/story_id/query는 버림 — audit에서 반복 관측된 구조적 손실 패턴 관측
username(핸들)계정 고유 식별자. 영문 소문자/숫자/마침표/밑줄만 허용, 대소문자 구분 없이 동일 계정으로 취급(정규화 시 소문자화 권장). 표시명(display name)과는 별개 개념 웹리서치/실측
shortcode(미디어 코드)게시물/릴스/IGTV 각각에 부여되는 base64 유사 인코딩 식별자(대소문자 구분, -, _ 포함). 내부적으로 media_id(64bit 정수)를 인코딩한 값으로, media_id → shortcode 변환은 결정적이나 역방향은 API 호출 없이는 어려움 웹리서치/실측
IGTV / 비디오 URL — /tv/{shortcode}/과거 장편 비디오 전용 포맷(현재는 릴스/피드 비디오로 통합됨). shortcode 체계는 post와 동일 웹리서치/실측
하이라이트(highlight) URL — /stories/highlights/{highlight_id}/만료되지 않도록 프로필에 고정한 스토리 모음. story_id와는 다른 별도 ID 네임스페이스 웹리서치/실측
해시태그 페이지 — /explore/tags/{tag}/특정 해시태그로 태깅된 게시물 모음 페이지. search/keyword 검색과 달리 태그명이 경로 세그먼트에 직접 노출됨 웹리서치/실측
위치(location) 페이지 — /explore/locations/{location_id}/{slug}/특정 지리적 위치 태그가 달린 게시물 모음. location_id는 숫자, slug는 사람이 읽는 이름 웹리서치/실측
공유(share) 단축 링크 — instagram.com/share/{code} 등특정 콘텐츠/프로필을 지칭하는 단기 리다이렉트 링크. 만료되거나 앱 딥링크로 리다이렉트되는 경우가 많아 shortcode/username과 별도 취급 필요 웹리서치/실측
지표: 팔로워(followers) / 팔로잉(following) / 게시물 수(post count)프로필 단위 공개 지표. 트위터의 팔로워와 동일 개념이나 Instagram은 following 수도 기본 공개 웹리서치/실측
지표: 좋아요(likes) / 댓글(comments) / 조회수(views) / 저장(saves) / 공유(shares)게시물·릴스 단위 참여 지표. 릴스는 views가 핵심 노출 지표로 별도 집계되며, 계정 설정에 따라 좋아요 수가 비공개(숨김) 처리될 수 있음 웹리서치/실측
플랫폼 특유 개념: 컬래버(collab) 게시물두 계정이 공동 작성자로 게시하는 포스트/릴스. 하나의 shortcode를 여러 프로필이 공유하는 X의 quote/retweet과는 다른 '공동 소유' 구조 웹리서치/실측
플랫폼 특유 개념: 리포스트(repost)/공유는 네이티브 리트윗 없음X의 retweet·Telegram의 forward와 달리 Instagram은 네이티브 재게시 기능이 약함(스토리 공유 또는 서드파티 리포스트 앱에 의존) — 콘텐츠 재사용 추적 시 별도 원본 대비 대사(reconciliation) 로직 필요 웹리서치/실측

1b. URL 패턴

instagram.comdtfa
프로필 URL — instagram.com/{handle} (390건, 최다)
instagram.compBjnx75lFFQp
게시물/릴스 URL — instagram.com/{p|reel|tv}/{shortcode}/ (170건 합산)
instagram.comstoriesdruski3927059123933463192
스토리 URL — instagram.com/stories/{handle}/{storyId}/ (router는 handle·storyId를 버리고 'stories' 리터럴만 채택 — 실제 식별자는 무시됨)
URL 패턴의미ID 후보관측출처/비고
instagram.com/{handle}프로필(계정) 페이지handle (username, router가 소문자화)관측 390건router: segments[0]이 p/reel/tv가 아니면 else 분기 → sourceKey=lc(segments[0]). fetcher는 apify instagram-scraper(details, 유료)로 username/followersCount/postsCount/verified/biography/externalUrl 반환(username만 fixed, 나머지 mutable). 샘플은 전부 status=skipped_paid(유료 게이트 미실행).
instagram.com/p/{shortcode}[/...]게시물(피드 포스트)shortcode (segments[1], 대소문자 원형 유지)관측 152건router: ['p','reel','tv'].includes(segments[0]) && segments[1] → sourceKey=segments[1] 원형. instagram.json의 template 'instagram.com/{value}/{value}' 170건 중 p/reel 혼재(reel도 동일 규칙).
instagram.com/reel/{shortcode}[/...]릴스(짧은 영상) 게시물shortcode (segments[1], 원형)관측 18건동일 분기(p/reel/tv 공통 처리). instagram.json 170건 template 'instagram.com/{value}/{value}'는 p와 reel이 합산 집계됨(수기 분리 불가, 샘플엔 둘 다 존재).
instagram.com/{handle}/reel/{shortcode}프로필 경유 릴스 링크(계정명이 1번째 세그먼트)실제로는 shortcode가 진짜 게시물 식별자이지만, router는 segments[0](handle)을 sourceKey로 채택 — shortcode는 버려짐관측 5건router 조건은 segments[0]가 p/reel/tv일 때만 shortcode를 취함. 이 형태는 segments[0]=handle, segments[1]='reel'이라 조건 불일치 → else 분기로 handle만 채택(petermsilveira). 게시물 단위 식별이 아니라 프로필 단위로 뭉개짐 — router 로직상 알려진 사각지대.
instagram.com/p/{shortcode}/embed/임베드용 게시물 iframe URLshortcode (segments[1], 원형)관측 3건p/reel/tv 분기로 정상 shortcode 추출.
instagram.com/stories/{handle}/{storyId}24시간 한정 스토리(개별 스토리 아이템)실제 식별은 handle+storyId 조합이지만 router는 segments[0]='stories' 리터럴을 sourceKey로 채택 — storyId·handle 모두 버려짐관측 2건segments[0]='stories'가 p/reel/tv 목록에 없어 else 분기 → sourceKey=lc('stories')='stories' 고정값. 서로 다른 스토리 2건이 동일 sourceKey로 충돌(druski/... vs wall/...).
instagram.com/explore/search/keyword/?q={query}검색 결과 페이지(계정/게시물 아님)없음 — 콘텐츠 식별자가 아니라 검색어(query)이며 router는 'explore' 리터럴을 sourceKey로 채택관측 1건else 분기로 segments[0]='explore' 그대로 채택. fetcher(InstagramFetcher)는 이 URL을 그대로 apify에 directUrls로 넘겨 프로필처럼 시도 — 검색페이지라 실질적으로 무의미한 fetch.
instagram.com/[빈 경로]말미 슬래시만 있는 손상 URL(계정/게시물 없음)없음 — segments가 비어 sourceKey=null관측 3건normalize()는 통과하지만 segments=[] → segments[0]=undefined → else 분기 lc(undefined)=null.
instagram.com/{handle}/[HTML 파편이 경로로 오염]잘못 파싱된 임베드 HTML 조각이 URL 뒤에 붙은 손상 링크handle (segments[0], 나머지는 쓰레기 세그먼트)관측 2건else 분기로 handle만 채택, u003c/u003e 등 잔여 세그먼트는 무시됨.
instagram.com/tv/{shortcode}IGTV 게시물(레거시, 현재 reel로 통합 추세)shortcode (segments[1], 원형)웹리서치/실측표본에 미관측이나 router의 ['p','reel','tv'] 목록에 이미 tv가 포함되어 정상 처리됨 — 코드 근거로 정식 지원 확인, 표본만 없음.
instagram.com/{handle}/highlights/{highlightId}스토리 하이라이트(고정 스토리 모음)highlightId (표준 인스타그램 URL 구조상 진짜 식별자)웹리서치/실측웹지식 기반 보완(미관측). router 규칙 적용 시 segments[0]=handle이 p/reel/tv가 아니므로 else 분기로 handle만 채택되고 highlights/{id}는 버려질 것으로 예상 — stories 패턴과 동일한 사각지대.
instagram.com/share/{p|reel}/{shareCode}공유 링크(단축형 게시물/릴스 링크, 클라이언트에서 리다이렉트)shareCode — 실제 shortcode와 다른 별도 토큰(리졸브 필요)웹리서치/실측웹지식 기반 보완(미관측). router상 segments[0]='share'가 p/reel/tv 목록에 없어 else 분기로 'share' 리터럴이 sourceKey가 될 것으로 예상 — explore/stories와 동일 유형의 사각지대.

2. sourceType / sourceKey 판정 👤 판단 반영

2.1 sourceType 적합도 👤 판단 반영

패턴현재 type판정/제안sourceKeyconf근거사용자 결정
instagram.com/{handle}
instagram (sourceKey=segments[0] 소문자)instagram (profile 전용으로 확정, post류와 분리)profilehighelse 분기가 handle을 정확히 소문자로 추출. observed 390건으로 최다 볼륨. fetcher(apify instagram-scraper, resultsType='details')가 원래 의도한 프로필 조회 경로와 정합적이며 username(fixed)/followersCount·postsCount·verified·biography·externalUrl(mutable) 모두 트레이더 관점 유효 필드(도달·러그리스크·팀진정성). 단 sourceType 'instagram'을 post류와 공유 중이므로 실질적으론 split 대상의 절반 — 이 패턴 자체는 정상 분류이나 옆 패턴들과 같은 sourceType을 쓰는 게 문제.✅ instagram(profile) 확정. 조인키=숫자 id(username은 표시+미fetch시 폴백). 기존 제안 유지.
instagram.com/p/{shortcode}[/...]
instagram (sourceKey=shortcode 원형, profile과 동일 sourceType)instagram_post (신규, key=shortcode)post_feedmedium라우터는 shortcode 추출까진 정확하지만, sourceType이 profile과 동일('instagram')이라 fetcher가 무조건 apify 'details' 모드로 이 URL을 던진다. 이 모드가 post URL에 대해 실제로 무엇을 반환하는지(글쓴이 profile 재조회인지 post 자체의 like/comment/caption인지) 확인된 바 없음 — 샘플 전부 skipped_paid라 실증 불가. 트레이더 관점에서 게시물 단위 신호(참여 속도·원본여부·캡션)는 프로필 신호(팔로워수)와 다른 축이라 field-necessity/trader-value 둘 다 별도 sourceType 필요. split 후 fetcher 응답 스키마를 실제 검증 필요.✅ instagram_post 확정(단일 타입, type/productType로 형식 구분, split 안 함). 기존 제안 유지.
instagram.com/reel/{shortcode}
instagram (p/reel/tv 공통 분기, sourceKey=shortcode 원형)instagram_post (p와 통합) 또는 instagram_reel (분리) — 우선 instagram_post 통합 권장post_reelmediumpost와 동일 근거(same 분기, same 문제). p와 reel을 별도 sourceType으로 더 쪼갤지는 트레이더 관점에서 릴스=하이프 확산 속도가 피드 포스트와 체감 다르다는 점에서 검토할 만하나, instagram.json 170건 template 집계가 이미 p/reel 혼재로 수기 분리 불가능한 상태라 당장은 instagram_post로 합쳐 profile과만 분리하는 게 실효적. 세분화는 데이터 쌓인 뒤 재검토.✅ instagram_post로 통합(instagram_reel 분리 안 함). 기존 제안 유지.
instagram.com/{handle}/reel/{shortcode}
instagram (segments[0]=handle이 p/reel/tv 목록에 없어 else 분기, shortcode 유실)instagram_post (key=shortcode, segments[1]==='reel'&&segments[2] 조건 추가)post_via_profile_prefixhighrouter 로직 결함이 명확함(설계 의도 아님) — p/reel/tv 체크가 segments[0]에만 걸려 있어 프로필 프리픽스가 붙은 릴스 링크를 통째로 놓친다. 게시물 단위 식별(신선도/원본여부 판별에 필수, context.md 카피캣 원칙)이 프로필 단위로 뭉개지는 것은 field-necessity 관점에서 신호 손실. 수정은 간단(segments 어디서든 'reel' 다음에 shortcode가 있으면 채택) — 우선순위 높음.✅ instagram_post 확정 — 라우터가 shortcode 추출하도록 수정(content↔creator 분리 원칙). 기존 제안 유지, discard 아님.
instagram.com/p/{shortcode}/embed/
instagram (p/reel/tv 분기, sourceKey=shortcode 원형)instagram_post (post_feed과 동일 sourceType으로 흡수)post_embedhighshortcode 추출 자체는 정상이며 /embed/ 접미사는 UI variant일 뿐 콘텐츠 식별에 영향 없음. post_feed 패턴과 완전히 동일한 엔티티이므로 별도 취급 불필요 — instagram_post로 합류.✅ instagram_post로 흡수. 기존 제안 유지.
instagram.com/stories/{handle}/{storyId}
instagram (sourceKey='stories' 리터럴 고정, 서로 다른 스토리 충돌)gap 방치 권장 — 신규 sourceType 만들 실익 낮음(story는 24시간 뒤 소멸, 비동기 apify fetch 시점엔 이미 사라졌을 공산 큼)storymediumrouter의 sourceKey 충돌(서로 다른 콘텐츠가 동일 키 'stories')은 명백한 버그지만, trader-value 페르소나 기준으론 story 자체가 ephemeral이라 라이브 fetch 파이프라인(토큰당 비동기 apify 호출)로 잡아내기 전에 사라질 가능성이 높아 실효 신호 가치가 의심됨. 최소 수정(sourceKey를 ${handle}/${storyId}로 충돌만 해소)은 하되, 정식 instagram_story sourceType+fetcher 투자는 보류 권장 — 사람 판단 필요.✅ instagram_story 타입(key=storyId)로 정의하되 fetch 보류(24h 소멸). sourceKey 충돌만 최소 수정.
instagram.com/explore/search/keyword/?q={query}
instagram (segments[0]='explore' 리터럴, fetcher가 이를 directUrls로 apify에 그대로 전달)unknown (콘텐츠 식별자 없음, 라우터에서 명시적으로 제외)explore_searchhigh검색결과 페이지는 계정도 게시물도 아니라 애초에 알파 신호가 될 수 없는 URL — 현재는 'instagram' sourceType으로 분류되어 유료 apify 호출까지 발생할 소지가 있음(불필요 비용). explore/search/hashtag 등 콘텐츠가 아닌 예약경로는 X_RESERVED_ROOTS처럼 별도 set으로 걸러 unknown 처리 권장.✅ 공통 search로 통합(추후 7소셜 공통 정의). 콘텐츠 아님 — 라우터에서 제외.
instagram.com/[빈 경로]
instagram (sourceKey=null)instagram (현행 유지, sourceKey=null로 사실상 무해)empty_pathhigh손상 URL이며 sourceKey가 null로 떨어져 다운스트림에서 사실상 스킵되거나 눈에 띄게 실패함 — 별도 처리 불필요. 볼륨도 3건으로 미미.✅ 공통 empty 처리(건질 handle 없음, key=null). 기존 제안과 동치.
instagram.com/{handle}/[HTML 파편 오염]
instagram (else 분기, handle만 채택, 잔여 세그먼트 무시)instagram (현행 유지)html_fragment_pollutedmedium파싱 오염이 크롤링/저장 파이프라인 상위 단의 문제이지 라우터 책임 밖. 현재 handle만 살리는 degrade가 합리적 fallback이며 볼륨 2건으로 낮음. 근본 수정은 상위 크롤러 쪽 이슈.✅ 정제 후 instagram profile로 살림(handle 유효) — discard 아님. 기존 제안 유지.
instagram.com/tv/{shortcode}
instagram (p/reel/tv 공통 분기, sourceKey=shortcode 원형)instagram_post로 흡수(post_feed/post_reel과 동일 sourceType)tv_igtvhigh코드상 이미 정상 지원(목록에 tv 포함). IGTV가 레거시화되어 reel로 통합되는 추세이므로 별도 sourceType을 새로 만들 실익 없이 instagram_post로 함께 묶는 게 맞음.✅ instagram_post로 흡수. 기존 제안 유지.
instagram.com/{handle}/highlights/{highlightId}
instagram (else 분기 예상, handle만 채택 — story와 동일 사각지대)gap, 낮은 우선순위 — 관측 0건이라 즉시 조치 불필요highlightlow웹지식 기반 추정이며 표본 미관측. story 패턴과 동일한 구조적 사각지대로 예상되나 실제 발생 빈도를 모르는 상태에서 사전 대응은 과잉 설계 위험. 향후 관측되면 story 수정과 함께 묶어 처리 권장.✅ unknown/보류(미관측, 안 씀). 기존 제안 유지.
instagram.com/share/{p|reel}/{shareCode}
instagram (else 분기 예상, 'share' 리터럴이 sourceKey가 될 것으로 예상)gap, shareCode는 실제 shortcode와 다른 리다이렉트 토큰이라 raw 리다이렉트 리졸브 없이는 sourceKey로 못 씀share_linklow웹지식 기반 추정, 미관측. 설령 라우터를 고쳐도 shareCode 자체가 진짜 shortcode가 아니라 클라이언트 리다이렉트로 리졸브해야 하는 간접 토큰이라 단순 세그먼트 추출로는 해결 안 됨(추가 HTTP resolve 필요) — 관측 빈도 확인 후 투자 여부 판단 필요.✅ gap 보류(미관측, redirect 토큰이라 단순 세그먼트 추출 불가). 기존 제안 유지.

2.2 id 추출 적합도 👤 판단 반영

패턴추출 idresolve?판정conf근거사용자 결정
instagram.com/{handle}lc(segments[0]) = username, e.g. 'dtfa'okhighrouter else-분기: sourceKey=lc(segments[0]). live-test 302없이 200 직결, Apify details 호출로 username/followersCount 등 실제 프로필 데이터와 일치 확인(gamesradar 예시). IG username은 계정당 유일(unique) — 진짜 고유 식별자.✅ router sourceKey=handle 유지. 단 저장 조인키는 profile.id(숫자) — content.ownerId와 매칭. handle은 미fetch시 폴백.
instagram.com/p/{shortcode}[/...]segments[1] 원형 shortcode, e.g. 'Bjnx75lFFQp'okhigh['p','reel','tv'].includes(segments[0]) 분기로 segments[1] 그대로 채택 — IG shortcode는 게시물 단위 전역 유일 base62 id. live probe에서 302 리다이렉트가 관측됐지만 이는 '비로그인 접근 시 IG가 무관 계정으로 튕기는' 데이터-freshness/인증 이슈이지 라우터가 잘못된 세그먼트를 뽑은 게 아님(shortcode 자체는 URL 그대로 정확히 추출됨). 단, fetcher(InstagramFetcher)가 profile 전용 스키마(username/followersCount 등)만 파싱해 게시물 아이템에서는 전부 null이 되는 별도 결함이 있음 — 이는 field-necessity/fetcher 이슈이지 id 식별자 자체의 결함이 아님.✅ shortCode=콘텐츠 키 확정. creator 링크는 ownerId(숫자).
instagram.com/reel/{shortcode}segments[1] 원형 shortcode, e.g. 'DZvu_BVzM5W'okhighp/reel/tv 공통 분기로 정상 추출. live probe에서 200(리다이렉트 없음) 및 유료 Apify 프로브에서 actor가 shortCode=DZvu_BVzM5W를 포함한 완전한 post-type 아이템을 반환 — sourceKey가 실제 콘텐츠와 일치하는 고유 식별자임을 직접 확인. (fetcher가 이 필드들을 파싱하지 못해 null을 반환하는 것은 별도 field-necessity 결함.)✅ shortCode=콘텐츠 키 확정(post와 동일). creator 링크는 ownerId.
instagram.com/{handle}/reel/{shortcode}segments[0]=handle만 채택('petermsilveira'), shortcode(segments[2])는 버려짐wronghighrouter 조건은 segments[0]이 p/reel/tv일 때만 shortcode를 취하는데 이 형태는 segments[0]=handle이라 조건 불일치 → else 분기로 handle만 남음. URL의 '의미'는 특정 릴스(게시물)인데 sourceKey는 게시물 단위 고유성을 잃고 프로필 단위로 뭉개짐 — 동일 계정의 서로 다른 릴스 링크들이 모두 같은 sourceKey로 충돌. handle 자체는 유효한 식별자이지만 pattern이 주장하는 콘텐츠 단위(릴스)와 불일치.✅ 라우터 수정해 shortcode 추출 → instagram_post. handle만 뽑던 버그 교정.
instagram.com/p/{shortcode}/embed/segments[1] 원형 shortcode(embed 세그먼트는 무시)okhighp/reel/tv 분기가 segments[1]만 사용하므로 뒤따르는 /embed/는 sourceKey에 영향 없음 — pattern 2/3와 동일 근거로 정상 추출.
instagram.com/stories/{handle}/{storyId}'stories' 리터럴 고정값(lc(segments[0]))wronghighclassify()에 stories 전용 분기가 없어 else로 빠짐 → segments[0]='stories'가 p/reel/tv 목록에 없어 그대로 sourceKey='stories' 고정. live-test에서 서로 다른 두 스토리(druski/... vs wall/...)가 동일 sourceKey로 충돌하는 것을 직접 확인. 유료 probe는 actor가 실제로는 handle(druski)의 프로필 데이터를 정확히 반환함을 보여주지만, 저장되는 join key는 여전히 'stories'라서 다운스트림 조인/dedup에서 붕괴됨 — 명백한 router 사각지대(문서에도 명시).
instagram.com/explore/search/keyword/?q={query}'explore' 리터럴 고정값wronghigh검색 페이지는 애초에 콘텐츠 식별자가 없는데(idCandidate='없음') else 분기가 segments[0]='explore'를 그대로 sourceKey로 채택 — null이 아닌 비어있지 않은 고정 문자열이라 서로 다른 검색어(query)를 가진 URL들이 전부 같은 sourceKey로 충돌할 위험. fetcher도 이를 directUrls로 그대로 apify에 넘겨 프로필처럼 시도하므로 무의미한 유료 호출까지 발생.
instagram.com/[빈 경로]null (lc(undefined))okhighsegments=[]이므로 else 분기가 lc(undefined)=null을 반환 — 식별자를 주장하지 않고 정확히 '식별 불가'로 abstain. live-test에서 IG가 200/201로 빈 페이지를 반환함을 확인했고, 유료 probe에서는 Apify actor가 이 URL을 정규식 위반으로 즉시 400 거절 → fetcher가 status='error'로 표면화(성공으로 위장되지 않음). 이 경로는 안전.
instagram.com/{handle}/[HTML 파편이 경로로 오염]segments[0]=handle만 채택, 나머지 쓰레기 세그먼트 무시okmediumelse 분기가 segments[0]만 취하므로 HTML 파편(u003c 등) 오염 세그먼트는 자연히 버려지고 실제 handle만 남음 — 결과적으로 올바른 식별자에 도달. 다만 원본 handle이 파싱 과정에서 이미 손상됐을 가능성(오염이 handle 자체를 침범했는지)은 live-test로 별도 확인되지 않아 confidence는 medium.
instagram.com/tv/{shortcode}segments[1] 원형 shortcodeokmedium표본 미관측이나 router 코드상 ['p','reel','tv'] 동일 분기로 처리되어 reel/p와 완전히 같은 로직 — reel 패턴에서 live-test로 이미 검증된 근거(shortCode 필드 일치)를 코드 동일성으로 그대로 전이 가능. 다만 tv 자체로 별도 live probe는 없었으므로 confidence는 medium.
instagram.com/{handle}/highlights/{highlightId}segments[0]=handle만 채택(예측), highlightId는 버려질 것으로 추정wrongmedium미관측이라 live-test 없음(웹지식 기반 추정). 코드 논리로 보면 segments[0]=handle이 p/reel/tv에 없으므로 else 분기 → sourceKey=handle만 남고 highlightId(진짜 하이라이트 단위 식별자)는 소실 — pattern 4(핸들경유 릴스)와 동일한 사각지대. live-test 미실행이므로 userDecision으로 실제 확인 필요.미관측 패턴이라 실제 URL로 id-probe 재현 필요 — 사각지대 추정의 확정 여부 확인 요망
instagram.com/share/{p|reel}/{shareCode}'share' 리터럴 고정값(예측)wrongmedium미관측(웹지식 기반 추정). segments[0]='share'가 p/reel/tv 목록에 없어 else 분기로 'share' 상수가 sourceKey가 될 것으로 예상 — stories/explore와 동일 유형(서로 다른 shareCode를 가진 URL들이 전부 충돌). live-test 미실행이므로 실측 확인 필요.미관측 패턴이라 실제 share 링크로 id-probe 재현 필요 — router가 'share'를 리터럴로 채택하는지 실측 확인 요망
커버리지 갭 — router가 post류(p/reel/tv/embed)를 profile과 동일 sourceType 'instagram'으로 묶어 fetcher가 항상 apify 'details' 모드로만 호출 — post URL에 대해 실제 무엇을 반환하는지 실증 데이터 없음(샘플 전부 skipped_paid, 유료 게이트 미실행)
커버리지 갭 — {handle}/reel/{shortcode} 형태에서 shortcode가 유실되고 프로필 단위로 뭉개지는 라우터 버그(observed 5건) — segments[0]에만 p/reel/tv 체크가 걸려있어 발생
커버리지 갭 — stories/{handle}/{storyId}가 sourceKey='stories' 리터럴로 고정되어 서로 다른 스토리 2건이 동일 키로 충돌
커버리지 갭 — explore/search 같은 비콘텐츠(검색) 경로가 걸러지지 않고 'instagram' sourceType으로 분류되어 fetcher가 무의미한 유료 apify 호출을 시도할 소지
커버리지 갭 — highlights/share 공유링크 패턴은 웹지식상 존재가 확실하나 표본 미관측 — 실제 발생 빈도 데이터 없이는 우선순위 판단 불가

3. Fetch 테스트 + 획득 방법

3.1 획득 사다리 (공짜 → 유료)

방법비용설명트레이드오프conf
확정 ✅ (2026-07-21)Apify 유지 — actor를 prodiger/instagram-scraper(저가)로 교체 잠정 채택. 현행 apify~instagram-scraper는 검증된 fallback adoptedprodiger $1.30/1k vs 현행 ~$2.50/1k. 볼륨: IG=전체 소셜 URL의 5.8%(576/9,891), 일 ~80건 → 월 ~2,500건 → 월 $3.3 vs $6.3Creator(프로필)=이 actor. Content(게시물/릴스)=동일 actor + 파싱 추가로 확정(source 교체 아님). Story=보류(24h 소멸). 공식 무료(oEmbed/Business Discovery)는 지표 부족·커버리지 낮아 탈락, 신규 통합(HikerAPI $0.6/1k)은 저볼륨이라 절대 절감 미미(월 $~5)로 비채택.prodiger 실제 코드 전환 시 응답 스키마가 현행과 동일 drop-in인지 유료 1회 검증 필요(미검증). 아니면 현행 actor 유지 — 비용차 월 $3로 미미. 크로스-플랫폼 단일 벤더(EnsembleData 등)는 7소스 일괄 단계에서 재검토.high
0 (현재 코드 구현 상태 — Read로 직접 확인)라우터: instagram.com/instagr.am → sourceType='instagram' 단일종류. /p|/reel|/tv/<code>는 sourceKey=게시물 shortcode(원형), 그 외는 sourceKey=핸들(소문자). 즉 프로필과 게시물이 sourceType 레벨에서 구분되지 않고 fetcher도 1개(InstagramFetcher)뿐. 구현됨(무료/공식 경로는 전혀 없음, 최초부터 유료 하나로 시작)Apify actor 'apify~instagram-scraper' pay-per-event, 아래 rank4와 동일fetcher는 URL 종류(프로필/게시물) 구분 없이 무조건 Apify apify~instagram-scraper를 resultsType:'details', resultsLimit:1로 호출하고, InstagramItem 인터페이스는 username/followersCount/postsCount/verified/biography/externalUrl(프로필형 필드)만 파싱한다. valueNature: username=fixed, 나머지는 mutable.게시물 URL(/p,/reel,/tv)을 넣어도 actor 응답의 게시물형 필드(작성자 핸들, 게시일, 좋아요/댓글수)는 InstagramItem에 선언돼 있지 않아 사실상 버려짐 — 우리가 필요로 하는 '게시물 작성자/게시일/참여수'는 현재 코드로는 못 채움. 계정 생성일(createdAt)도 Apify 응답에 없어 애초에 못 채우는 필드.high (소스 코드 직접 Read로 확인)
1 (공식·무료, 그러나 우리 요구사항엔 사실상 무용)Instagram oEmbed API (Meta, 2026-06-15부터 토큰·App Review 불필요로 완화) 미구현무료게시물(포스트/릴스/캐러셀) URL을 넣으면 html 임베드 코드와 version/provider_name/width 정도만 반환. thumbnail_url/author_name은 최근 deprecate 진행 중.좋아요/댓글수, 게시일, 팔로워수 등 우리가 필요로 하는 필드가 애초에 응답에 없음(임베드용 API라 계정/게시물 메타데이터 API가 아님). 프로필 자체는 아예 조회 불가(공개 게시물만 대상, 스토리·비공개계정 불가). 사실상 채택 불가.high (Meta 공식 문서 직접 인용)
2 (공식 API, 조건부 무료)Instagram Graph API — Business Discovery 미구현무료(호출 자체는 과금 없음, 단 자체 Meta 앱 등록 + IG Business/Creator 계정 연결 필요)우리 소유의 IG Business/Creator 계정을 하나 연결한 뒤 GET /{our-ig-id}?fields=business_discovery.username(target){followers_count,media_count,biography}로 '다른' 계정의 팔로워수/게시물수/바이오를 조회 가능. rate limit 200 req/hour/user-token.대상 계정이 Business 또는 Creator 계정으로 전환돼 있어야만 조회 가능 — 크립토 토큰 관련 개인/익명 인스타 계정은 대부분 일반 개인계정일 가능성이 높아 실제 커버리지는 낮을 것으로 예상됨(실측 필요). 게시물 단위 참여수(좋아요/댓글)는 media 필드로 추가 조회해야 하며 대상 계정이 미디어를 공개 허용했을 때만 가능. 계정 생성일 필드는 Graph API에 없음(비공식 정보).medium (공식 엔드포인트 존재는 확실하나, 벤더 블로그(keyapi.ai, elfsight 등)의 '실사용 커버리지' 주장은 검증 안 된 마케팅성 콘텐츠라 회의적으로 취급함)
3 (서드파티 저가/무료티어 API — RapidAPI 등)RapidAPI 마켓 비공식 Instagram 래퍼 (예: 'Instagram Scraper', 'Free Instagram Scraper' 등 13개 이상 리스팅), 또는 HikerAPI/RocketAPI 같은 전문 서드파티 미구현무료~$100+/월 티어제 또는 요청당 과금(품질/신뢰도 벤더별 상이)핸들/URL 넣으면 팔로워수·게시물·참여수까지 폭넓게 반환한다고 광고. RapidAPI 마켓 리스팅은 품질 편차가 매우 크고 Meta의 클레임으로 자주 내려감. HikerAPI/RocketAPI는 상대적으로 안정적이라는 평.Instagram ToS의 자동수집 금지 조항 위반 소지(특히 쿠키 기반 인증형은 우리 IG 계정 자체가 정지될 위험). SLA·성공률 미공개인 곳이 많아 프로덕션 안정성 담보 안 됨. 가격/성능 주장은 벤더 자체 발행 블로그가 다수라 신뢰도 낮게 봐야 함.low (검색 결과 상당수가 SEO성 벤더 비교 블로그로, 벤더 편향 강함 — 실사용 검증 없이는 채택 비권장)
4 (Apify 서드파티 — 현재 채택된 유료 경로)Apify apify/instagram-scraper (actor id: apify~instagram-scraper, 현재 코드가 쓰는 것) — 프로필/게시물 상세 모두 커버 가능한 범용 actor 구현됨(코드에서 확인)Pay-Per-Event: 무료플랜 $2.70/1,000 result, Starter $2.30/1,000, Scale $1.90/1,000, Business $1.50/1,000 (result 종류별로 세분 과금 — post/comment/profile 등 이벤트마다 단가 다름)directUrls에 프로필/게시물 URL을 그대로 넣으면 actor가 URL 종류를 자동 판별해 프로필 상세 또는 게시물 상세(작성자, 게시일, 좋아요·댓글수 포함)를 반환. 즉 actor 자체는 우리가 필요로 하는 게시물 참여수 필드도 이미 주고 있을 가능성이 높음 — 현재 fetcher의 InstagramItem 타입이 그걸 안 받고 있는 것으로 보임(위 rank0 참고).저볼륨 온디맨드 목적엔 pay-per-event 방식이 합리적이나, Apify 플랫폼 자체의 vendor lock-in과 Instagram 측 변경에 따른 actor 유지보수 리스크(actor가 언제든 깨질 수 있음)는 존재. 계정 생성일 필드는 이 actor도 제공 안 함(Instagram이 아예 공개 안 하는 정보).high (actor 존재/가격은 Apify 공식 스토어 페이지 기준, 코드 매칭도 확인됨). 단 '어떤 필드가 실제로 반환되는지'는 Apify 문서 페이지를 직접 열람하지 않고 검색 스니펫만 봤으므로 실제 스키마는 actor README로 재확인 권장.
5 (자체 호스팅 무료 스크레이핑 — 고위험, 비권장)Instaloader 등 오픈소스 self-hosted unofficial scraper 미구현명목상 무료(단 우회를 위한 residential proxy + TLS fingerprint spoofing(curl_cffi) 비용 발생 가능)IG 세션쿠키로 프로필/게시물 메타데이터를 직접 크롤링. 팔로워수, 게시일, 참여수 등 원하는 필드 대부분 이론적으로 수집 가능.익명 요청은 30초당 1~2건 수준으로 강하게 rate limit, 세션쿠키 사용은 명백한 ToS 위반으로 계정 영구정지 위험. 커뮤니티 평가도 '개인용 일회성 다운로드용이지 프로덕션 부적합'으로 결론. 저볼륨 온디맨드라도 '외부 백엔드'라는 우리 요구사항(안정성/재현성)에 안 맞음.medium (GitHub 이슈/Instaloader 공식 트러블슈팅 문서 기반이라 근거는 탄탄하나, '2026년 실제 차단 임계값' 수치는 커뮤니티 추정치)

3.2 공식 API 상세

확신도 medium — 공식 API로 우리가 필요로 하는 4개 필드(팔로워수, 생성일, 게시물 작성자, 게시일, 참여수)를 모두 채울 수 있는 단일 무료 경로는 없다. oEmbed(무료)는 게시물 임베드 메타데이터만 주고 팔로워/참여수가 없다. Business Discovery(무료지만 조건부)는 팔로워/게시물수/바이오는 주지만 대상 계정이 Business/Creator로 전환된 경우에만 동작하며, 크립토 토큰 관련 익명/개인 계정 다수는 이 조건을 충족 못 할 가능성이 크다. '계정 생성일'은 어떤 공식 API도 노출하지 않는다(Instagram이 애초에 비공개 정보로 취급).
단계내용conf
게시물 메타데이터(임베드용) — Instagram oEmbed2026-06-15부터 access token/App Review 불필요. GET graph.facebook.com/v22.0/instagram_oembed?url=<post_url>. 반환: html, provider_name, width 등. 팔로워/참여수 없음.high
프로필 팔로워/게시물수 — Graph API Business Discovery자체 Meta 앱 등록 + 우리 소유 IG Business/Creator 계정 1개를 앱에 연결해야 호출 가능. GET /{our-ig-id}?fields=business_discovery.username(target){followers_count,media_count,biography}. rate limit 200req/hr/user-token. 대상이 Business/Creator 계정일 때만 동작.medium
게시물 참여수(좋아요/댓글) — Graph API media 필드Business Discovery로 얻은 media id에 대해 추가로 like_count/comments_count 필드 조회. 역시 대상 계정이 Business/Creator이고 미디어 공개 허용 시에만.low (문서상 가능성만 확인, 실제 크립토 관련 계정 커버리지는 실측 안 됨)
계정 생성일 / 인증(verified) 여부공식 API에 계정 생성일 필드 자체가 없음. verified(파란 배지) 여부도 Graph API 공개 필드에 없는 것으로 보임 — 필요하면 유료 서드파티(Apify 등)의 파싱 결과에 의존해야 함.medium

3.3 라이브 테스트 실측

대상방법과금결과비고
재검증(2026-07-23): https://www.instagram.com/reel/DZvu_BVzM5W/ — 이전 §3.3 절단 응답 재확인용Apify apify~instagram-scraper run-sync-get-dataset-items (동일 호출, 이번엔 응답 전체를 잘리지 않게 저장)ok — 32개 필드 전체 확인. 신규 발견: alt(Meta AI 생성 시각콘텐츠 설명), videoUrl/audioUrl/displayUrl(미디어 원본 URL), hashtags[]/mentions[](caption과 별개 파싱), firstComment/latestComments[](댓글 내용), isCommentsDisabled. 이전 실측 로그의 '<omitted-not-shown-in-truncated-keylist>' 자리가 이 필드들이었음url 필드가 /reel/이 아닌 /p/로 canonical화됨 — reel/post 동일 엔티티라는 §Q1 판단의 추가 실증. 총 32필드 중 InstagramFetcher가 파싱하는 건 여전히 0(post/reel 타입 아이템 기준)
redirect probe: https://www.instagram.com/dtfa/ (profile, template instagram.com/{value})curl -sI (id-probe)무료HTTP/2 200 — no redirect, live public profilerouter sourceKey='dtfa' (lc(segments[0])) matches actual account handle — canonical id confirmed
redirect probe: https://www.instagram.com/p/Bjnx75lFFQp/ (post, template instagram.com/{value}/{value})curl -sI (id-probe)무료HTTP/2 302 → location: https://www.instagram.com/cryptosharkk/router sourceKey='Bjnx75lFFQp' (shortcode, correct per its own rule) but the post itself is unreachable unauthenticated — IG redirects to an unrelated account page (cryptosharkk), not the post owner. Canonical id mismatch is a data-freshness issue, not a router bug: router still extracts the literal shortcode from the URL as designed.
redirect probe: https://www.instagram.com/reel/DZvu_BVzM5W/?utm_source=ig_embed&utm_campaign=loadingcurl -sI (id-probe)무료HTTP/2 200 — no redirectrouter sourceKey='DZvu_BVzM5W' (segments[1] under 'reel') — matches; utm query params correctly ignored by classify()
redirect probe: https://www.instagram.com// (template instagram.com/, sourceKey=null)curl -sI (id-probe)무료HTTP/2 201 (IG returns 201 for the bare/double-slash root, not an error)router correctly falls to sourceKey=null via lc(undefined); confirms this template carries no usable identity
redirect probe: https://www.instagram.com/stories/druski/3927059123933463192/curl -sI (id-probe)무료HTTP/2 200 — no redirect (page renders, though story content itself expires in ~24h on IG's side)router sourceKey='stories' (classify() has no /stories/<handle>/<id> case, falls through to lc(segments[0])='stories') — WRONG vs canonical id. See paid probe below: actor resolves the real account (druski) despite router mislabeling.
https://www.instagram.com/gamesradar/ (profile)Apify apify~instagram-scraper run-sync-get-dataset-items (resultsType=details, resultsLimit=1) — exact call InstagramFetcher.fetch() makesok — actor returned a full profile-type item (see schemas). Fetcher's InstagramItem parse would yield username='gamesradar', followersCount=70114, postsCount=5013, verified=false, biography set, externalUrl set — all 6 declared fields populated correctly for this URL shape.HTTP 201 from Apify run-sync-get-dataset-items. Confirms rank0/rank4 in acquisition: actor works fine for the fetcher's declared field set when input is a profile URL.
https://www.instagram.com/reel/DZvu_BVzM5W/ (post/reel — template instagram.com/{value}/{value}, count:170 in sample, 29.5% of all instagram URLs)Apify apify~instagram-scraper run-sync-get-dataset-items (same call as InstagramFetcher.fetch())ok at actor level, but DATA LOSS in current fetcher — actor returned a post-type item with entirely different top-level fields (shortCode, ownerUsername, likesCount, commentsCount, timestamp, videoViewCount, videoPlayCount, musicInfo). None of InstagramItem's 6 declared fields (username, followersCount, postsCount, verified, biography, externalUrl) exist on this item, so InstagramFetcher would set ALL of them to null via '?? null' despite the actor call succeeding and returning real, richer data.CONFIRMED LIVE BUG: profile-only InstagramItem schema silently drops all data for the ~30-45% of instagram.com URLs that are posts/reels (templates instagram.com/{value}/{value}, .../embed, .../DZ8NYQ8xnRp-style nested reel paths — 170+5+3=178/576=~31% of sample). fetcher still reports status='ok' with data={username:null,followersCount:null,postsCount:null,verified:null,biography:null,externalUrl:null} — indistinguishable from a genuinely empty/private profile at the data layer.
https://www.instagram.com/stories/druski/3927059123933463192/ (router sourceKey='stories', wrong)Apify apify~instagram-scraper run-sync-get-dataset-items (same call as InstagramFetcher.fetch())ok — actor ignored the story-specific path/id and returned the PROFILE of the account in segments[1] (druski): username='druski', followersCount=13384199, postsCount=510, verified=true, biography=''(empty string, not null), externalUrl='https://youtu.be/tH84nGEi-UI'. All 6 InstagramItem fields populate correctly here (because actor happens to resolve to a profile-shaped item), but the STORED sourceKey ('stories') does not match the actual account key ('druski') that the data describes.Router bug confirmed live: classify() has no case for host segments[0]==='stories', falls through to generic sourceKey=lc(segments[0])='stories'. Data fetched is correct (druski's profile) but keyed under the wrong join key ('stories' instead of 'druski'), which would collide with every other /stories/<anyone>/<id> URL in the dataset and break any downstream join/dedup on sourceKey.
https://www.instagram.com// (malformed, template instagram.com/, count:3)Apify apify~instagram-scraper run-sync-get-dataset-itemserror — actor rejected the input before running (HTTP 400, invalid-input), matching InstagramFetcher's catch-block → buildResult(routed,'error')Confirms malformed/empty-path instagram URLs (sourceKey=null) correctly surface as fetch status='error', not a silent success with null data — this path is safe.

재검증(2026-07-23): https://www.instagram.com/reel/DZvu_BVzM5W/ — 이전 §3.3 절단 응답 재확인용 실측 응답 원문:

{"alt":"Video by ぬいぐるみ神社(公式) on June 18, 2026. May be an image of bear and text...","videoUrl":"https://scontent-lga3-2.cdninstagram.com/...","audioUrl":"https://scontent-lga3-1.cdninstagram.com/...","displayUrl":"https://scontent-atl3-2.cdninstagram.com/...","hashtags":["ぬいぐるみ神社","美山町","誕生祭","かやぶき屋根","鳥居"],"mentions":[],"firstComment":"I will visit with my favorite plush stitch❤️","latestComments":[{"id":"18120249850701700","text":"I will visit with my favorite plush stitch..."}],"isCommentsDisabled":false,"url":"https://www.instagram.com/p/DZvu_BVzM5W/","dimensionsHeight":1333,"dimensionsWidth":750}

redirect probe: https://www.instagram.com/dtfa/ (profile, template instagram.com/{value}) 실측 응답 원문:

HTTP/2 200 
vary: Accept-Encoding
set-cookie: csrftoken=...

redirect probe: https://www.instagram.com/p/Bjnx75lFFQp/ (post, template instagram.com/{value}/{value}) 실측 응답 원문:

HTTP/2 302 
location: https://www.instagram.com/cryptosharkk/
reporting-endpoints: ...

redirect probe: https://www.instagram.com/reel/DZvu_BVzM5W/?utm_source=ig_embed&utm_campaign=loading 실측 응답 원문:

HTTP/2 200 
vary: Accept-Encoding
set-cookie: csrftoken=...

redirect probe: https://www.instagram.com// (template instagram.com/, sourceKey=null) 실측 응답 원문:

HTTP/2 201 
content-type: text/html; charset=utf-8

redirect probe: https://www.instagram.com/stories/druski/3927059123933463192/ 실측 응답 원문:

HTTP/2 200 
vary: Accept-Encoding
set-cookie: csrftoken=...

https://www.instagram.com/gamesradar/ (profile) 실측 응답 원문:

{"inputUrl":"https://www.instagram.com/gamesradar/","id":"16166573","username":"gamesradar","fullName":"GamesRadar+","biography":"Upgrade your downtime...","followersCount":70114,"followsCount":69,"isBusinessAccount":true,"private":false,"verified":false,"postsCount":5013,"latestPosts":[{...11 items...}],"fbid":"17841401157190252"}

https://www.instagram.com/reel/DZvu_BVzM5W/ (post/reel — template instagram.com/{value}/{value}, count:170 in sample, 29.5% of all instagram URLs) 실측 응답 원문:

{"inputUrl":"https://www.instagram.com/reel/DZvu_BVzM5W/","id":"3922560441369022038","type":"Video","shortCode":"DZvu_BVzM5W","caption":"...","commentsCount":33,"likesCount":<omitted-not-shown-in-truncated-keylist>,"timestamp":<not-shown>,"ownerFullName":..,"ownerUsername":..,"ownerId":..,"productType":..,"videoDuration":..,"musicInfo":..,"videoPlayCount":..}

https://www.instagram.com/stories/druski/3927059123933463192/ (router sourceKey='stories', wrong) 실측 응답 원문:

{"inputUrl":"https://www.instagram.com/stories/druski/3927059123933463192/","id":"247039745","username":"druski","fullName":"DRUSKI","biography":"","followersCount":13384199,"verified":true,"postsCount":510,"externalUrl":"https://youtu.be/tH84nGEi-UI", "latestPosts":[...]}

https://www.instagram.com// (malformed, template instagram.com/, count:3) 실측 응답 원문:

{"error":{"type":"invalid-input","message":"Input is not valid: Values in input.directUrls at positions [0] must match regular expression \"^(https:\\/\\/)?(www\\.)?instagram\\.com\\/[A-Za-z0-9._-]+(\\/.*)?\$\""}}
src/common/social-fetcher/fetchers/instagram.fetcher.ts: InstagramItem interface only declares profile-shaped fields (username/followersCount/postsCount/verified/biography/externalUrl). Live Apify call confirms that for post/reel URLs (/p, /reel, /tv — templates covering ~170+5+3=178 of 576 sample URLs, ~31%) the actor returns a completely different post-shaped schema (shortCode/ownerUsername/likesCount/commentsCount/timestamp/...) with none of the 6 declared fields present. Result: fetch status='ok' but all 6 data fields are null — indistinguishable at the data layer from a genuinely empty/private account. This silently discards real, richer data the actor already returns (post author, post date, engagement counts) that InstagramItem simply never reads.
src/common/social-fetcher/social-fetcher.router.ts classify(): the instagram branch only special-cases segments[0]∈{p,reel,tv}; any other reserved/system path (stories, explore, accounts, direct, ...) falls through to the generic sourceKey=lc(segments[0]) branch, producing a wrong join key. Live-confirmed: /stories/druski/3927059123933463192/ → sourceKey='stories' instead of 'druski', even though the Apify actor itself correctly resolves and returns druski's profile data. Any two different accounts' /stories/ URLs would collide under the same sourceKey='stories', breaking dedup/joins keyed on sourceKey.
Same router gap extends to /explore/search/keyword/?q=... (sample template count:1) → sourceKey='explore', a system search page misclassified as a content-owner key. Not live-tested via paid call (would be meaningless to fetch), but confirmed by code reading; same root cause as the 'stories' finding — instagram's classify() lacks an X_RESERVED_ROOTS-style guard that x.com's classifyX() already has.
Observed but not a code defect: /p/<shortcode>/ URLs can 302-redirect to an unrelated account page when the specific post is deleted/private/unreachable unauthenticated (curl -sI on https://www.instagram.com/p/Bjnx75lFFQp/ → redirects to https://www.instagram.com/cryptosharkk/). Router still extracts the shortcode as designed; flagging as a data-freshness caveat for anyone treating fetched post data as guaranteed to belong to the URL's literal shortcode.

4. 객체별 필드 통합 (변동성 · 파싱상태 · 트레이딩 유용성) 👤 판단 반영

Creator/Content 객체별로 필드를 한 표에 통합. 값 변동성 = 값이 시간에 따라 변하나(immutable 역사적 사실 / mutable 드리프트 / derivable 계산값) → look-ahead 안전성이 여기서 도출(immutable=safe · mutable=conditional/as-of · derivable=unsafe). 파싱 상태 = 현재 파이프라인이 실제로 저장하나.

4.1 · Creator (프로필) — instagram (profile)

source: Apify profile item (prodiger/instagram-scraper 잠정 · 현행 apify~instagram-scraper 동일 스키마) · mutable 값은 전부 actor scrapedAt 시점 스냅샷 — 진입시점 값과 다를 수 있어 as-of 주의. immutable은 나중 관측해도 그 시점 값과 동일(look-ahead safe)
필드설명값 변동성파싱 상태(현재)최종 결정트레이딩 유용성
id (숫자 계정 id)계정 불변 숫자 id. 예: '16166573' — content의 ownerId와 매칭되는 canonical 조인 키. profile fetch 응답에 존재(추가 쿼리 불필요)immutable⚠️ actor제공·미파싱🔑 key조인 키 — 리네임돼도 분기 없음. 현재 버려지므로 캡처 필요
username계정 핸들. 예: 'gamesradar' — 표시속성 + router sourceKey(URL 유래). 미fetch 엔트리는 id 없어 handle 폴백 키immutable✅ 캡처keepcontext — 사칭/원본 대조 anchor + id 없을 때 폴백 조인
followersCount팔로워 수. 예: 70114mutable✅ 캡처keepcontext — 도달·팀규모 보조(as-of 주의)
postsCount총 게시물 수. 예: 5013mutable✅ 캡처keepcontext — 활동량
biography소개글('' 가능, null과 구분). 예: 'Upgrade your downtime...'mutable✅ 캡처keepcontext — 내러티브/사칭/링크 tell
externalUrls[]외부링크 전체 배열(title/url/type). externalUrl(단일)을 대체 — 배열만 저장하면 [0]으로 첫 링크 파생 가능, 손실 없음mutable⚠️ actor제공·미파싱keepactionable — 다중 링크 중 스캠 도메인 탐지(첫 개만 보면 놓침)
externalUrl프로필 외부링크(첫 번째만 flatten) — externalUrls[]로 대체되므로 단일 필드는 제거mutable✅ 캡처removeactionable — 단 externalUrls[][0]으로 파생, 별도 저장 불필요
verified인증배지. 예: falsemutable✅ 캡처removenoise — 구매 가능, 신뢰 신호 아님
fbidFacebook 연동 idimmutable⚠️ actor제공·미파싱removenoise — id(계정)로 충분, 불필요
private비공개 계정 여부. 예: falsemutable⚠️ actor제공·미파싱skipcontext — fetch 가능성/투명성(미채택)
isBusinessAccount / businessCategoryName비즈니스 계정 여부 + 카테고리. 예: true / 'Comedian'mutable⚠️ actor제공·미파싱skipcontext — 개인 vs 브랜드(미채택)
fullName표시명(핸들과 별개). 예: 'GamesRadar+'mutable⚠️ actor제공·미파싱skipcontext — 표시명 대조/사칭(미채택)
followsCount팔로잉 수. 예: 69mutable⚠️ actor제공·미파싱skipnoise
latestPosts[]프로필 fetch가 함께 반환하는 최근 ~12개 게시물(각 shortCode/likesCount/commentsCount/timestamp) — 별도 post fetch 없이 Content engagement 확보 경로mutable⚠️ actor제공·미파싱skipactionable(잠재) — 이번 keep셋 미포함, 향후 재검토
createdAt (계정 생성일)계정 개설 시각 — 신규/급조 계정 판별의 핵심 러그 tell. ⚠ IG는 어떤 source도 미제공 → 확보 불가(X 대비 러그 신호 구조적 약점)immutable❌ 없음actionable(이론상)이나 IG 확보 불가

4.2 · Content (게시물/릴스) — instagram_post (분리 제안 · 현재 미분리, 파싱 추가 필요)

source: Apify post item (Creator와 동일 actor, URL 종류로 자동 판별) — 단 현재 InstagramFetcher가 프로필 필드만 파싱해 아래 전부 미저장 · engagement(mutable)은 scrapedAt 스냅샷(as-of 주의). timestamp·shortCode 등 immutable은 look-ahead safe. 현재 fetcher는 이 객체 필드를 하나도 안 읽어 게시물/릴스 URL은 6필드 전부 null. 2026-07-23 재검증: 이전 §3.3 실측 로그는 응답이 절단(truncated)돼 alt/videoUrl/audioUrl/hashtags/firstComment 등을 놓쳤음 — 전체 미절단 응답(32필드)으로 재확인, 아래 목록 갱신
필드설명값 변동성파싱 상태(현재)최종 결정트레이딩 유용성
ownerId (숫자 creator id)작성자 계정 숫자 id. 예: 75036941532 — creator(profile.id)와 매칭되는 조인 키. content 응답에 존재(추가 쿼리 불필요)immutable⚠️ actor제공·미파싱🔑 key조인 키 — content→creator 연결의 canonical. 현재 버려지므로 캡처 필요
shortCode게시물 코드(URL과 일치, 콘텐츠 자체 고유 id). 예: 'DZvu_BVzM5W'immutable⚠️ actor제공·미파싱🔑 key콘텐츠 식별 키 — 카피캣/원본 대조
ownerUsername작성자 핸들. 예: 'druski' — 표시속성(조인은 ownerId)immutable⚠️ actor제공·미파싱keepcontext — 누가 밀었나 + id 없을 때 폴백
timestamp게시 시각(ISO). 예: '2026-06-27T...'immutable⚠️ actor제공·미파싱keepactionable — 신선도/신규성(카피캣 원칙)
caption본문 텍스트 — 단순 본문이 아니라 $티커/CA가 담기는 token↔content 링크 지점immutable⚠️ actor제공·미파싱keepactionable — 티커/CA 언급 = 토큰 연결 + 내러티브
altMeta AI 생성 시각 콘텐츠 설명. 예: 'Video by ... May be an image of bear and text...' — 2026-07-23 재검증 신규 확인immutable⚠️ actor제공·미파싱keepactionable — OCR/vision 없이 이미지·영상 내용 텍스트화(사칭·재사용 탐지). 단 AI생성이라 품질 편차
hashtags[] / mentions[]caption에서 파싱된 해시태그/멘션 배열(별개 구조화 필드)immutable⚠️ actor제공·미파싱keepcontext — 티커/캠페인 태그·@프로젝트 추적
likesCount좋아요 수. 예: 197mutable⚠️ actor제공·미파싱keepactionable — 참여 모멘텀(as-of)
commentsCount댓글 수. 예: 33mutable⚠️ actor제공·미파싱keepactionable — 참여 강도
videoViewCount / videoPlayCount릴스 조회/재생수. 예: 14330558mutable⚠️ actor제공·미파싱keepactionable — 바이럴 확산 속도(릴스 핵심)
type / productTypeImage/Video/Sidecar · 'clips'(릴스)/'feed'/'carousel_container' — 병합된 instagram_post 안에서 형식(피드/릴스/캐러셀) 구분자immutable⚠️ actor제공·미파싱keep형식 구분자 — post/reel 병합 결정상 필수(제거 시 구분 불가)
coauthorProducers공동작성자(collab 게시물)immutable⚠️ actor제공·미파싱removecontext — 저가치(audit 명시 제거)
taggedUsers태그된 계정 목록immutable⚠️ actor제공·미파싱skipcontext — collab/제휴(미채택)
id (media 숫자)미디어 숫자 id — shortCode가 콘텐츠 키라 중복immutable⚠️ actor제공·미파싱skipnoise — shortCode로 충분
videoDuration영상 길이(초)immutable⚠️ actor제공·미파싱skipnoise
childPosts캐러셀 하위 미디어 배열immutable⚠️ actor제공·미파싱skipnoise — 구조
musicInfo사용 음원 정보immutable⚠️ actor제공·미파싱skipnoise
videoUrl / audioUrl / displayUrl영상/음원/썸네일 원본 파일 URL. 2026-07-23 재검증 확인immutable⚠️ actor제공·미파싱skipcontext — 자체 vision/audio 분석 입력(후속, 지금 미채택)
firstComment / latestComments[]첫 댓글/최근 댓글 목록(내용). commentsCount와 별개mutable⚠️ actor제공·미파싱skipactionable(잠재) — mutable·부피 커서 이번 제외
isCommentsDisabled댓글 잠금 여부mutable⚠️ actor제공·미파싱skipcontext — 약한 러그tell(미채택)
dimensionsHeight/Width, originalHeight/Width, images[]미디어 크기·캐러셀 하위 이미지 배열immutable⚠️ actor제공·미파싱skipnoise — 구조 메타
(engagement rate)likes/followers 등 우리가 계산할 파생값derivable❌ 없음skipnoise — 저장 말고 raw 저장 후 재계산(look-ahead 함정)

4-부록. actor raw 응답 스키마 (ground truth)

위 통합 표의 원천 — actor가 실제 반환하는 원본 필드 전체.

dataType: "router: SocialFetcherRouter.classify() output for host∈{instagram.com, instagr.am}"

필드예시값설명
sourceTypeinstagramalways 'instagram' — single type regardless of URL shape (profile/post/reel/story all collapse to one)
sourceKey'Bjnx75lFFQp' (post) | 'dtfa' (profile) | 'stories' (WRONG for /stories/druski/<id>/, should be 'druski')if segments[0]∈{p,reel,tv} and segments[1] exists → segments[1] verbatim (post shortcode, case preserved). Else → lc(segments[0]) (handle, lowercased). No case for /stories/, /explore/, /accounts/ etc. — these fall into the generic handle branch and produce a wrong key (e.g. 'stories', 'explore').

dataType: "fetcher output: InstagramFetcher.fetch() → buildResult data (what actually gets persisted)"

필드예시값설명
username'gamesradar' | null (for post/reel URLs)item.username ?? null — fixed valueNature. Only present when Apify item happens to be profile-shaped.
followersCount70114 | nullitem.followersCount ?? null — mutable. Same profile-shaped dependency as username.
postsCount5013 | nullitem.postsCount ?? null — mutable.
verifiedfalse | nullitem.verified ?? null — mutable.
biography'Upgrade your downtime...' | '' | nullitem.biography ?? null — mutable. Can legitimately be '' (empty string) for accounts with no bio, distinct from null.
externalUrl'https://likeshop.me/gamesradar' | nullitem.externalUrl ?? null — mutable. Only first external link; externalUrls[] array (multiple links) is dropped entirely.

dataType: "apify raw response — profile-type item (directUrls = instagram.com/{handle}/ or actor resolves to a profile, e.g. /stories/{handle}/{id}/)"

필드예시값설명
id'16166573'numeric account id (string)
username'gamesradar'handle
fullName'GamesRadar+'display name
biography'Upgrade your downtime...'bio text, can be ''
externalUrls[{"url":"https://likeshop.me/gamesradar",...}]array of link objects (title/url/link_type) — actor gives multiple, fetcher only keeps externalUrl (first)
externalUrl'https://likeshop.me/gamesradar'first external link, flattened
followersCount70114int
followsCount69int — NOT captured by fetcher
isBusinessAccounttruebool — NOT captured by fetcher
businessCategoryName'Comedian'string — NOT captured
privatefalsebool — NOT captured (arguably important for filtering)
verifiedfalsebool
postsCount5013int
latestPosts[{shortCode:'Da869cNESYa', likesCount:197, commentsCount:1, timestamp:'2026-07-19T01:00:53.000Z', ...}]array of ~12 most recent posts (id/shortCode/caption/likesCount/commentsCount/timestamp/ownerUsername/...) — entirely dropped by fetcher; no account creation date field exists anywhere in the actor response, confirming acquisition's claim.
fbid'17841401157190252'Facebook-linked id — NOT captured

dataType: "apify raw response — post/reel-type item (directUrls = instagram.com/p|reel|tv/{shortcode}/)"

필드예시값설명
id'3922560441369022038'numeric media id — NOT profile id
type'Video''Video' | 'Image' | 'Sidecar'
shortCode'DZvu_BVzM5W'post shortcode, matches URL
caption(Japanese caption text)post text/hashtags
commentsCount33int
likesCountnon-null on top-level itemint (can be null for some carousel child items)
timestamp'2026-06-27T...' styleISO post datetime
ownerUsername'gamesradar' | 'druski' | 'bet' (tagged co-post)the account that POSTED this content — semantically what a 'post-author handle' field should be, but has no home in InstagramItem
ownerId'16166573'poster's numeric id
productType'clips''clips' etc.
videoViewCount / videoPlayCount14330558engagement metrics unique to video posts
musicInfo, taggedUsers, coauthorProducers, childPosts[...]rich structural fields for Reels/Sidecar posts — entirely unused
NONE of: username, followersCount, postsCount, verified, biography, externalUrln/a — confirms silent data lossthe 6 fields InstagramFetcher's InstagramItem interface expects are simply absent on post-type items — every parsed field resolves to null via '?? null'

다음 액션