AIREITER

역공학 코드, 어디까지 재작성할까: 브라우저 브리지를 통제하는 3단계 기준

마지막 업데이트: 2026-07-31 07:19:47

역공학 작업의 앞부분, 즉 기계적인 코드 분할과 알고리즘 계열 식별, 차분 검증까지 마치면 “무엇을 계산하는지 이해했다”는 결론에 도달한다. 하지만 그 결론만으로는 배포할 수 없다. 로직은 여전히 원래 런타임에 묶여 있고, 이를 CI에 넣을 순수 함수로 만들지, 누군가 계속 관리해야 하는 외부 프로세스로 남길지를 결정해야 한다. 여기서 선택을 잘못하면 앞선 단계에서 아낀 시간을 운영 단계에서 이자까지 붙여 되돌려 받게 된다.

4단계 개요에서는 이 과정을 한 줄로 “4단계, 전송 계층으로 폴백한다”고 정리했다. 이번 글에서는 그 한 줄을 풀어본다. 핵심은 간단하다. 한 단계 아래로 갈 때마다 의존성 범위, 장애 유형, 배포 비용이 자릿수 단위로 늘어난다. 그러므로 기본적으로는 위쪽 단계를 지켜야 한다.

배포 형태는 세 가지뿐이다

역공학한 로직을 안착시키는 방식은 아래 세 가지다. 순서도 정해져 있어야 한다. “일단 가장 빨리 돌아가는 방식”으로 매번 즉흥적으로 정할 일이 아니다.

  1. 네이티브로 다시 구현한다. 대상 언어로 재작성해 원래 런타임에서 분리하고, 표준 라이브러리에만 의존하게 만든다. 전제는 알고리즘 계열을 정확히 식별하는 것이다. 핑거프린팅 단계를 통과했다면 90%는 공개 구현을 바탕으로 옮길 수 있고, 남은 차이점만 따로 처리하면 된다. 최종 결과물은 순수 함수다.

  2. 로컬 JS 엔진에서 최소 조각만 실행한다. 단기간에 완전히 정제하기에는 비용이 큰 로직도 있다. 이때는 원래 JS의 작은 일부만 남기고, 전체 페이지가 아니라 필요한 수십 줄만 로컬 Node/V8에서 실행한다.

  3. 수동형 브라우저 브리지를 둔다. 실제 로그인된 페이지 런타임에만 존재하는 상태가 있다. 가령 런타임에서 전달되는 시그니처나 세션에 묶인 동적 식별자는 정적 재구성으로 재현할 수 없고, 당장은 브라우저 안에서만 읽을 수 있다. 이는 임시 형태다. 인터페이스 문서에 반드시 표시해야 하며, 기본 방식으로 배포해서는 안 된다.

단계별 비용은 선형으로 늘지 않는다

이 순서를 고정해야 하는 이유는 명확하다. 세 단계의 비용은 조금씩 증가하는 것이 아니라, 단계마다 자릿수 단위로 뛰어오른다.

단계

의존성 범위

장애 유형

CI 가능 여부

네이티브 재작성

표준 라이브러리만 사용, 외부 프로세스 없음

출력 불일치, assert 하나로 위치 확인

가능 — 순수 함수이기 때문

로컬 JS 엔진

Node 런타임 하나 추가, V8 컨텍스트는 스레드 안전하지 않아 동시성에 락 필요

엔진 버전 문제, 조각 코드가 의존하는 전역값 누락

간신히 가능 — 엔진 설치가 필요

수동형 브라우저 브리지

실제 Chrome + 확장 프로그램 + 사람이 유지하는 세션 + 로컬 루프백 채널

페이지가 열려 있지 않음, 세션 만료, 구조 변경, 탭 종료

불가 — 실제 사용자가 필요

첫 번째 단계의 장애는 단위 테스트가 잡아낸다. 세 번째 단계의 장애는 “오늘 사용자가 그 탭을 닫았다”다. 순수 함수가 될 수 있는 것을 세 번째 단계로 보내면, 호출마다 사람을 붙이는 셈이 된다. 참고로 알고리즘 계열을 식별한 뒤에는 난독화된 시그니처 SDK도 내장 crypto에만 의존하는 600줄 미만의 독립 구현으로 만들 수 있다. 즉, 브라우저 브리지를 거쳐야만 한다고 생각했던 대상은 대개 아직 충분히 식별하지 못한 알고리즘일 뿐이다.

