파이썬과 레지덴셜 프록시로 구글 랭크 트래커 구축하기

curl_cffi와 ProxyHat 레지덴셜 프록시로 프로덕션 준비 완료된 구글 랭크 트래커를 구축하는 방법을 코드 예제와 함께 단계별로 설명합니다.

Build a Google Rank Tracker in Python with Residential Proxies
이 글의 목차

파이썬과 레지덴셜 프록시로 구글 랭크 트래커 구축하기: 개요

SEO 엔지니어와 파이썬 개발자에게 순위 추적은 일상적이지만 까다로운 작업이다. 상용 랭크 트래커는 키워드당 월 $0.50~$2를 부과하고, 원시 데이터 접근을 제한하며, 커스텀 지표 추가를 허용하지 않는 경우가 많다. 파이썬과 레지덴셜 프록시로 구글 랭크 트래커를 구축하면 전체 제어권을 확보하고 비용을 90% 이상 절감할 수 있다.

이 가이드는 2026년 현재 구글 SERP 구조에 맞춘 프로덕션 준비 완료된 랭크 트래커를 구축하는 전체 과정을 다룬다. 핵심은 세 가지다: start=0,10,20 페이지네이션으로 상위 100개 결과 수집, curl_cffi로 Chrome TLS 지문 모방, ProxyHat 레지덴셜 프록시로 도시 수준 지역 타겟팅과 키워드별 고정 세션 적용. 이 조합이 없으면 구글의 봇 탐지가 평균 200ms 이내에 요청을 차단한다.

다음 섹션에서 데이터 모델부터 프로덕션 배포까지 단계별로 살펴본다. 모든 코드는 직접 실행할 수 있으며, ProxyHat 게이트웨이(gate.proxyhat.com:8080)를 사용한다. SERP 스크래핑 파이썬 2026 환경에 최적화된 코드를 제공한다.

데이터 모델 — 키워드, 타겟 도메인, 순위, 그리고 일일 스냅샷의 가치

랭크 트래커의 핵심은 단순한 데이터 모델에서 출발한다. 다음 필드가 최소 요구 사항이다:

  • keyword — 추적할 검색어 (예: "best running shoes")
  • target_domain — 순위를 확인할 도메인 (예: "nike.com")
  • country — 검색 지역 (예: "US", "DE", "JP")
  • device — desktop 또는 mobile
  • position — 자연 검색 결과 내 순위 (1~100)
  • captured_at — UTC 타임스탬프

일일 스냅샷이 일회성 체크보다 나은 이유

일회성 순위 체크는 특정 시점의 스냅샷만 제공한다. 하지만 구글의 순위는 하루에도 여러 번 변동한다. 일일 스냅샷을 저장하면 다음을 할 수 있다:

  • 순위 변동성 패턴 파악 — 주말 vs 평일, 알고리즘 업데이트 전후 비교
  • 7일/30일 이동 평균으로 노이즈 제거 — 하루 순위가 ±3위 변동해도 트렌드는 안정적
  • 알고리즘 업데이트 감지 — 여러 키워드의 순위가 동시에 급변하면 코어 업데이트 신호

SQLite는 키워드 1,000개 × 일일 스냅샷 기준으로 1년에 약 365,000행을 생성하지만, 디스크 사용량은 50 MB 미만이다. CSV 백업을 병행하면 데이터 이식성도 확보된다.

SERP 가져오기 — num=100 폐지 후의 페이지네이션 전략

2025년 9월, 구글은 num=100 파라미터를 공식적으로 폐지했다. 이제 num 값과 무관하게 페이지당 10개 결과만 반환한다. 상위 100개 결과를 수집하려면 start=0,10,20...90으로 10페이지를 순회해야 한다.

Google Search Central 문서에 따르면, 자연 검색 결과는 광고, 지역 팩, 추천 스니펫과 구분되어야 한다. 랭크 트래커는 자연 결과의 순위만 추적해야 한다.

페이지네이션 구현

각 페이지는 독립적인 HTTP 요청이 필요하다. 10페이지 = 10개 요청이므로, 프록시 회전과 지연이 필수적이다. 다음은 기본 curl 예제다:

curl -x "http://user-country-US-city-chicago-session-mykw123:pass@gate.proxyhat.com:8080" \
  "https://www.google.com/search?q=best+running+shoes&start=0&gl=us&hl=en" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"

start 값을 0, 10, 20...으로 변경하면서 각 페이지의 HTML을 수집한다. 각 요청마다 새 세션 ID를 사용하면 IP가 회전되지만, 같은 키워드 내에서는 고정 세션을 유지하는 것이 안정적이다.

레지덴셜 프록시가 필수인 이유 — TLS 지문과 IP 평판

