Social Fetcher URL Structure Audit — 2026-07-17
관련 코드: social-fetcher.router.ts(classifyReddit) · fetchers/reddit.fetcher.ts
관련 데이터: samples/reddit.json · samples/unknown.json(커버리지 갭 후보)
✅ 최종 결정 (수기 audit 반영) 원칙: sourceKey는 unique & immutable & linkable. Reddit은 username과 subreddit name이 영구 불변이고 삭제해도 재사용되지 않아(재할당 없음) 이름 자체를 키로 써도 안전 — TikTok(30일마다 변경+타인 재할당)·X(변경 가능)와 정반대. 따라서 audit의 username/name 키 채택은 원칙 위반이 아니라 플랫폼 특성에 맞는 판단.
조인 키
객체
키
비고
Creator (user)
username
Reddit은 username 변경 불가(대소문자조차 불가). 계정 삭제 시에도 해당 username은 재사용 불가 → 충돌 없음. audit "username == authorName → content와 연결" 그대로 성립 — post 응답의 authorName이 곧 username이라 추가 조회 없이 링크됨.
Content (post)
parsedId + authorName (creator 링크)
parsedId(base36 post id)가 dedup 키. venue의 id/parsedId는 audit이 제거 지시했으나 content의 parsedId는 키라 제거 대상이 아님 — 객체가 달라 같은 필드명이 다르게 처리됨.
Venue (subreddit)
name (subreddit slug)
subreddit 이름은 생성 후 변경 불가 → 키로 안전. audit "name (id 안써도 됨)" 채택, id/parsedId(t5_2th52 / 2th52)는 제거.
sourceType 통합 맵
최종 타입
포함 패턴
reddit
/r/{sub}, /r/{sub}/search, /r/{sub}/new — key=subreddit(소문자). 기존 제안 유지
reddit_post
/r/{sub}/comments/{postId}/{slug}, redd.it/{postId} — key=postId. redd.it은 차단되지 않음(301 정상)이고 URL에 이미 postId가 있어 해석 불필요
/user/{name}, /u/{name} — key=username. 현재 fetcher가 URL 정규식 sniffing으로 분기하던 것을 타입으로 분리. 기존 제안 유지
구조 결정 — 필드 단위 key/keep/remove로 표현할 수 없는 결정
항목
내용
share 링크는 해석하지 않고 shareId 유지 — Reddit이 우리 IP를 전면 차단
audit: "만약 share가 기본적으로 post에 대한 share라면 type post로 설정하고 key는 post id로. 안된다면 어쩔 수 없이 shareId로". 실측 결과 "안된다": /r/{sub}/s/{shareId} HEAD = 403인데, /r/wallstreetbets/(일반 subreddit)·/r/vinted/comments/{id}/(일반 post)·old.reddit.com전부 403 → share 전용 문제가 아니라 Reddit이 우리 인프라 IP를 전면 차단(문서 §3의 about.json 100% 403과 동일 원인). TikTok shortlink처럼 무료 HEAD로 푸는 길이 막혀 있음. 비대칭 발견: redd.it/{postId}는 차단되지 않고 301 정상 응답(→ reddit.com/comments/{postId}). 다만 redd.it은 URL에 이미 postId가 있어 리다이렉트가 애초에 불필요. → share 해석은 유료 Apify actor 경유만 남으며, 그 전까지 reddit_share+shareId 유지. 후속 과제.
stickied / distinguished — 무엇인지 확인 후 audit 지시대로 제거
audit이 "이게 뭔지 체크하고 제거"로 유보한 항목. stickied = 모더레이터가 subreddit 상단에 고정한 글(공지·메가스레드). distinguished = 작성자가 모더레이터/관리자 자격으로 작성했음을 표시하는 태그. 둘 다 "공식성"의 약한 보조 신호이고(문서 판정: context), 토큰 홍보 글이 고정돼 있으면 "운영진 관여"로 해석할 여지는 있으나 단독 매매 신호로는 약함 → audit 지시대로 remove. postType/isSelf/isVideo도 함께 제거(문서 판정 noise와 일치).
이 문서에서 쓰는 pill 범례
관측 source-data에서 실제 발견됨웹리서치/실측 curl 또는 검색으로 보완(이 데이터셋엔 미관측)정상 fetch 성공/의도대로 동작차단·버그·갭 실패했거나 고쳐야 할 것redirect/mutable유지 / 신규 타입fixed / mutable / 미선언
✅ 신규 타입 확정. redd.it/{postId}도 여기 편입 — 실측상 redd.it은 차단되지 않고(301 정상) URL에 이미 postId가 있어 리다이렉트 해석도 불필요.
reddit_share
신규
/r/{sub}/s/{shareId}
shareId(raw)
router는 순수함수 유지(네트워크 없이 shareId만 추출) — 진짜 postId는 fetcher가 redirect resolve로 확보
✅ key=shareId 유지 확정. audit 조건문("post에 대한 share면 post로, 안되면 shareId로")이 실측상 "안된다"로 판정 — /s/ 뿐 아니라 일반 subreddit·post·old.reddit 전부 403이라 Reddit이 우리 IP를 전면 차단(§3 about.json 403과 동일 원인). 유료 Apify 경유 해석은 후속 과제.
reddit_user
신규
/user/{name}, /u/{name}
username(소문자)
지금은 fetcher가 URL 문자열을 정규식으로 sniffing해서 분기 — X/Telegram처럼 타입 자체로 분리
✅ 신규 타입 확정. key=username — Reddit username은 변경 불가라 그대로 canonical 조인키.
커버리지 갭(이번 스코프 밖, 후속 이슈로만 기록) — old.reddit.com / new.reddit.com / np.reddit.com / m.reddit.com은 classify()의 host 체크가 host === 'reddit.com'만 봐서 전부 website로 오분류됨. redd.it은 host 체크 대상이라 reddit으로는 분류되지만, 위 4-way 분리 전까지는 postId를 subreddit으로 착각해서 /r/{postId}/about.json을 호출하는 버그 상태.
3. Fetch 테스트 + 에러 원인
3.1 실측 (curl, RedditFetcher와 동일 엔드포인트/헤더)
GET https://www.reddit.com/r/smallstreetbets/about.json (UA: social-fetcher/1.0)
→ HTTP 403, content-type: text/html (JSON 아님 — 차단 페이지)
GET https://www.reddit.com/user/ElegantCombination43/about.json (UA: social-fetcher/1.0)
→ HTTP 403, content-type: text/html
GET https://www.reddit.com/r/smallstreetbets/about.json (UA: 브라우저 Chrome UA로 교체)
→ HTTP 403, content-type: text/html (UA 문제 아님을 확인)
GET https://old.reddit.com/r/smallstreetbets/about.json (UA: 브라우저)
→ HTTP 403, content-type: text/html (old.reddit.com도 동일 차단)
GET https://redd.it/eorhm
→ HTTP 301 → https://www.reddit.com/comments/eorhm (리다이렉트 자체는 차단 안 됨)
GET https://www.reddit.com/r/TheMatpatEffect/s/RT8o3wyoRD
→ HTTP 301 → .../comments/1u3vkxd/the_original_hopium_needle_guy_image/?share_id=... (리다이렉트 정상)
GET https://www.reddit.com/r/TheMatpatEffect/s/RT8o3wyoRD.json
→ HTTP 307 → https://www.reddit.com/r/TheMatpatEffect/ (post 데이터 없이 subreddit root로 튕김 — .json 트릭이 공유링크엔 안 먹음)
3.2 원인 분석
about.json 계열은 100% 403 — fetched.reddit[](161건) 전량 status: 'error'와 정확히 일치. User-Agent를 바꿔도, old.reddit.com으로 바꿔도 동일하게 차단됨 → UA/파싱 문제가 아니라 이 환경(서버 egress IP)에 대한 Reddit의 봇 차단(WAF/anti-scraping). 다만 이 403은 비인증 www.reddit.com 도메인 대상이라는 점이 중요 — 인증된 oauth.reddit.com API 서버까지 막는지는 별개 문제(§3.3 참고, 미확인).
redirect(301/307)는 차단되지 않음 — 실제 데이터 본문을 반환하는 요청만 막히고, 헤더만 오가는 redirect resolve는 통과됨. 즉 reddit_share의 postId 확보(§2)는 가능하지만, 확보한 postId로 다시 .json을 요청하면 위와 동일하게 막힐 가능성이 높음(post permalink .json은 별도 실측 필요).
data: null로 저장돼 있어 지금까지는 원인 불명이었음 — RedditFetcher가 catch한 에러를 로그(logger.warn)에만 남기고 SocialFetchResult.data엔 아무것도 안 남기기 때문에, source-data 스냅샷만 봐서는 403인지 timeout인지 파싱 실패인지 구분이 안 됐음. 이번 실측으로 원인이 확정됨.
3.3 해결 옵션 (결정 필요, 이번 audit에서 바로 고르지 않음)
공식 API 상태 업데이트(웹 리서치, 2026-07-17) — Reddit이 2025년 말 "Responsible Builder Policy"를 도입하면서 reddit.com/prefs/apps의 self-service 앱 등록(즉시 무료 발급)이 닫혔다. 이제는 승인 신청을 거쳐야 client_id/secret이 나오는 구조로 바뀐 것으로 보인다. OAuth2 메커니즘 자체(client_credentials grant, 로그인 유저 없이 서버가 바로 토큰 발급)와 무료 티어(비상업 용도 OAuth 클라이언트당 분당 100건)는 여전히 존재해서 우리 볼륨(하루 수백~수천 건)엔 충분하지만, 등록이 가능한지 자체가 게이트가 됐다. 신청 흐름 상세는 §3.4.
확신도 — 정책 존재: 중간(1차 문서 직접 확인 못 함, 2차 출처 다수 일치) · 인증된 oauth.reddit.com까지 WAF가 막는지: 미확인(우리가 실측한 403은 비인증 도메인 대상)
옵션
설명
트레이드오프
A. Reddit 공식 OAuth API
script 타입 앱 등록 → client_id/secret으로 인증된 요청. 단, 신청/승인 절차 필요(§3.4)
승인만 나면 credential 관리 정도로 끝, rate limit(100 QPM) 내에서 안정적. 승인 자체가 불확실(거절/무응답 흔하다는 2차 보고)
A-1. Devvit
조사 결과 부적합 — 기각 Reddit이 "Devvit으로 안 되면 수동신청" 순서로 안내하길래 확인했으나, Devvit은 특정 subreddit에 설치되어 Reddit 쪽 이벤트로 실행되는 인터랙티브 앱 플랫폼 — 우리처럼 "외부 이벤트로 임의 subreddit을 온디맨드 조회"하는 구조 자체를 지원 안 함. 토큰도 Devvit 내부에서만 쓰이고 외부로 노출되지 않음. 게다가 Devvit을 거쳐도 결국 A와 동일한 Responsible Builder Policy 승인 게이트를 만남
사용 불가로 결론 — 더 조사 불필요
B. 프록시/주거용 IP 사용
요청을 프록시 경유로 우회(비인증 www.reddit.com 경로 유지)
비용 발생, 별도 인프라, 승인 절차 없이 바로 시도 가능
C. Known limitation으로 문서화, 방치
reddit 신호는 fetch 불가 상태 유지, errors[]로만 계속 기록
구현 비용 0, 대신 reddit 관련 signal(subscriber growth 등)은 영구히 확보 불가
D. Apify (harshmaur~reddit-scraper)
채택 — 실측 완료, 4개 타입 전부 성공(§3.5) 기존 tiktok/instagram과 동일한 ApifyClient.runActorSync() 패턴 재사용 가능
비용 발생(건당 ~$0.022, pay-per-event), 승인 절차 없이 바로 사용 가능. subreddit/post/user는 이 actor로 직접, share만 우리 쪽 redirect resolve 한 스텝 추가 — §3.5
2026-07-17 결정: A(공식 API 승인)는 대기시간이 불확실해서 D(Apify)로 진행. 1차로 시도한 actor(trudax~reddit-scraper-lite)는 subreddit/user에서 반복 실패해 harshmaur~reddit-scraper로 교체, 4개 타입 전부 실측 성공(§3.5). A는 병행 신청 후보로 남겨둠(§3.4).
3.4 공식 API 신청 흐름 (Responsible Builder Policy 이후)
이 섹션 전체의 확신도: 낮음~중간. 조사 에이전트도 reddit.com / support.reddithelp.com 직접 fetch가 403으로 막혀서 1차 공식문서를 확인 못 했다. 아래는 2차 출처(블로그·커뮤니티 요약) 교차 인용 기반이고, 그중 상세도가 가장 높은 출처 2곳(redditapis.com, molehill.io)은 유료 프록시/대체 API를 파는 벤더라 "공식 절차가 어렵다"고 서술할 금전적 유인이 있다 — 사실관계는 액면 그대로 받아들이지 말 것. 프로덕션 결정 전에 reddit.com/prefs/apps에 직접 신청해서 ground truth를 확인하는 걸 권장한다.
단계
내용
확신도
1. 시작 지점
reddit.com/prefs/apps에서 앱 생성(이름·타입=script·redirect URI)은 예전과 동일. 다만 이제 이것만으론 토큰이 안 나오고, 별도 승인 절차(Developer Support 티켓/폼)를 통과해야 실제 client_id/secret이 활성화되는 구조로 보임
중간
2. 신청 시 요구 정보
use-case 설명(무슨 데이터를 읽는지, 쓰기 작업 여부) · 예상 호출량 · 대상 subreddit/범위 · 데이터 보존/삭제 정책(Reddit이 48시간 삭제 요구를 강제한다는 보고 — 미확인, §하단 참고) · 살아있는 privacy policy 링크 · 경우에 따라 아키텍처 다이어그램/영상
낮음~중간
3. 신청자 카테고리
Developer(비상업) · Researcher(별도 r/reddit4researchers 트랙, 기관 윤리승인 필요) · Moderator(본인이 운영하는 subreddit, 특히 10만+ 구독자 sub — 승인율 가장 높다고 보고됨) · Commercial(별도 파트너 트랙, 가장 까다로움)
중간
4. 우리 케이스가 어디에 속하는지
크립토 트레이딩 회사의 내부 백엔드, 저볼륨 읽기전용 메타데이터 조회 — 공개 서비스도 아니고 Reddit 데이터를 상업적으로 재판매하지도 않지만, 회사 소속 상업적 활동이라 "Developer"에도 "Commercial"에도 깔끔하게 안 들어맞음. 2차 출처들이 공통으로 지적하는 "애매한 케이스가 거절/무응답으로 이어지는" 전형적 패턴
낮음(추정)
5. 소요기간·승인율
공식 SLA 확인 안 됨(한 출처는 "7일 목표"라 주장, 다른 출처는 "SLA 없음, 수 주~무기한"). 개인/소규모·설명 모호한 케이스가 가장 많이 거절되거나 무응답이라는 보고 다수
낮음
6. 승인되면
표준 OAuth2 client_credentials flow 그대로 사용(§3.3 옵션 A와 동일), 무료 티어 분당 100건, User-Agent 포맷 여전히 요구. 별도 SDK 강제나 호스팅 제약은 보고 안 됨
중간
7. Grandfathering
정책 시행 이전에 이미 발급된 credential은 계속 동작한다는 보고 다수(예: Apollo-Reborn 프로젝트가 옛 앱 credential을 재사용). 신규 앱 등록은 계정에 예전 승인 이력이 있어도 다시 승인 게이트를 거쳐야 함
중간
체크할 것: 회사 내에(다른 팀 포함) 2025년 말 이전에 등록된 Reddit API credential이 이미 있는지 — 있다면 grandfathering으로 바로 쓸 수 있어 승인 절차를 건너뛸 가능성.
3.5 Apify 실측 테스트 결과
1차로 시도한 trudax~reddit-scraper-lite는 post/share만 되고 subreddit/user가 반복 실패해서(아래 3.5.1), 2차로 harshmaur~reddit-scraper를 테스트한 결과 4개 타입 전부 커버 가능함을 확인했다(3.5.2). 최종 채택은 harshmaur.
3.5.1 1차 시도 — trudax~reddit-scraper-lite (post/share만 성공)
조회 대상
결과
비고
Post
성공 (1/1)
title/username/createdAt 정확
Share(/s/{shareId})
성공 (1/1)
actor가 redirect를 내부적으로 resolve — 응답의 parsedId가 우리가 curl로 수동 확인한 진짜 postId(1u3vkxd)와 정확히 일치
Subreddit(community)
전부 실패 (0/3, TIMED-OUT)
서로 다른 subreddit 2개, 총 3회 시도 전량 실패
User
실패 (TIMED-OUT)
3.5.2 2차 시도 — harshmaur~reddit-scraper (채택)
30일 통계 SUCCEEDED 143,805 / TOTAL 149,079 ≈ 96.5%(trudax 68%보다 훨씬 안정적), 4.9★/17리뷰, 오늘(2026-07-17) 빌드 갱신. 가격은 PAY_PER_EVENT — actor 시작 $0.02 + 저장된 결과 1건당 $0.002(공식 API로 직접 확인, 확신도 높음). 결과 1건짜리 단건 조회 기준 건당 약 $0.022.
조회 대상
입력
결과
받은 데이터
Subreddit(reddit)
startUrls + maxPostsCount: 0
성공dataType: "community" 1건
title/publicDescription/membersCount/createdAt/nsfw — §4에서 필요하다고 정리한 5개 필드 전부
Post(reddit_post)
startUrls
성공dataType: "post" 1건
title/authorName/createdAt + 보너스로 score/upVotes/subredditSubscribers/commentsCount 등 다수
Share(reddit_share)
startUrls에 /s/{shareId}를 그대로 줌
실패 — 0건 반환 (에러도 없이 그냥 빈 결과)
이 actor는 share 링크를 못 품 — 아래 우회 필요
Share, 우리가 redirect 먼저 resolve
plain HTTP로 301 Location 확보(§3.2에서 이미 확인한 방식, Apify 비용 없음) → 그 canonical post URL을 startUrls로
createdAt + totalKarma/linkKarma/commentKarma/verified 등. maxPostsCount만으론 댓글이 안 걸러져서(기본 10건) maxCommentsCount: 0도 같이 필요했음
최종 아키텍처 — actor는 harshmaur~reddit-scraper 하나로 통일. reddit_share만 예외적으로 우리 쪽에서 plain HTTP 301 redirect를 먼저 resolve(Apify 호출 아님, 무료)해서 진짜 post URL로 바꾼 뒤 나머지 3개 타입과 동일하게 처리. router는 여전히 순수함수 유지(redirect resolve는 router가 아니라 fetcher 책임 — §2 원칙과 일치).
// Subreddit 실측 응답 (community, 2026-07-17)
{
"name": "wallstreetbets", "title": "wallstreetbets",
"publicDescription": "Like 4chan found a Bloomberg Terminal.",
"membersCount": 20159454,
"createdAt": "2012-01-31T21:26:07.000Z",
"nsfw": false, "dataType": "community"
}
// Post 실측 응답 (share 링크 resolve 후 동일 경로로 조회, 2026-07-17)
{
"parsedId": "1u3vkxd", "title": "The Original \"Hopium\" Needle Guy Image",
"authorName": "Rytheredditguy", "createdAt": "2026-06-12T13:39:54.000Z",
"parsedCommunityName": "TheMatpatEffect", "dataType": "post"
}
// User 실측 응답 (user_profile, 2026-07-17)
{
"username": "hell0k1ttycum", "totalKarma": 3790,
"linkKarma": 2280, "commentKarma": 1510, "verified": true,
"createdAt": "2023-03-13T08:58:18.000Z", "dataType": "user_profile"
}
4. 객체별 필드 통합 (변동성 · 파싱상태 · 트레이딩 유용성) 👤 판단 반영
Creator/Content 객체별로 필드를 한 표에 통합. 값 변동성 = 값이 시간에 따라 변하나(immutable 역사적 사실 / mutable 드리프트 / derivable 계산값) → look-ahead 안전성이 여기서 도출(immutable=safe · mutable=conditional/as-of · derivable=unsafe). 파싱 상태 = 현재 파이프라인이 실제로 저장하나.
4.1 · Venue (subreddit) — reddit (subreddit)
source: Apify harshmaur~reddit-scraper, dataType=community (확정) · membersCount 등 mutable은 actor crawledAt 스냅샷. immutable은 look-ahead safe. 현재 RedditFetcher는 about.json(§3, 100% 403)이라 실캡처 0 — Apify 전환 시 확보(parseStatus=dropped=구현 대기)
참고: user URL을 조회하면 maxCommentsCount를 0으로 안 주면 dataType: "comment"(그 유저가 단 최근 댓글들)가 같이 딸려온다 — §3.5에서 maxPostsCount: 0 + maxCommentsCount: 0으로 억제한 이유. comment 아이템 자체 스키마는 이번 audit 범위 밖(우리는 항상 억제해서 안 받을 예정)이라 여기 정리 안 함.
다음 액션 (결정 필요)
1. sourceType 4-way 분리 — §2의 reddit_post/reddit_share/reddit_user 신설 + redd.it 커버리지를 라우터 코드에 반영할지, 별도 태스크로 분리할지
2. Fetch 전환 구현 — RedditFetcher를 비인증 about.json 대신 Apify(harshmaur~reddit-scraper)로 교체. §3.5 실측상 subreddit/post/user 전부 가능, share만 우리 쪽 redirect resolve 스텝 추가 필요. §2의 4-way sourceType 분리(1번 액션)와 함께 진행하는 게 자연스러움 — 이 시점에 RedditAbout 인터페이스도 4개 dataType(community/post/user_profile)에 맞게 재설계. OAuth(§3.4)는 굳이 급하지 않음(Apify로 충분히 해결됨) — 비용 절감 옵션으로만 병행 검토
3. 필드 keep/drop 판단 — §4.2는 아직 raw 스키마 나열뿐, 어떤 필드를 실제로 저장할지(예: ageHours류 파생지표는 그대로 저장하면 look-ahead 위험, removedByCategory는 스캠 신호로 유용할 수 있음) 판단이 안 됨 — 다음 대화에서 진행. 확정되면 valueNature 매핑도 같이 정의
4. 데이터 보존 정책 확인 — §3.4에서 언급된 "48시간 삭제 요구"가 사실이면, 지금 social.jsonl/DB에 reddit 데이터를 영구 저장하는 현재 방식이 Reddit ToS와 충돌할 수 있음(확신도 낮음이라 별도 확인 필요) — Apify 스크레이핑도 이 정책 적용 대상인지 포함해서 법무/컴플라이언스 확인 대상