수동형 브라우저 브리지의 원칙: 한 번 읽고, 절대 개입하지 않는다

수동형 브리지가 통제 불능으로 가는 경로는 하나다. 브리지가 페이지를 “도와주기” 시작하는 순간이다. 자동 새로고침, 자동 로그인, 로딩 완료까지의 자동 대기가 여기에 해당한다. 자동 기능 하나를 더할 때마다 포워더는 크롤러에 가까워진다. 그래서 경계는 극도로 좁게 잡아야 한다. 아래 제약은 시행착오 끝에 얻은 것이다.

한 번만 스냅샷을 읽고, 페이지는 절대 바꾸지 않는다. 이미 열려 있는 페이지의 쿠키, 세션 상태, 페이지 런타임을 각각 한 번만 조회한다. 탭을 만들지 않고, 새로고침하지 않고, 이동하지 않고, 포커스를 주지 않고, 폴링하며 기다리지도 않는다. 페이지가 없으면 없는 것이다. 사용자를 대신해 페이지를 열지 않는다.

무언가 없으면 즉시 명시적인 오류를 반환한다. 맞는 탭이 없으면 tab_unavailable, 페이지는 열려 있지만 로그인하지 않았으면 not_logged_in, 로그인은 했지만 런타임이 준비되지 않았으면 runtime_unavailable을 반환한다. 세 오류 코드는 각각 현실의 상태 하나와 다음 행동 하나에 대응한다. 페이지를 기다리거나, 로그인하거나, 대상을 바꾸면 된다. 호출자가 추측해야 하는 뭉뚱그린 “실패”를 던져서는 안 된다.

민감한 상태는 브라우저 밖으로 나가지 않는다. 확장 프로그램은 cookies / webRequest 권한을 요청하지 않는다. 이미 열려 있는 일치 탭만 조회하고, 그 페이지 컨텍스트 안에서 요청을 완료한 뒤 반환 전에 필드를 정리한다. 쿠키와 페이지 시그니처 상태는 단 한 단계도 Chrome 밖으로 나가지 않으며, 채널은 기본적으로 로컬 루프백 주소에만 연결한다. 브리지가 전달하는 것은 자격 증명이 아니라 결과다.

플랫폼은 몇 개인데 scope가 15개인 이유

수동형 브리지는 어댑터로 경로, 파라미터, referer를 모두 제한한 화이트리스트 요청만 전달한다. 이때 가장 직관에 반하는 부분은 scope의 단위다. 플랫폼은 몇 개 되지 않지만 scope는 총 15개다. scope를 플랫폼 기준이 아니라 “페이지 컨텍스트” 기준으로 나누기 때문이다. TikTok만 해도 Creative Center, Top Ads, Creator platform, 인플루언서 라이브러리, Ads Manager는 각기 독립된 다섯 scope다. 세션 상태도, 페이지 런타임도 각각 다르다. 광고 백엔드에 로그인했다고 Creator platform의 런타임까지 얻는 것은 아니다. 플랫폼당 한 덩어리로 잘라버리면, 첫 번째 “하위 사이트 A에는 로그인했지만 하위 사이트 B는 처리할 수 없음” 사례에서 바로 설계가 무너진다. Xiaohongshu 역시 메인 사이트, 앱에 해당하는 경로, 크리에이터 마켓플레이스를 세 scope로 분리한다.

스케줄링 단위도 여기에 맞춘다. 락은 플랫폼 패밀리 수준에서만 건다. 같은 패밀리 안의 요청, 예를 들어 Douyin 패밀리 요청은 같은 실제 탭을 재사용하므로 직렬 실행한다. 한 페이지 컨텍스트에 동시에 요청을 보내면 서로 덮어쓴다. 반면 Douyin과 Xiaohongshu처럼 서로 다른 패밀리는 별개 탭이므로 병렬로 실행한다. 같은 패밀리 안에서는 최소 요청 간격도 둔다. 너무 굵게 묶으면 병렬 처리 가능한 작업까지 직렬화하고, 너무 잘게 나누면 같은 탭을 공유하는 요청이 충돌한다. 플랫폼 패밀리가 바로 “같은 페이지 런타임을 공유한다”는 자연스러운 경계다.