구글은 단순히 IP 주소만 보지 않는다. curl_cffi 프로젝트가 입증하듯, 구글은 TLS 핸드셰이크의 JA3/JA4 지문을 분석하여 비브라우저 클라이언트를 탐지한다. 표준 requests 라이브러리의 TLS 지문은 Python urllib3의 것이어서 즉시 차단된다.

이중 방어선이 필요하다:

  1. TLS 지문 모방curl_cffiimpersonate="chrome" 옵션으로 실제 Chrome 브라우저의 TLS 핸드셰이크를 재현
  2. IP 평판 우회 — 레지덴셜 프록시로 실제 ISP IP를 사용. 데이터센터 IP는 구글의 IP 평판 데이터베이스에서 "known bot"으로 분류되어 99% 차단률을 보인다.

프록시 유형 비교

프록시 유형TLS 지문 통과IP 평판지역 타겟팅추천 용도
데이터센터불가 (IP만으로 차단)낮음 (봇으로 분류)국가 수준비추천
레지덴셜curl_cffi와 조합 시 가능높음 (실제 ISP)도시 수준랭크 트래킹 최적
모바일curl_cffi와 조합 시 가능최고 (4G/5G IP)국가 수준모바일 순위 추적

ProxyHat 레지덴셜 프록시는 사용자 이름에 지역과 세션을 지정할 수 있다. 예: user-country-US-city-chicago-session-mykw123:pass. 이렇게 하면 시카고 ISP IP로 고정되어, 같은 키워드의 순위가 지역별로 다를 때 정확한 데이터를 얻을 수 있다. 지원 지역 목록에서 전체 국가/도시 코드를 확인할 수 있다.

curl_cffi와 ProxyHat으로 구글 랭크 트래커 프록시 구현하기

이제 실제 구현에 들어간다. curl_cffi로 Chrome TLS 지문을 모방하고, ProxyHat 레지덴셜 프록시로 요청을 보내며, 결과를 파싱하여 SQLite에 저장한다. 파이썬 랭크 트래커 구축의 핵심은 사용자 이름 파라미터로 프록시를 제어하는 것이다.

1단계: 기본 SERP 가져오기

from curl_cffi import requests as cffi_requests

proxy_url = "http://user-country-US-city-chicago-session-mykw123:pass@gate.proxyhat.com:8080"

resp = cffi_requests.get(
    "https://www.google.com/search?q=best+running+shoes&start=0&gl=us&hl=en",
    proxies={"https": proxy_url},
    impersonate="chrome",
    timeout=30,
)
print(f"Status: {resp.status_code}, Length: {len(resp.text)}")

impersonate="chrome"이 핵심이다. 이 한 줄로 TLS 지문이 Chrome과 일치하여, 구글의 JA3/JA4 탐지를 우회한다. timeout=30초는 프록시 지연을 고려한 여유 값이다.

2단계: 자연 검색 결과 파싱

import re

def parse_organic_results(html):
    """HTML에서 자연 검색 결과 URL을 추출한다."""
    pattern = r'<a href="/url\?q=(https?://[^&"]+)&[^"]*"'
    urls = re.findall(pattern, html)
    seen = set()
    results = []
    for u in urls:
        if u not in seen:
            seen.add(u)
            results.append(u)
    return results

def find_position(results, target_domain):
    """타겟 도메인의 순위를 찾는다."""
    for i, url in enumerate(results, 1):
        if target_domain in url:
            return i
    return None

구글의 HTML 구조는 자주 변경되므로, 정규식 기반 파싱은 보수적으로 작성해야 한다. CSS 선택자(BeautifulSoup)를 병행하면 더 견고하지만, /url?q= 리다이렉트 패턴은 안정적으로 유지되고 있다.

3단계: 상위 100개 수집 및 SQLite 저장

import sqlite3
from datetime import datetime, timezone

DB_PATH = "rank_history.db"

