리버스 엔지니어링 전반부에서 기계적으로 코드를 쪼개고, 알고리즘 계열을 식별하고, 차분 검증까지 마치면 “무엇을 계산하는지는 알겠다”는 결론에 도달한다. 하지만 그 결론만으로는 배포할 수 없다. 로직은 여전히 원래 런타임에 묶여 있고, 이를 CI에 넣을 순수 함수로 만들지, 누군가 계속 돌봐야 하는 외부 프로세스로 남길지 결정해야 한다. 여기서 선택을 잘못하면 앞 단계에서 아낀 시간은 운영 단계에서 이자까지 붙어 돌아온다.
4단계 개요에서는 이를 한 줄로 정리했다. “4단계: 전송 계층으로 단계적 하향.” 이 글은 그 한 줄을 풀어 쓴다. 핵심은 간단하다. 한 단계 내려갈 때마다 의존성 범위, 장애 유형, 배포 비용이 자릿수 단위로 증가한다. 따라서 기본값은 언제나 위쪽 단계를 지키는 것이다.
구현 형태는 세 가지뿐이다
역분석 로직이 안착할 수 있는 방식은 아래 세 가지다. 순서는 고정해야 한다. “일단 먼저 돌아가는 것”을 기준으로 매번 즉흥적으로 고르면 안 되며, 팀의 규약으로 명시하는 편이 좋다.
네이티브 재작성. 대상 언어로 다시 구현해 원래 런타임에서 완전히 분리한다. 의존성은 표준 라이브러리만 둔다. 전제는 알고리즘 계열을 정확히 찾아내는 것이다. 핑거프린팅 단계를 통과했다면 대개 90%는 공개 구현을 옮겨올 수 있고, 남은 차이점만 별도로 처리하면 된다. 결과물은 순수 함수다.
최소 조각을 로컬 JS 엔진에서 실행. 단기간에 정제하기에는 비용이 너무 큰 로직도 있다. 이때는 원본 JS의 작은 조각만 남기고, 전체 페이지가 아니라 필요한 수십 줄만 로컬 Node/V8에서 돌린다.
수동형 브라우저 브리지. 실제 로그인 상태의 페이지 런타임에서만 존재하는 상태가 있다. 런타임에 전달되는 서명이나 세션에 묶인 동적 식별자처럼 정적으로 복원할 수 없고, 당장은 브라우저 내부에서 읽는 방법밖에 없는 경우다. 이는 임시 수단이다. 인터페이스 문서에 반드시 표시하고, 기본 경로로 배포해서는 안 된다.
단계별 비용은 선형적으로 늘지 않는다
순서를 고정해야 하는 이유는 명확하다. 세 단계의 비용 차이는 완만하지 않다. 한 단계 내려갈 때마다 사실상 자릿수가 달라진다.
단계 | 의존성 범위 | 장애 유형 | CI 적용 가능? |
|---|---|---|---|
네이티브 재작성 | 표준 라이브러리만 사용, 외부 프로세스 없음 | 출력 불일치, | 가능 — 순수 함수이기 때문 |
로컬 JS 엔진 | Node 런타임 하나 추가, V8 컨텍스트는 스레드 안전하지 않아 동시성에 락 필요 | 엔진 버전 문제, 조각 코드가 의존하는 전역 객체 누락 | 간신히 가능 — 엔진 설치가 필요 |
수동형 브라우저 브리지 | 실제 Chrome + 확장 프로그램 + 사람이 유지하는 세션 + 로컬 루프백 채널 | 페이지가 열려 있지 않음, 세션 만료, 구조 변경, 탭 닫힘 | 불가능 — 실제 사람이 필요 |
1단계 장애는 단위 테스트로 잡힌다. 3단계 장애는 “오늘 사용자가 그 탭을 닫았다”다. 순수 함수로 만들 수 있는 것을 3단계로 보내면 모든 호출에 사람을 묶어두는 셈이다. 참고로 알고리즘 계열만 식별되면, 난독화된 서명 SDK도 내장 crypto만 의존하는 600줄 미만의 독립 구현으로 안착할 수 있다. 즉 1단계에서 실행 가능하다. 브라우저 브리지를 거쳐야만 한다고 생각했던 대상도, 대개는 아직 알고리즘을 충분히 식별하지 못했을 뿐이다.
수동형 브리지는 한 번 읽고 끝낸다
수동형 브리지가 통제 불능으로 커지는 경우는 하나뿐이다. 브리지가 “도와주기” 시작할 때다. 자동 새로고침, 자동 로그인, 로드 완료까지 자동 대기 같은 기능이 붙는 순간, 단순 전달기는 크롤러에 가까워진다. 따라서 경계는 극단적으로 좁게 잡아야 한다. 아래 제약은 시행착오 끝에 얻은 기준이다.
스냅샷 조회는 한 번만, 페이지 조작은 절대 하지 않는다. 이미 열려 있는 페이지의 쿠키, 세션 상태, 페이지 런타임을 각각 한 번만 조회한다. 탭을 만들지 않고, 새로고침·이동·포커스 전환을 하지 않으며, 폴링으로 기다리지도 않는다. 페이지가 없으면 없는 것이다. 사용자를 대신해 페이지를 열지 않는다.
무언가 빠진 순간 즉시 명시적인 오류를 반환한다. 일치하는 탭이 없으면 tab_unavailable, 페이지는 열려 있지만 로그인되지 않았으면 not_logged_in, 로그인은 됐지만 런타임이 준비되지 않았으면 runtime_unavailable을 반환한다. 이 세 오류 코드는 각각 하나의 실제 상태와 다음 행동에 대응한다. 페이지를 기다릴지, 로그인할지, 대상을 바꿀지 알 수 있다. 호출자가 “실패했다”는 한 줄짜리 오류를 보고 추측하게 해서는 안 된다.
민감한 상태는 브라우저 밖으로 나가지 않는다. 확장 프로그램은 cookies / webRequest 권한을 요청하지 않는다. 이미 열려 있는 일치 탭만 조회하고, 해당 페이지 컨텍스트 안에서 요청을 끝낸 뒤 반환 전에 필드를 제거한다. 쿠키와 페이지 서명 상태는 단 한 단계도 Chrome 밖으로 나가지 않으며, 채널은 기본적으로 로컬 루프백 주소에만 연결한다. 브리지가 전달하는 것은 자격 증명이 아니라 결과다.
플랫폼은 몇 개인데 스코프는 왜 15개인가
수동형 브리지가 전달하는 것은 어댑터로 제한된 화이트리스트 요청이다. 경로, 파라미터, referer를 모두 제한한다. 여기서 가장 직관에 반하는 점은, 플랫폼은 몇 개 안 되는데 스코프가 총 15개라는 사실이다. 스코프는 “플랫폼”이 아니라 “페이지 컨텍스트”를 기준으로 나누기 때문이다. 같은 TikTok이라도 Creative Center, Top Ads, Creator 플랫폼, 인플루언서 라이브러리, Ads Manager는 다섯 개의 독립 스코프다. 각각 세션 상태와 페이지 런타임이 따로 있다. 광고 백오피스에 로그인했다고 Creator 플랫폼 런타임까지 얻는 것은 아니다. 플랫폼당 스코프 하나로 잘라두면, “하위 사이트 A에는 로그인했는데 하위 사이트 B 요청은 처리할 수 없다”는 첫 사례에서 다시 설계해야 한다. Xiaohongshu 역시 메인 사이트, 앱 대응 경로, 크리에이터 마켓플레이스가 각각 별도 스코프다.
스케줄링 단위도 여기에 맞춘다. 락은 플랫폼 패밀리 수준에서만 건다. 같은 패밀리 안의 요청, 예를 들어 Douyin 계열 요청은 직렬로 처리한다. 같은 실제 탭을 공유하므로 하나의 페이지 컨텍스트에 동시에 요청을 넣으면 서로 충돌한다. 반면 Douyin과 Xiaohongshu처럼 서로 다른 패밀리는 별개 탭이므로 병렬 처리할 수 있다. 같은 패밀리 안에서는 최소 요청 간격도 추가한다. 범위를 너무 넓게 잡으면 병렬 처리 가능한 작업까지 직렬화되고, 너무 좁게 잡으면 탭을 공유하는 요청이 충돌한다. 플랫폼 패밀리는 곧 “같은 페이지 런타임을 공유하는 범위”라는 자연스러운 경계다.
사람이 개입할 수 있는 유일한 경로, 명시적 로그인 핸드오프
수동형 브리지는 페이지를 조작하지 않지만 세션은 만료된다. 해결책은 사람의 개입을 명시적이고 단발적인 동작 하나로 축소하는 것이다. 대화형 명령은 핸드오프에 들어가고, 프로그램은 운영체제를 통해 해당 업무 페이지를 연다. 사용자가 직접 로그인하고 페이지가 준비될 때까지 기다린 뒤, 원래 요청을 다시 실행한다. 이 과정에서 확장 프로그램은 버튼을 누르지 않고, 폼을 채우지 않으며, 쿠키를 내보내지 않는다. 로그인은 실제 브라우저에서 사용자가 하는 일이고, 프로그램은 끝난 뒤 요청을 이어받을 뿐이다.
가장 중요한 제약은 절대 암묵적으로 발동해서는 안 된다는 것이다. 비대화형 명령(CI, 예약 작업)은 브라우저를 띄우지 않는다. 세션 오류를 명확히 반환하고 상위 계층이 판단하게 둔다. 핸드오프는 오탐도 막아야 한다. 로그인에 성공한 뒤 런타임에 허용하는 추가 대기는 짧아야 하며, 구현에서는 최대 20초다. 그 안에 결론이 나야 한다. 업무 페이지가 이미 대상 인터페이스 컨텍스트가 없는 계정 페이지로 리디렉션됐다면, “아직 로딩 중”이라고 잘못 판단해 무의미하게 기다리지 말고 현재 단계를 즉시 끝낸다. 단계적 하향을 허용하는 흐름이라면 이 단계는 unavailable로 기록하고 계속 진행하며, 전체 작업을 실패시키지 않는다.
단계 상태는 성공·실패 둘로는 부족하다
앞선 내용을 모두 관통하는 원칙은 하나다. 어떤 단계도 “성공” 또는 “실패”만 반환해서는 안 된다. 오케스트레이션 파이프라인에서 각 단계의 결과는 여섯 가지 형태를 가진다. completed(완료), empty(실행했지만 데이터 없음), ready(제출 대기 상태로 준비됨), skipped(규칙에 따라 의도적으로 건너뜀), unavailable(현재 사용할 수 없음, 보통 세션 필요를 의미), blocked(선행 조건 미충족)다.
“의도적으로 건너뜀”, “세션 필요”, “실제로 데이터 없음”은 전혀 다른 신호다. 불투명한 결과 하나만 돌려주면 빈 결과가 “원래 비어야 하는 것”인지, “세션이 죽었는데 아무도 몰랐던 것”인지 구분할 수 없다. 그런 파이프라인은 운영할 수 없다. 단계 상태를 유한한 enum으로 모델링해야 스크립트나 모델 같은 오케스트레이션 계층이 이를 바탕으로 하향 처리, 재인증, 중단 중 무엇을 할지 결정할 수 있다. 수동형 브리지의 세 오류 코드를 흐름 전체 수준으로 끌어올린 원리와 같다.
모델은 구현 단계가 아니라 배치 판단에 쓴다
모델이 등장하는 지점은 여기뿐이며, 역할도 제한적이다. 모델이 로직을 정제해주는 것이 아니라, 정제할 수 있는지와 어디까지 정제할지를 판단하도록 돕는 것이다. 이는 서명 해독이 아니라 아키텍처 판단이다. 핵심 질문은 하나다. 의존하는 상태를 정적으로 재구성할 수 있는가, 아니면 런타임에서만 얻을 수 있는가? 그다음 정제에 드는 노력과 변경 빈도를 비교한다. 각 단계에서 모델에 요구하는 역량은 다르다.
단계 | 필요한 역량 | 선택 | model id |
|---|---|---|---|
모듈 전체를 읽고 의존성 범위 파악 | 긴 컨텍스트, 호출 그래프를 한 번에 읽는 능력 | Kimi K3 |
|
단계 배치를 양쪽 관점에서 논증하고 “일단 되게 하자”에 반론 제기 | 강한 추론 능력, 하루를 더 써서 순수화할 근거를 제시 | Claude Opus 5 |
|
수십~수백 개 기능의 대량 1차 분류 | 낮은 비용, 높은 동시성 | Claude Sonnet 5 |
|
하향 처리 이후 원인 귀속 | 중간 수준 추론, 실패 로그를 근거로 설명 | GPT-5.6 Sol |
|
가장 중요한 것은 두 번째 단계다. 구현 방식을 배치할 때 흔히 하는 실수는 모델이 “일단 되게 하자”는 사용자의 분위기를 따라가며 “브라우저 브리지가 가장 쉽다”는 답을 내놓는 것이다. 장기 비용을 계산하는 것이 아니라 사용자를 따라 하는 셈이다. 강한 추론 모델은 반대로 말한다. “이 구간은 표준 해시와 상수 하나의 변형에 불과하니, 하루를 써서 순수 함수로 구현할 가치가 있습니다. 브리지에 올리면 안 됩니다.” 내 말을 믿을 필요는 없다. 직접 시험해보면 된다. 로직 3개를 고르고, 그중 최소 1개는 올바른 배치를 이미 아는 대조군으로 둔다. 같은 “배치를 제안하고, 근거를 논증하고, 너무 이르게 브라우저 브리지로 보내려는 판단에는 반론을 제기하라”는 프롬프트를 claude-opus-5와 gpt-5.6-sol에 넣어라. 볼 것은 하나다. 더 높은 단계로 끌어올리려 싸우는가, 아니면 게으르게 3단계를 기본값으로 택하는가.
진짜 걸림돌은 전환 비용이다
세 벤더에 걸친 네 단계는 SDK도 세 개, 인증 방식도 세 개, 오류 형식도 세 개가 된다. 단계마다 모델을 바꾸겠다고 클라이언트를 다시 쓰는 일은 가치가 없다. 그래서 대부분은 모든 작업에 모델 하나만 쓰게 되고, 정작 추론 모델이 가장 필요한 배치 판단에서도 사용자의 말만 따라 하는 모델을 쓰게 된다.
AIReiter는 이 계층을 하나로 평탄화한다. 키 하나와 OpenAI 호환 인터페이스 하나로 네 단계를 모두 사용할 수 있으며, 요청 본문의 model 필드만 바꾸면 전환된다.
# 배치 논증: 추론 단계
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"messages": [{"role": "user", "content": "<배치 판단 프롬프트 + 역분석한 로직 조각 + 의존성 목록>"}]
}'
# 대량 1차 분류: 필드 하나만 변경
# "model": "claude-sonnet-5"
# 하향 처리 원인 귀속:
# "model": "gpt-5.6-sol"
이미 OpenAI SDK를 사용 중이라면 base_url을 https://aireiter.com/api/v1로 지정하면 된다. Anthropic SDK를 쓴다면 같은 키로 POST /api/v1/messages를 호출하면 된다. 가격은 Claude 모델이 정가 대비 30% 할인, GPT 모델은 반값이다. 이 워크플로의 비용은 두 곳에 집중된다. 수십~수백 개 기능을 대상으로 하는 대량 1차 분류(Sonnet, 높은 호출량), 그리고 모듈 전체의 의존성 범위를 읽기 위한 긴 컨텍스트 단일 입력(Kimi, 호출당 많은 토큰)이다. 대량 분류는 Claude 모델에서 돌기 때문에 가장 밀도 높은 단계에 할인이 적용된다. 추론 단계의 배치 논증 역시 Claude 모델이며 30% 할인이다. Kimi K3의 긴 컨텍스트 단계도 같은 키로 사용할 수 있다.
가입 없이 사용해 보기 — 먼저 로직 조각 몇 개를 직접 넣어보고, 두 모델이 사용자의 말만 따라 하는지 아니면 “이걸 브라우저 브리지에 올려야 하는가”라는 판단에 반론을 제기하는지 확인해 보자.
마무리
역분석 로직을 안착시키는 일은 기술 문제가 아니라 비용 문제다. 네이티브 재작성 > 로컬 JS 엔진 > 수동형 브라우저 브리지라는 3단계 사다리는 뒤집을 수 없다. 한 단계 내려갈 때마다 순수 함수는 외부 의존성, 사람의 개입, 살아 있는 탭이 필요한 프로세스로 바뀌기 때문이다. 수동형 브리지가 금지 구역인 것은 아니다. 단, 엄격한 경계를 가진 임시 컴포넌트여야 한다. 한 번만 스냅샷을 읽고 능동적으로 행동하지 않을 것, 페이지가 없으면 즉시 명시적 오류를 낼 것, 민감한 상태를 브라우저 밖으로 내보내지 않을 것, 로그인은 명시적 핸드오프에서만 할 것, 단계 상태는 항상 읽을 수 있을 것. 이 기준을 지키면 신뢰할 수 있는 임시 방편이 되지만, 하나라도 놓치면 누구도 유지보수하고 싶어 하지 않는 블랙박스가 된다. 모델은 기능을 어느 단계에 배치할지 판단하고 “일단 되게 하자”는 관성에 맞서는 데 쓴다. 반면 각 재작성의 정확성을 판정하는 기준은 모델이 아니라 차분 테스트다.
