데모 영상이 곧 공개됩니다...
맞팔하지 않는 계정을 찾아주는 Chrome 확장 프로그램 · 백엔드와 계정 정보 전송 없음 · 삽입된 콘텐츠 스크립트가 사용자 본인의 Instagram 세션을 활용
아키텍처
- Popup UI, 프런트엔드 · 작업 팝업, popup · popup.js
- Profile categorizer, 분석 · 인플루언서 점수, popup.js · categorizeUsers
- Content script, 핵심 · 삽입된 API 클라이언트, content · content.js, 커서 페이지네이션, 429 kill switch, 300ms 속도 조절, 쿠키에서 CSRF, follow / unfollow POST, 프로필 상세 정보 조회
- Session detector, 인증 · 로그인 감지, content.js · isUserLoggedIn
- chrome.storage.local, 스토리지 · 로컬, instagramAnalysisResults_<id>
- Instagram web API, 외부 · 비공개 API, 외부 서비스, friendships · web_profile_info
- Popup UI에서 Profile categorizer 방향으로: 목록 차이 계산
- Popup UI에서 Content script 방향으로: 메시지 · 검사 + 언팔로우
- Profile categorizer에서 Content script 방향으로: 프로필 조회 · 5개씩
- Content script에서 Session detector 방향으로: 사용자 ID 확인
- Content script에서 Instagram web API 방향으로: API 호출 · 페이지네이션 + 속도 조절
- Popup UI에서 chrome.storage.local 방향으로: 검사 저장 · 24시간 기록
- chrome.storage.local에서 Popup UI 방향으로: 열 때 복원
instagram.com 탭에서 팝업을 열면 Popup UI가 먼저 chrome.storage.local에서 이전 검사 결과를 복원하고 24시간 대기 시간을 확인합니다. Analyze를 누르면 Content script에 메시지를 보내고, 콘텐츠 스크립트는 Session detector에서 로그인한 사용자 ID를 가져옵니다. 쿠키를 우선 확인하고 필요하면 프로필을 요청합니다. 이후 Instagram web API의 /friendships/<id>/followers와 /following을 페이지별로 순회합니다. 페이지 크기는 각각 25개와 200개이며 요청 사이에는 300ms 간격을 둡니다. 팝업은 팔로워와 팔로잉의 차이를 계산해 맞팔하지 않는 계정을 찾습니다. Profile categorizer는 계정을 5개씩 묶어 상세 정보를 가져온 뒤 인플루언서와 일반 사용자를 구분합니다. 결과와 캐시는 다시 로컬 저장소에 기록합니다.
배포: Extension → Chrome · Manifest V3, 압축 해제 상태로 로드하거나 Web Store를 통해 로드
성과
- 0대 · 서버 · 모든 작업을 사용자 브라우저에서 실행
- 40개 이상 · 로그인 감지를 지원하는 UI 언어
- 첫 429 응답 · 즉시 추가 요청을 중단하고 기본 정보만으로 검사 계속
- 24시간 · 계정별 검사 대기 시간 동안 이전 결과 표시
보여준 역량: 비공개 API 리버스 엔지니어링 · 요청 제한을 고려한 페이지네이션 · 캐시 무효화(7일 TTL) · Chrome 메시지 통신 · CSRF 토큰 처리
문제와 해결책
핵심 제약은 Instagram에 팔로워 목록을 제공하는 공개 API가 없다는 점입니다. 확장 프로그램이 계정 정보를 보관하지 않으면서 요청 제한을 넘지 않고 사용자 본인의 세션을 활용해야 했습니다.
█ 백엔드가 아니라 사용자의 세션을 활용하기
문제: 팔로워를 나열하는 공식 API가 없습니다. 서버 측 스크래퍼는 사용자의 비밀번호나 탈취한 쿠키가 필요하고, 차단을 견디기 위한 프록시도 필요합니다. 두 가지 모두 신뢰와 비용 측면에서 받아들일 수 없습니다.
instagram.com에 삽입된 콘텐츠 스크립트가 웹 앱과 같은 비공개 엔드포인트를 호출합니다. credentials: include, 브라우저의 User-Agent, 웹 앱 ID 헤더를 그대로 사용하므로 일반적인 Instagram 웹 요청과 같은 형태입니다.
- 계정 정보를 다루지 않는 것이 수집 과정을 완전히 통제하는 것보다 중요해 스크래핑 백엔드는 두지 않았습니다
- 팔로워 팝업의 DOM 수집은 마크업 변경에 취약하고 JSON 엔드포인트보다 느려 제외했습니다
- 문서화되지 않은 엔드포인트가 예고 없이 바뀔 수 있어 모든 응답 필드를 선택 값으로 처리합니다
▒ 요청 제한 대응: 속도 조절, 감지, 기능 축소
문제: 팔로워 목록을 순회하면서 계정마다 프로필을 한 번씩 더 요청하면 전체 요청이 수백 건에 이를 수 있습니다. 비공개 엔드포인트를 너무 자주 호출하면 429 응답을 받고 사용자의 계정이 의심 활동으로 표시될 위험이 있습니다.
페이지 요청 사이에 300ms를 기다리고 프로필 상세 정보는 5개씩 묶어 가져옵니다. 첫 429 응답이 오면 전역 중단 플래그를 켜 해당 세션의 추가 프로필 요청을 모두 멈춥니다. 검사는 기본 정보만으로 계속 진행합니다.
- 악용 방지 시스템이 요청을 막은 뒤 재시도하면 상황이 나빠질 수 있어 backoff 재시도도 하지 않습니다
- 검사 전체를 실패시키지 않고 인플루언서 점수만 제외한 맞팔하지 않는 계정 목록을 제공합니다
- 중단 플래그는 한 번 켜지면 되돌리지 않아 사용자의 계정을 보수적으로 보호합니다
▚ 검사 후 24시간 대기
문제: 전체 검사는 가장 많은 요청을 사용합니다. Analyze를 반복해서 눌러도 새로운 정보는 거의 없고 차단 위험만 커집니다.
검사할 때마다 계정별 시각을 chrome.storage.local에 저장합니다. 24시간이 지나기 전에는 버튼을 비활성화하고 남은 시간을 표시하며 기존 결과를 대신 보여줍니다.
- 최신 정보보다 계정 보호가 중요해 무제한 검사는 허용하지 않습니다
- 결과가 최대 하루 전 데이터일 수 있다는 점은 감수했습니다
- 팔로우와 언팔로우 작업은 저장된 결과에도 즉시 반영해 다음 검사 전까지 화면을 정확하게 유지합니다
▞ 40개 이상 언어에서 로그인 상태 감지
문제: 첫 버전은 영어 alt text 'profile picture'로 로그인을 감지했습니다. 영어가 아닌 Instagram UI는 확장 프로그램이 사용자가 로그아웃했다고 착각하게 만들었습니다.
다섯 가지 신호를 차례로 확인합니다. 40개 이상 언어의 대체 텍스트 목록, nav 안의 작은 정사각형 이미지, Instagram의 데이터 객체, 로그아웃 링크, 세션 쿠키입니다.
- Instagram의 난독화된 클래스 이름은 너무 자주 바뀌므로 하나의 선택자에 의존하지 않습니다
- 개별 휴리스틱은 잘못 판단할 수 있어 독립적인 신호 다섯 개를 함께 확인합니다
- 쿠키는 실제 세션보다 오래 살아남을 수 있으므로 쿠키 확인은 최후의 수단입니다
▓ 적극적인 캐시와 계정별 데이터 분리
문제: 프로필 상세 정보를 가져올 때마다 사용자 한 명당 요청이 하나씩 추가됩니다. 다시 검사하거나 계정을 전환할 때마다 같은 요청을 반복하면 요청 제한 위험도 커집니다.
사용자명별 분석 결과는 7일 동안 캐시합니다. 검사 결과는 계정 ID별로 저장하므로 여러 계정을 사용해도 각각의 기록이 유지됩니다.
- 요청 횟수가 가장 중요한 자원이므로 캐시 없는 단순한 구조는 선택하지 않았습니다
- 팔로워 수는 천천히 변하므로 7일이면 데이터 최신성과 요청 수 사이의 균형을 맞출 수 있습니다
- 429 응답으로 추가 요청을 멈춰도 캐시된 프로필로 일부 점수를 계속 계산할 수 있습니다
▤ 기록을 남기며 언팔로우하기
문제: 목록을 보다가 계정을 언팔로우하면 해당 사용자가 목록에서 사라집니다. 이 변경을 반영하려고 다시 검사하면 하루 한 번의 검사 기회를 소모하게 됩니다.
언팔로우한 계정은 사라지지 않고 저장된 최근 언팔로우 목록으로 이동합니다. 다시 팔로우하면 일반 목록으로 돌아오며 모든 변경은 기존 검사 결과에 즉시 반영됩니다.
- 언팔로우 한 번 때문에 전체 검사를 다시 실행하지 않습니다
- 실행을 취소하고 누구를 삭제했는지 확인할 수 있도록 계정을 바로 숨기지 않습니다
- 상태 변경을 로컬 저장소에 기록해 정리 도중 팝업을 닫아도 진행 상황이 남습니다