def init_db():
    conn = sqlite3.connect(DB_PATH)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS rankings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            keyword TEXT NOT NULL,
            target_domain TEXT NOT NULL,
            country TEXT NOT NULL,
            device TEXT NOT NULL,
            position INTEGER,
            captured_at TEXT NOT NULL
        )
    """)
    conn.commit()
    return conn

def track_keyword(keyword, target_domain, country="US", city="chicago"):
    conn = init_db()
    all_results = []
    session_id = keyword.replace(" ", "-")

    for start in [0, 10, 20]:
        proxy_url = f"http://user-country-{country}-city-{city}-session-{session_id}:pass@gate.proxyhat.com:8080"
        url = f"https://www.google.com/search?q={keyword}&start={start}&gl={country.lower()}&hl=en"

        resp = cffi_requests.get(
            url,
            proxies={"https": proxy_url},
            impersonate="chrome",
            timeout=30,
        )
        resp.raise_for_status()
        all_results.extend(parse_organic_results(resp.text))

    position = find_position(all_results, target_domain)
    captured_at = datetime.now(timezone.utc).isoformat()

    conn.execute(
        "INSERT INTO rankings (keyword, target_domain, country, device, position, captured_at) "
        "VALUES (?, ?, ?, ?, ?, ?)",
        (keyword, target_domain, country, "desktop", position, captured_at)
    )
    conn.commit()
    conn.close()
    print(f"[{keyword}] {target_domain} -> position {position}")
    return position

track_keyword("best running shoes", "nike.com")

이 코드는 3페이지(상위 30개)를 수집한다. 상위 100개가 필요하면 [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]으로 확장한다. 각 요청마다 2~5초 지연을 추가하는 것이 안전하다.

프로덕션 강화 — 재시도, CAPTCHA 감지, 동시성 제어

프로덕션 환경에서는 네트워크 오류, CAPTCHA, 속도 제한이 일어난다. 다음 패턴들로 이를 처리한다.

지수 백오프 재시도와 CAPTCHA 감지

import time
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rank_tracker")

CAPTCHA_INDICATORS = ["unusual traffic", "captcha", "detected", "verify"]

def detect_captcha(html):
    html_lower = html.lower()
    return any(ind in html_lower for ind in CAPTCHA_INDICATORS)

def fetch_serp_safe(keyword, country="US", city="chicago", start=0, max_retries=3):
    session_id = keyword.replace(" ", "-")
    proxy_url = f"http://user-country-{country}-city-{city}-session-{session_id}:pass@gate.proxyhat.com:8080"
    url = f"https://www.google.com/search?q={keyword}&start={start}&gl={country.lower()}&hl=en"

    for attempt in range(max_retries):
        try:
            resp = cffi_requests.get(
                url,
                proxies={"https": proxy_url},
                impersonate="chrome",
                timeout=30,
            )
            if detect_captcha(resp.text):
                logger.warning(f"CAPTCHA detected on attempt {attempt+1}")
                session_id = f"{session_id}-r{attempt}"
                proxy_url = f"http://user-country-{country}-city-{city}-session-{session_id}:pass@gate.proxyhat.com:8080"
                time.sleep(2 ** attempt)
                continue

            resp.raise_for_status()
            return resp.text
        except Exception as e:
            delay = 2 ** attempt
            logger.warning(f"Attempt {attempt+1}/{max_retries} failed: {e}. Retrying in {delay}s")
            if attempt < max_retries - 1:
                time.sleep(delay)
            else:
                raise
    return None

CAPTCHA가 감지되면 세션 ID를 변경하여 새 IP를 할당받고 지수 백오프로 대기한다. 이 패턴은 CAPTCHA가 연속으로 발생해도 최대 3회 재시도하며, 4초, 8초 지연을 적용한다.

동시성 제어

from concurrent.futures import ThreadPoolExecutor, as_completed

def track_multiple_keywords(keywords, target_domain, country="US", city="chicago", max_workers=5):
    results = {}
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {
            executor.submit(track_keyword, kw, target_domain, country, city): kw
            for kw in keywords
        }
        for future in as_completed(futures):
            kw = futures[future]
            try:
                pos = future.result()
                results[kw] = pos
            except Exception as e:
                logger.error(f"Failed to track '{kw}': {e}")
                results[kw] = None
    return results

keywords = ["best running shoes", "running shoes review", "marathon shoes"]
results = track_multiple_keywords(keywords, "nike.com", max_workers=5)
for kw, pos in results.items():
    print(f"{kw}: position {pos}")

max_workers=5는 보수적인 값이다. ProxyHat 레지덴셜 프록시는 100개 이상의 동시 세션을 지원하지만, 구글의 속도 제한을 피하려면 5~10개 동시 요청이 안전하다. ProxyHat 요금제에서 동시 세션 한도를 확인할 수 있다.

순위 변동성 스무딩

import csv

def export_csv(keyword, output_file="rank_history.csv"):
    conn = sqlite3.connect(DB_PATH)
    rows = conn.execute(
        "SELECT captured_at, position FROM rankings WHERE keyword = ? ORDER BY captured_at",
        (keyword,)
    ).fetchall()
    conn.close()

    with open(output_file, "w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["captured_at", "position", "7day_ma"])
        positions = [r[1] for r in rows]
        for i, (ts, pos) in enumerate(rows):
            window = positions[max(0, i-6):i+1]
            ma = sum(window) / len(window) if window else None
            writer.writerow([ts, pos, f"{ma:.1f}" if ma else ""])
    print(f"Exported {len(rows)} rows to {output_file}")

7일 이동 평균은 일일 노이즈를 제거하여 실제 트렌드를 보여준다. 순위가 일시적으로 5위에서 8위로 떨어져도 7일 평균이 6.2위로 안정적이라면 알고리즘 페널티가 아닌 자연스러운 변동으로 판단할 수 있다.

윤리와 한계 — 책임감 있는 랭크 추적

구글 랭크 트래킹은 구글의 서비스 약관과 충돌할 수 있다. 다음 원칙을 지킨다:

  • 본인 소유 도메인 추적 — 자신의 웹사이트 순위를 모니터링하는 것이 가장 안전하다.
  • 공개 순위만 추적 — 로그인이 필요한 개인화된 결과는 수집하지 않는다.
  • 속도 제한 준수 — 키워드당 10페이지 요청 시 2~5초 간격을 유지한다. 하루 1,000키워드 = 약 10,000 요청이며, 이는 합리적인 범위다.
  • 저용량에서는 공식 API 사용Google Custom Search API는 하루 100건 무료 쿼리를 제공한다. 소규모 추적에는 API가 더 안정적이다.
  • robots.txt 확인 — 구글의 robots.txt는 검색 결과 페이지 크롤링을 허용하지만, 과도한 자동화는 권장하지 않는다.

대규모 추적이 필요하면 SERP 추적 유스케이스에서 ProxyHat의 권장 설정을 참고하고, 웹 스크래핑 가이드에서 일반적인 스크래핑 모범 사례를 확인할 수 있다. 자세한 프록시 설정은 ProxyHat 문서를 참조한다.

핵심 요약

파이썬으로 구글 랭크 트래커를 구축하려면:

  • curl_cffiimpersonate="chrome"으로 TLS 지문을 모방한다 — 표준 requests는 즉시 차단된다.
  • ProxyHat 레지덴셜 프록시(gate.proxyhat.com:8080)로 도시 수준 지역 타겟팅과 키워드별 고정 세션을 사용한다.
  • num=100이 폐지되었으므로 start=0,10,20...로 페이지네이션한다.
  • 일일 스냅샷을 SQLite에 저장하고 7일 이동 평균으로 순위 변동성을 스무딩한다.
  • 지수 백오프 재시도, CAPTCHA 감지, 동시성 제어(max_workers=5)로 프로덕션 안정성을 확보한다.
  • 본인 소유 도메인과 공개 순위만 추적하고, 저용량에서는 Google Custom Search API를 우선한다.

자주 묻는 질문

파이썬과 레지덴셜 프록시로 구글 랭크 트래커 구축이란 무엇인가요?

파이썬으로 구글 검색 결과를 자동 수집하여 특정 도메인의 순위를 추적하는 시스템을 구축하는 것을 의미합니다. 레지덴셜 프록시는 실제 ISP IP를 제공하여 구글의 봇 탐지를 우회하며, curl_cffi로 Chrome TLS 지문을 모방합니다. 일일 스냅샷을 SQLite에 저장하여 순위 변동성과 트렌드를 분석할 수 있습니다.

랭크 트래커에 레지덴셜 프록시가 필요한 이유는 무엇인가요?

구글은 TLS 핸드셰이크 지문(JA3/JA4)과 IP 평판을 결합하여 봇을 탐지합니다. 데이터센터 IP는 known bot으로 분류되어 99% 차단률을 보이지만, 레지덴셜 프록시는 실제 ISP IP를 사용하므로 차단 확률이 크게 낮아집니다. 도시 수준 지역 타겟팅도 가능하여 지역별 순위 차이를 정확히 추적할 수 있습니다.

구글 랭크 트래커에 어떤 프록시 유형이 가장 적합한가요?

데스크톱 순위 추적에는 레지덴셜 프록시가 최적입니다. 도시 수준 지역 타겟팅이 가능하고 IP 평판이 높기 때문입니다. 모바일 순위 추적에는 모바일 프록시(4G/5G IP)가 더 적합합니다. 데이터센터 프록시는 구글의 IP 평판 시스템에서 즉시 차단되므로 랭크 트래킹에 부적합합니다.

구글 랭크 트래커 구축 시 차단을 피하려면 어떻게 해야 하나요?

세 가지를 결합해야 합니다: curl_cffi의 impersonate=chrome으로 TLS 지문 모방, ProxyHat 레지덴셜 프록시로 도시 수준 지역 타겟팅, 키워드별 고정 세션으로 IP 일관성 유지. 추가로 요청 간 2~5초 지연, 지수 백오프 재시도, CAPTCHA 감지 로직을 구현하고 동시 요청을 5~10개로 제한합니다.

시작할 준비가 되셨나요?

148개국 이상의 주거용, ISP, 모바일 프록시. 무료 계정을 만드세요.

무료 계정 만들기
← 블로그로 돌아가기