맞팔하지 않는 사람을 찾아주는 Chrome 확장 프로그램 · 백엔드 없음, 자격 증명 전달 없음 · 주입된 content script를 통해 사용자 본인의 Instagram 세션을 활용합니다
아키텍처
어떤 instagram.com 탭에서든 팝업을 엽니다. Popup UI가 먼저 chrome.storage.local에서 이전 스캔을 복원하고 24시간 쿨다운을 확인합니다. Analyze를 누르면 Content script에 메시지를 보내고, content script는 Session detector에게 로그인한 사용자 id를 요청합니다(쿠키 우선, 프로필 요청은 대체 수단). 그다음 content script는 /friendships/<id>/followers(페이지당 25개)와 /following(페이지당 200개)을 Instagram web API에 대해 요청 간 300ms 간격으로 순회합니다. 다시 팝업으로 돌아오면 diff가 맞팔하지 않는 집합을 산출하고, Profile categorizer가 각 계정을 5개씩 배치로 하이드레이션하여 인플루언서와 일반 사용자를 구분하며, 결과와 캐시가 storage에 다시 기록됩니다.
배포: Extension → Chrome · Manifest V3, 압축 해제 상태로 로드하거나 Web Store를 통해 로드
성과
- 0 · 서버 · 모든 것이 사용자의 브라우저 안에서 실행됩니다
- 40+ · 로그인 감지가 견디는 UI 언어 수
- 첫 429 · kill switch를 발동시켜 스캔이 실패하는 대신 저하됩니다
- 24시간 · 계정별 스캔 쿨다운, 그동안 결과는 복원됩니다
보여준 역량: 비공개 API 리버스 엔지니어링 · 레이트 리밋을 고려한 페이지네이션 · 캐시 무효화(7일 TTL) · Chrome 메시지 패싱 · CSRF 토큰 처리
문제와 해결책
하나의 제약이 모든 것을 좌우합니다: Instagram에는 팔로워 목록을 위한 공개 API가 없으므로, 확장 프로그램은 자격 증명을 절대 보유하지 않고 레이트 리밋을 건드리지 않으면서 사용자 본인의 세션을 재사용해야 합니다.
█ 백엔드가 아니라 사용자의 세션을 활용하기
문제: 팔로워를 나열하는 공식 API가 없습니다. 서버 측 스크래퍼는 사용자의 비밀번호나 탈취한 쿠키가 필요하고, 차단을 견디기 위한 프록시도 필요합니다. 두 가지 모두 신뢰와 비용 측면에서 받아들일 수 없습니다.
instagram.com에 주입된 content script는 웹 앱이 사용하는 것과 동일한 비공개 엔드포인트를 호출합니다. 요청은 credentials: include, 브라우저 본인의 user agent, 웹 앱 id 헤더와 함께 나가므로, Instagram 입장에서는 사용자가 브라우징하는 것처럼 보입니다.
- 스크래핑 백엔드 거부: 자격 증명 처리 제로가 파이프라인의 완전한 통제보다 낫습니다
- 팔로워 모달의 DOM 스크래핑 거부: 마크업 변경에 취약하고 JSON 엔드포인트보다 훨씬 느립니다
- 수용한 트레이드오프: 문서화되지 않은 엔드포인트는 예고 없이 바뀔 수 있으므로, 코드는 모든 응답을 선택적인 것으로 취급합니다
▒ 레이트 리밋: 속도 조절, 감지, 저하
문제: 팔로워 페이지네이션에 계정마다 프로필 요청 하나를 더하면 수백 건의 요청이 될 수 있습니다. 비공개 엔드포인트를 두드리면 429를 받고 사용자의 실제 계정이 플래그될 수 있습니다.
페이지 사이 300ms 정지, 5개씩 배치로 하는 프로필 하이드레이션, 그리고 전역 kill switch: 첫 429가 세션의 이후 모든 프로필 요청을 멈추고 스캔은 기본 데이터만으로 계속됩니다.
- retry-with-backoff 거부: 어뷰즈 방지 시스템에 대고 재시도하는 것은 상황을 더 악화시킵니다
- 저하가 실패보다 낫습니다: 사용자는 인플루언서 점수만 빠질 뿐 여전히 맞팔하지 않는 전체 목록을 얻습니다
- kill switch는 일방향 플래그로, 사용자의 계정을 보호하기 위해 의도적으로 보수적입니다
▚ 24시간 스캔 쿨다운
문제: 전체 스캔은 가장 무거운 작업입니다. 사용자가 Analyze를 반복해서 클릭하면 새로운 정보 하나 없이 차단 위험만 배가됩니다.
각 스캔은 chrome.storage.local에 계정별로 타임스탬프가 찍힙니다. 24시간 이내에는 버튼이 실시간 카운트다운과 함께 비활성화되고 저장된 결과가 대신 표시됩니다.
- 무제한 스캔 거부: 사용자의 계정 보호가 최신성보다 우선합니다
- 수용한 트레이드오프: 데이터가 최대 하루까지 오래될 수 있습니다
- follow / unfollow 액션은 저장된 결과를 제자리에서 업데이트하므로, 캐시된 뷰가 스캔 사이에도 정확하게 유지됩니다
▞ 40개 이상 언어에서의 로그인 감지
문제: 첫 버전은 영어 alt text 'profile picture'로 로그인을 감지했습니다. 영어가 아닌 Instagram UI는 확장 프로그램이 사용자가 로그아웃했다고 착각하게 만들었습니다.
다섯 겹의 대체 수단: 40개 이상 언어를 커버하는 지역화된 alt text 테이블, 기하학적 휴리스틱(nav 안의 작은 정사각형 이미지), Instagram 본인의 데이터 객체, 로그아웃 링크, 그리고 세션 쿠키.
- 단일 '신뢰할 수 있는' 셀렉터 거부: Instagram의 난독화된 클래스 이름은 고정하기에는 너무 자주 바뀝니다
- 휴리스틱은 개별적으로 오탐할 수 있으므로, 다섯 개의 독립적인 신호가 서로를 교차 확인합니다
- 쿠키는 실제 세션보다 오래 살아남을 수 있으므로 쿠키 확인은 최후의 수단입니다
▓ 공격적으로 캐시하고, 계정으로 키 지정하기
문제: 프로필 하이드레이션은 분석된 사용자당 요청 하나를 소모합니다. 재스캔과 계정 전환은 그 비용을 다시 지불하고 레이트 리밋을 다시 감수하게 됩니다.
사용자명별 분석 결과는 7일 캐시에 저장됩니다; 스캔 결과는 계정 id로 키가 지정되므로, 여러 계정을 관리하는 사람은 전환 후에도 유지되는 별도의 이력을 갖게 됩니다.
- 캐시 없는 단순함 거부: 반복 요청이 여기서 가장 희소한 자원입니다
- 7일은 오래됨과 요청 예산 사이의 균형을 맞춥니다, 팔로워 수는 천천히 움직입니다
- 429 kill switch가 발동하면, 캐시된 프로필이 여전히 점수를 무료로 채워줍니다
▤ 이력을 잃지 않고 언팔로우하기
문제: 목록을 검토하는 동안 누군가를 언팔로우하면 아래의 데이터가 변경됩니다: 사용자가 사라지고, 이를 반영하기 위한 재스캔은 일일 스캔을 소진하게 됩니다.
언팔로우된 계정은 사라지는 대신 영속화된 최근 언팔로우 목록으로 이동합니다. 다시 팔로우하면 일반 목록으로 복원되고, 모든 변경은 저장된 결과를 제자리에서 편집합니다.
- 액션 후 재스캔 거부: 언팔로우 한 번이 전체 스캔을 소모해서는 안 됩니다
- 조용한 제거 거부: 사용자는 실행 취소 경로와 누구를 제거했는지에 대한 기록을 원합니다
- 상태 전환은 storage에 저장되므로, 정리 중간에 팝업을 닫아도 아무것도 잃지 않습니다