로그인이 필요할 때는 명시적으로만 사용자에게 넘긴다

수동형 브리지는 페이지를 조작하지 않지만, 세션은 만료된다. 해결책은 사람의 개입을 단 한 번의 명시적 동작으로 압축하는 것이다. 대화형 명령이 핸드오프를 시작하면 프로그램은 운영체제를 통해 해당 업무 페이지를 열고, 사용자가 직접 로그인해 페이지가 준비될 때까지 기다린 다음 원래 요청을 재실행한다. 이 과정에서 확장 프로그램은 버튼을 누르지 않고, 폼을 채우지 않고, 쿠키를 내보내지 않는다. 로그인은 실제 브라우저에서 사용자가 하고, 프로그램은 끝난 뒤 요청을 다시 이어받을 뿐이다.

가장 중요한 조건은 절대로 암묵적으로 실행하면 안 된다는 것이다. 비대화형 명령, 즉 CI나 예약 작업은 브라우저를 띄우지 않는다. 세션 오류를 명확히 반환하고 상위 레이어가 결정하게 둔다. 핸드오프는 오탐도 막아야 한다. 로그인에 성공한 뒤 런타임에는 짧은 추가 대기만 허용한다. 구현에서는 20초다. 그 안에 판정이 나와야 한다. 업무 페이지가 이미 대상 인터페이스 컨텍스트가 없는 계정 페이지로 리디렉션됐다면, 결정적으로 “도달할 수 없음”인 상황을 “아직 로딩 중”으로 오인해 무작정 기다리지 말고 현재 단계를 즉시 끝낸다. 폴백을 허용하는 흐름이라면 이 단계는 unavailable로 기록하고 계속 진행하면 된다. 전체 작업이 실패할 필요는 없다.

단계 결과는 성공·실패 두 개로는 부족하다

위 모든 설계의 공통 기반은 하나다. 어떤 단계도 “성공” 또는 “실패”만 반환해서는 안 된다. 오케스트레이션 파이프라인에서 단계 결과는 여섯 가지다. completed는 완료, empty는 실행됐지만 데이터 없음, ready는 준비됐고 제출 대기, skipped는 규칙에 따라 의도적으로 건너뜀, unavailable은 현재 사용할 수 없음으로 대개 세션이 필요하다는 뜻, blocked는 선행 조건을 충족하지 못했다는 뜻이다.

“의도적으로 건너뜀”, “세션 필요”, “정말로 데이터가 없음”은 완전히 다른 신호다. 불투명한 결과 하나만 돌려주면 빈 결과가 “원래 비어야 하는 것”인지, “세션이 죽었는데 아무도 몰랐던 것”인지 알 수 없다. 그런 파이프라인은 운영할 수 없다. 단계 상태를 유한한 enum으로 모델링하면 스크립트든 모델이든 오케스트레이션 레이어가 이를 보고 폴백할지, 재인증할지, 중단할지를 결정할 수 있다. 수동형 브리지의 세 오류 코드를 흐름 전체 수준으로 끌어올린 원리와 같다.

기능을 어느 단계에 둘지 모델로 판단하는 법

모델은 여기서만 제한적으로 등장한다. 모델이 대신 정제해 주는 것이 아니라, 정제할 수 있는지와 어디까지 해야 하는지를 판단하도록 돕는다. 이는 시그니처를 깨는 작업이 아니라 아키텍처 판단이다. 핵심 질문은 하나다. 의존하는 상태를 정적으로 재구성할 수 있는가, 아니면 런타임에서만 얻을 수 있는가? 이 질문을 출발점으로 정제 비용과 변경 빈도를 함께 비교한다. 각 단계에서 모델에 요구하는 역할은 다르다.

단계

필요한 역량

선택

model id

모듈 전체를 읽고 의존성 범위 파악

긴 컨텍스트, 호출 그래프를 한 번에 읽는 능력

Kimi K3

kimi-k3

단계 배치를 양쪽 관점에서 논증하고 “일단 되게 하자”에 반론 제기

강한 추론 능력, 하루를 더 써서 순수화할 이유를 제시

Claude Opus 5

claude-opus-5

수십~수백 개 기능의 대량 1차 분류

저렴한 비용, 높은 동시성

Claude Sonnet 5

claude-sonnet-5

폴백 이후 원인 귀속

중간 수준의 추론, 장애 로그를 근거로 설명

GPT-5.6 Sol

gpt-5.6-sol

가장 중요한 것은 두 번째 단계다. 단계 배치를 할 때 흔히 저지르는 실수는 모델도 “일단 되게 하자”는 사용자의 분위기를 따라가며 “브라우저 브리지가 가장 쉽다”는 답을 내놓는 것이다. 이는 장기 비용을 계산한 결과가 아니라 사용자의 말을 되비추는 것에 불과하다. 추론 역량이 강한 모델은 반대로 말한다. “이 구간은 표준 해시와 상수 하나의 변형에 불과하므로, 하루를 들여 순수 함수로 만드는 편이 낫고 브리지에 올리면 안 된다.” 내 말을 믿을 필요는 없다. 직접 시험해 보면 된다. 로직 3개를 고르되, 최소 1개는 올바른 배치를 이미 알고 있는 사례를 대조군으로 둔다. 같은 “배치를 제안하고 근거를 논증하며, 너무 이르게 브라우저 브리지로 보내려는 선택에는 반론을 제기하라” 프롬프트를 claude-opus-5와 gpt-5.6-sol에 넣고 한 가지만 보자. 모델이 한 단계 위로 끌어올리려 하는가, 아니면 게으르게 세 번째 단계를 기본값으로 삼는가.

진짜 장벽은 모델 전환 비용이다

서로 다른 벤더의 네 가지 모델 계층을 쓰려면 SDK도 세 개, 인증 방식도 세 개, 오류 형식도 세 개가 된다. 단계마다 모델을 바꾸려고 클라이언트를 다시 쓰는 일은 수지가 맞지 않는다. 그래서 대부분은 모든 작업에 모델 하나만 쓰고, 정작 가장 강한 추론이 필요한 배치 판단에서는 사용자의 말만 따라 하는 모델을 쓰게 된다.

AIReiter는 이 레이어를 하나로 평탄화한다. 키 하나, OpenAI 호환 인터페이스 하나로 네 가지 계층을 모두 쓸 수 있으며, 요청 본문의 model 필드만 바꾸면 전환된다.

# Placement argument: the reasoning tier
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": "<placement prompt + the reversed logic fragment + dependency list>"}]
  }'

# Bulk first-pass triage: change one field
#   "model": "claude-sonnet-5"
# Degradation attribution:
#   "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 계층 역시 같은 키로 이용할 수 있다.

  • API 키 받기

  • 가입 없이 사용해 보기 — 먼저 로직 조각 몇 개를 직접 넣어보고, 두 모델이 사용자의 말을 되풀이하는지 아니면 “이걸 브라우저 브리지에 올려야 하는가”라는 판단에 반론을 제기하는지 확인해 보자.

마무리

역공학한 로직을 어디에 안착시킬지는 기술 문제가 아니라 비용 문제다. 네이티브 재작성 > 로컬 JS 엔진 > 수동형 브라우저 브리지라는 3단계 순서는 뒤집을 수 없다. 한 단계 내려갈 때마다 순수 함수는 외부 의존성, 사람의 개입, 살아 있는 탭을 요구하는 프로세스로 바뀌기 때문이다. 수동형 브리지가 금지 구역인 것은 아니다. 다만 엄격한 경계를 둔 임시 구성 요소여야 한다. 한 번만 읽고 절대 개입하지 않을 것, 페이지가 없으면 즉시 명시적인 오류를 낼 것, 민감한 상태를 브라우저 밖으로 내보내지 않을 것, 로그인은 명시적 핸드오프로만 처리할 것, 단계 상태를 언제나 읽을 수 있게 할 것. 이 원칙을 지키면 믿을 만한 임시방편이 되지만, 하나라도 빠지면 누구도 유지보수하고 싶어 하지 않는 블랙박스가 된다. 모델은 기능을 어느 단계에 둘지 판단하고 “일단 되게 하자”는 관성을 견제하는 데 도움을 준다. 반면 각 재작성의 정합성을 판정하는 기준은 모델이 아니라 차분 테스트다.