프록시를 활용한 아마존 키워드 순위 추적 개발자 가이드

아마존 검색 결과는 자체적인 SERP 알고리즘을 따릅니다. 레지덴셜 프록시와 Python을 활용해 키워드별 유기적 순위를 추적하고, 스폰서드 광고를 분리하며, 차단을 회피하는 방법을 코드 중심으로 설명합니다.

Amazon Keyword Rank Tracking with Proxies: A Developer's Guide
법적 안내: 본 가이드는 공개된 검색 결과 페이지에서 공개 데이터만 수집하는 것을 전제로 합니다. 미국에서는 CFAA(Computer Fraud and Abuse Act)가 무단 접근을 제한하며, EU에서는 GDPR이 개인정보 처리를 규율합니다. 본인 소유 리스팅 또는 공개 정보만 추적하고, robots.txt와 아마존 이용약관을 준수하며, 상업적 이용이 필요한 경우 Amazon SP-API를 우선 검토하세요.

아마존 셀러와 마켓플레이스 SEO 개발자에게 아마존 키워드 순위 추적은 유기적 트래픽을 좌우하는 핵심 작업입니다. 구글과 달리 아마존 검색은 판매 속도(sales velocity), 전환율, 재고 상태, 광고 입찰이 순위에 직접 영향을 미치며, 결과는 국가와 우편번호마다 다르게 현지화됩니다. 이 가이드는 프록시를 활용한 아마존 키워드 순위 추적을 코드 중심으로 설명합니다. Python 예제, 레지덴셜 프록시 설정, 스폰서드 분리, CAPTCHA 회피 패턴을 모두 다룹니다.

프록시를 활용한 아마존 키워드 순위 추적: 왜 필요한가

아마존의 검색 결과 페이지는 구글 SERP와 근본적으로 다릅니다. 구글이 링크 기반 권위도(PageRank)와 콘텐츠 관련성을 중시한다면, 아마존은 판매 전환 데이터를 순위 신호로 직접 사용합니다. 즉, 같은 키워드에서도 ASIN의 최근 판매 속도와 광고 입찰이 순위를 크게 바꿉니다. 따라서 셀러는 키워드·마켓플레이스·ASIN 조합별로 유기적 순위를 지속 추적해야 변화를 감지할 수 있습니다.

더 복잡한 점은 현지화입니다. amazon.com의 결과는 미국 우편번호에 따라 달라지고, amazon.de는 독일 배지 주소를 기준으로 재정렬됩니다. 데이터센터 IP로 요청하면 아마존은 요청을 비정상 트래픽으로 분류해 CAPTCHA를 띄우거나 IP를 차단합니다. 실제 사용자가 보는 순위를 재현하려면 국가 지오타겟팅이 가능한 레지덴셜 프록시가 필수입니다.

아마존 검색 결과 페이지 구조 이해

아마존 검색 결과는 [data-component-type="s-search-result"] 컴포넌트로 구성된 카드 그리드로 렌더링됩니다. 각 카드는 data-asin 속성으로 ASIN을 식별하고, 광고 배치는 카드 내부에 Sponsored 라벨이 포함된 요소로 표시됩니다. 유기적 순위를 계산하려면 광고 카드를 제외한 뒤 DOM 순서대로 위치를 매겨야 합니다.

아마존 SERP 스크래핑의 기술적 맥락

아마존은 AWS WAF와 자체 anti-bot 계층을 결합해 봇 트래픽을 차단합니다. 주요 방어 수단은 세 가지입니다.

  • TLS 지문 검사: Python requests의 기본 TLS 핸드셰이크는 브라우저와 달라 차단 대상이 됩니다.
  • 요청 빈도 제한: 동일 IP에서 짧은 시간에 수십 건의 검색 요청이 발생하면 CAPTCHA가 트리거됩니다.
  • 지역 현지화: IP의 국가와 우편번호가 결과 순서에 직접 반영됩니다.

이 때문에 아마존 SERP 스크래핑에서는 브라우저와 유사한 TLS 지문을 내는 클라이언트(curl_cffi, Playwright)와 레지덴셜 프록시 조합이 사실상 표준입니다. 데이터센터 프록시는 속도는 빠르지만 아마존의 IP 평판 검사를 통과하기 어렵고, 50~200ms의 지연을 감수하더라도 레지덴셜 IP가 성공률이 월등히 높습니다.

프록시 유형 비교: 아마존 순위 추적에 무엇이 적합한가

프록시 유형성공률지연비용아마존 추적 적합성
데이터센터낮음 (빠른 차단)~50ms낮음권장하지 않음
레지덴셜높음200~800ms중간권장
모바일매우 높음300~1200ms높음고신뢰 시나리오

대부분의 셀러는 레지덴셜 프록시로 비용과 성공률의 균형을 잡습니다. ProxyHat의 프록시 로케이션 페이지에서 지원 국가 목록을 확인할 수 있습니다.

ProxyHat 레지덴셜 프록시 설정

ProxyHat 게이트웨이는 사용자 이름 플래그로 지오타겟팅과 세션 제어를 처리합니다. HTTP 기본 포트는 8080, SOCKS5는 1080입니다.

# amazon.com - 미국 레지덴셜, 스티키 세션http://user-country-US-session-amz-keyword-001:pass@gate.proxyhat.com:8080# amazon.de - 독일 레지덴셜http://user-country-DE-session-amz-de-001:pass@gate.proxyhat.com:8080# SOCKS5 (TLS 지분이 중요할 때)socks5://user-country-US-session-amz-sock-001:pass@gate.proxyhat.com:1080

스티키 세션은 동일 키워드의 페이지 1~5를 동일 IP로 순회할 때 필수입니다. 페이지마다 IP가 바뀌면 아마존이 세션을 끊거나 결과 일관성이 깨집니다. 키워드 추적 한 사이클이 끝나면 세션 ID를 교체해 새 IP로 로테이션합니다.

Python으로 아마존 키워드 순위 추적 구현하기

이제 curl_cffi와 Playwright 두 가지 접근을 보여줍니다. curl_cffi는 Chrome의 TLS 지문을 흉내 내는 경량 HTTP 클라이언트이고, Playwright는 실제 브라우저를 구동해 JavaScript 렌더링까지 처리합니다.

예제 1: curl_cffi로 SERP 수집 및 순위 계산

import reimport timeimport jsonimport uuidfrom datetime import datetime, timezonefrom curl_cffi import requests as cffi_requestsfrom bs4 import BeautifulSoupPROXY = "http://user-country-US-session-{sid}:pass@gate.proxyhat.com:8080"MARKETPLACES = {    "US": "https://www.amazon.com/s?k={kw}&page={page}",    "DE": "https://www.amazon.de/s?k={kw}&page={page}",}USER_AGENT = (    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "    "AppleWebKit/537.36 (KHTML, like Gecko) "    "Chrome/124.0.0.0 Safari/537.36")def fetch_page(url: str, session_id: str, max_retries: int = 3) -> str | None:    proxy = PROXY.format(sid=session_id)    for attempt in range(max_retries):        try:            resp = cffi_requests.get(                url,                proxies={"http": proxy, "https": proxy},                impersonate="chrome124",                headers={                    "User-Agent": USER_AGENT,                    "Accept-Language": "en-US,en;q=0.9",                },                timeout=20,            )            if resp.status_code == 200 and "captcha" not in resp.text.lower():                return resp.text            if resp.status_code == 503:                time.sleep(2 ** attempt)                continue            # CAPTCHA 페이지 감지            if "captcha" in resp.text.lower():                print(f"CAPTCHA detected, rotating session (attempt {attempt})")                return None        except Exception as exc:            print(f"Request error: {exc}, retrying in {2 ** attempt}s")            time.sleep(2 ** attempt)    return Nonedef parse_serp(html: str, target_asin: str) -> dict:    soup = BeautifulSoup(html, "html.parser")    cards = soup.select('[data-component-type="s-search-result"]')    organic_position = None    sponsored_positions = []    organic_count = 0    for idx, card in enumerate(cards, start=1):        asin = card.get("data-asin", "")        is_sponsored = bool(card.select_one('span:contains("Sponsored")')) or \                       bool(re.search(r"Sponsored", card.get_text()))        if is_sponsored:            sponsored_positions.append({"position": idx, "asin": asin})            continue        organic_count += 1        if asin == target_asin:            organic_position = organic_count    return {        "target_asin": target_asin,        "organic_position": organic_position,        "organic_count_seen": organic_count,        "sponsored_count": len(sponsored_positions),        "indexed": organic_position is not None,    }def track_keyword(keyword: str, target_asin: str, marketplace: str = "US", pages: int = 5) -> list[dict]:    results = []    session_id = f"amz-{uuid.uuid4().hex[:8]}"    for page in range(1, pages + 1):        url = MARKETPLACES[marketplace].format(kw=keyword.replace(" ", "+"), page=page)        html = fetch_page(url, session_id)        if not html:            # 세션 교체 후 재시도            session_id = f"amz-{uuid.uuid4().hex[:8]}"            html = fetch_page(url, session_id)        if not html:            results.append({"page": page, "error": "fetch_failed"})            break        parsed = parse_serp(html, target_asin)        parsed["page"] = page        parsed["keyword"] = keyword        parsed["marketplace"] = marketplace        parsed["timestamp"] = datetime.now(timezone.utc).isoformat()        results.append(parsed)        if parsed["organic_position"] is not None:            break  # 대상 ASIN 찾음        time.sleep(2.0)  # 페이지 간 최소 대기    return resultsif __name__ == "__main__":    history = track_keyword("wireless earbuds", "B0CX22V6ZG", marketplace="US", pages=5)    print(json.dumps(history, indent=2))

이 예제는 페이지 1~5를 순회하며 대상 ASIN의 유기적 순위를 찾고, 스폰서드 배치 수를 함께 기록합니다. impersonate="chrome124" 옵션이 TLS 지문을 Chrome 124와 일치시켜 기본 requests보다 차단 확률을 크히 낮춥니다.

예제 2: Playwright로 JavaScript 렌더링 포함 추적

import asyncioimport uuidfrom datetime import datetime, timezonefrom playwright.async_api import async_playwrightPROXY_TEMPLATE = "http://user-country-US-session-{sid}:pass@gate.proxyhat.com:8080"async def fetch_serp_playwright(keyword: str, target_asin: str, pages: int = 5):    session_id = f"pw-{uuid.uuid4().hex[:8]}"    async with async_playwright() as p:        browser = await p.chromium.launch(            headless=True,            proxy={                "server": "http://gate.proxyhat.com:8080",                "username": f"user-country-US-session-{session_id}",                "password": "pass",            },        )        context = await browser.new_context(            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "                       "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",            locale="en-US",        )        page = await context.new_page()        results = []        for pg in range(1, pages + 1):            url = f"https://www.amazon.com/s?k={keyword.replace(' ', '+')}&page={pg}"            try:                await page.goto(url, wait_until="domcontentloaded", timeout=25000)                # CAPTCHA 감지                content = await page.content()                if "captcha" in content.lower():                    session_id = f"pw-{uuid.uuid4().hex[:8]}"                    await context.close()                    context = await browser.new_context(                        user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "                                   "AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",                        locale="en-US",                        proxy={                            "server": "http://gate.proxyhat.com:8080",                            "username": f"user-country-US-session-{session_id}",                            "password": "pass",                        },                    )                    page = await context.new_page()                    continue                cards = await page.query_selector_all('[data-component-type="s-search-result"]')                organic_pos = None                organic_count = 0                for card in cards:                    asin = await card.get_attribute("data-asin")                    text = (await card.inner_text()) or ""                    if "Sponsored" in text:                        continue                    organic_count += 1                    if asin == target_asin:                        organic_pos = organic_count                results.append({                    "page": pg,                    "keyword": keyword,                    "target_asin": target_asin,                    "organic_position": organic_pos,                    "organic_count_seen": organic_count,                    "timestamp": datetime.now(timezone.utc).isoformat(),                })                if organic_pos is not None:                    break                await asyncio.sleep(2.0)            except Exception as exc:                print(f"Page {pg} error: {exc}")                results.append({"page": pg, "error": str(exc)})        await browser.close()        return resultsif __name__ == "__main__":    data = asyncio.run(fetch_serp_playwright("wireless earbuds", "B0CX22V6ZG", pages=5))    print(data)

Playwright는 DOM 렌더링이 완료된 후 data-asin을 읽으므로, 지연 로딩 카드가 있는 키워드에서 더 안정적입니다. 단, 브라우저 오버헤드로 인해 페이지당 3~5초가 추가됩니다.

예제 3: Node.js로 동일한 추적 수행

import https from 'node:https';import { HttpsProxyAgent } from 'https-proxy-agent';import * as cheerio from 'cheerio';const PROXY = (sid) => `http://user-country-US-session-${sid}:pass@gate.proxyhat.com:8080`;function fetchPage(url, sessionId) {  return new Promise((resolve, reject) => {    const agent = new HttpsProxyAgent(PROXY(sessionId));    https.get(url, { agent, headers: {      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36',      'Accept-Language': 'en-US,en;q=0.9',    }}, (res) => {      let body = '';      res.on('data', (c) => body += c);      res.on('end', () => resolve(body));    }).on('error', reject);  });}function parseSerp(html, targetAsin) {  const $ = cheerio.load(html);  let organicPos = null, organicCount = 0, sponsored = 0;  $('[data-component-type="s-search-result"]').each((_, el) => {    const asin = $(el).attr('data-asin');    const text = $(el).text();    if (text.includes('Sponsored')) { sponsored++; return; }    organicCount++;    if (asin === targetAsin) organicPos = organicCount;  });  return { targetAsin, organicPosition: organicPos, organicCount, sponsoredCount: sponsored };}async function track(keyword, targetAsin, pages = 5) {  const sid = `node-${Date.now()}`;  const out = [];  for (let p = 1; p <= pages; p++) {    const url = `https://www.amazon.com/s?k=${encodeURIComponent(keyword)}&page=${p}`;    try {      const html = await fetchPage(url, sid);      if (html.toLowerCase().includes('captcha')) {        out.push({ page: p, error: 'captcha' });        break;      }      const parsed = parseSerp(html, targetAsin);      out.push({ ...parsed, page: p, keyword, timestamp: new Date().toISOString() });      if (parsed.organicPosition !== null) break;      await new Promise((r) => setTimeout(r, 2000));    } catch (e) {      out.push({ page: p, error: e.message });    }  }  return out;}track('wireless earbuds', 'B0CX22V6ZG').then(console.log);

예제 4: curl 원라이너로 빠른 디버깅

curl -x "http://user-country-US-session-test01:pass@gate.proxyhat.com:8080" \  -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36" \  -H "Accept-Language: en-US,en;q=0.9" \  "https://www.amazon.com/s?k=wireless+earbuds&page=1" \  -o serp_us.html

새 프록시 세션이 제대로 작동하는지 확인할 때 가장 빠른 방법입니다. 반환된 HTML에 data-component-type="s-search-result"가 포함되어 있으면 정상, captcha 문자열이 있으면 세션 교체가 필요합니다.

예제 5: 순위 이력 저장 및 인덱싱 체크

import sqlite3from datetime import datetime, timezoneDB_PATH = "rank_history.db"def init_db():    conn = sqlite3.connect(DB_PATH)    conn.execute("""        CREATE TABLE IF NOT EXISTS rank_history (            id INTEGER PRIMARY KEY AUTOINCREMENT,            keyword TEXT NOT NULL,            asin TEXT NOT NULL,            marketplace TEXT NOT NULL,            organic_position INTEGER,            page INTEGER,            sponsored_count INTEGER,            indexed INTEGER,            captured_at TEXT NOT NULL        )    """)    conn.commit()    return conndef save_result(conn, result: dict):    conn.execute("""        INSERT INTO rank_history        (keyword, asin, marketplace, organic_position, page, sponsored_count, indexed, captured_at)        VALUES (?, ?, ?, ?, ?, ?, ?, ?)    """, (        result.get("keyword"),        result.get("target_asin"),        result.get("marketplace", "US"),        result.get("organic_position"),        result.get("page"),        result.get("sponsored_count", 0),        1 if result.get("organic_position") is not None else 0,        result.get("timestamp", datetime.now(timezone.utc).isoformat()),    ))    conn.commit()def is_indexed(conn, keyword: str, asin: str, marketplace: str = "US", days: int = 7) -> bool:    """최근 N일간 한 번이라도 순위가 관측되었는지 확인 (인덱싱 체크)."""    cur = conn.execute("""        SELECT COUNT(*) FROM rank_history        WHERE keyword=? AND asin=? AND marketplace=? AND indexed=1          AND captured_at >= datetime('now', ?)    """, (keyword, asin, marketplace, f"-{days} days"))    return cur.fetchone()[0] > 0

is_indexed() 함수는 ASIN이 특정 키워드에서 전혀 순위에 노출되지 않는지(즉, 인덱싱 또는 랭킹 문제)를 진단합니다. 7일간 0회 관측이면 리스팅 최적화나 인덱싱 문제를 의심해야 합니다.

프로덕션 팁: 일일 스케줄링과 안정성

1. 일일 스케줄링

아마존 순위는 하루 단위로 변동이 의미 있으므로, 셀러마다 24시간 주기 추적이 일반적입니다. APScheduler 또는 cron으로 매일 새벽(트래픽이 적은 시간대)에 실행하세요.

from apscheduler.schedulers.blocking import BlockingSchedulerdef daily_job():    keywords = load_keywords()    for kw in keywords:        for asin in kw["asins"]:            results = track_keyword(kw["term"], asin, marketplace=kw["marketplace"])            for r in results:                save_result(conn, r)scheduler = BlockingScheduler()scheduler.add_job(daily_job, "cron", hour=4, minute=30)scheduler.start()

2. 재시도와 백오프

지수 백오프는 기본이지만, 서킷 브레이커 패턴을 추가하면 연속 실패 시 전체 작업을 중단해 IP 풀을 보호할 수 있습니다. 예를 들어 5회 연속 CAPTCHA가 발생하면 30분간 해당 마켓플레이스 추적을 일시 정지하세요.

3. CAPTCHA 감지

아마존 CAPTCHA 페이지는 URL에 /errors/validateCaptcha가 포함되거나, 본문에 captcha 문자열이 있습니다. 감지 즉시 세션 ID를 교체하고, 동일 키워드 재시도 전 최소 30초 대기하세요.

4. 동시성 제한

한 키워드의 페이지 1~5는 직렬로 요청하고, 키워드 간에만 동시성을 적용하세요. asyncio.Semaphore(5) 정도가 안정적입니다. 과도한 동시 요청(예: 100 concurrent sessions)은 프록시 공급자 비용과 차단 위험을 동시에 키웁니다.

5. 인덱싱 체크

순위가 5페이지 밖에 있으면 track_keywordorganic_position: None을 반환합니다. 이 값이 며칠간 지속되면 (a) ASIN이 해당 키워드에 인덱싱되지 않았거나, (b) 랭킹이 50위 밖으로 빠졌거나, (c) 리스팅이 일시 정지 상태일 수 있습니다. Amazon Seller Central의 검색 템플릿 리포트와 교차 검증하세요.

윤리와 대안: SP-API

아마존은 자사 셀러를 위해 Amazon SP-API(Selling Partner API)를 제공합니다. 본인 소유 ASIN의 카탈로그, 리포트, 랭킹 관련 데이터를 정식으로 가져올 수 있으므로, 셀러 본인 리스팅 추적이라면 SP-API가 가장 안전한 경로입니다. 다만 SP-API는 경쟁사 ASIN 순위나 타 셀러 키워드 분포를 제공하지 않으므로, 공개 SERP 데이터가 필요한 부분만 프록시 기반 추적을 보완재로 사용하는 것이 실무적입니다.

요청 속도는 공개 데이터라도 자제하세요. 키워드당 하루 1~2회, 페이지당 2초 이상 간격이면 아마존 인프라에 유의미한 부하를 주지 않습니다. 웹 스크래핑 사용 사례SERP 추적 사용 사례에서 추가 가이드를 확인할 수 있고, 프록시 가격 페이지에서 레지덴셜 요금제를 비교하세요.

핵심 요약

  • 아마존 SERP는 판매 속도와 광고 입찰이 순위를 좌우하므로, 키워드·마켓·ASIN별 유기적 순위 추적이 필수다.
  • [data-component-type="s-search-result"]data-asin으로 카드를 식별하고, Sponsored 라벨로 광고를 분리해 유기적 순위를 계산한다.
  • 데이터센터 IP는 빠르게 차단되므로, 국가 지오타겟팅과 스티키 세션을 지원하는 레지덴셜 프록시가 기본 선택이다.
  • curl_cffi의 impersonate 또는 Playwright로 TLS 지문을 브라우저와 일치시키면 차단 확률이 크게 낮아진다.
  • 일일 스케줄, 지수 백오프, CAPTCHA 감지, 인덱싱 체크를 갖추면 프로덕션 등급 추적 시스템이 완성된다.
  • 본인 소유 리스팅은 SP-API를 우선하고, 공개 데이터 추적은 속도 제한과 윤리를 지킨다.

자주 묻는 질문

프록시를 활용한 아마존 키워드 순위 추적이란 무엇인가요?

레지덴셜 또는 모바일 프록시를 통해 아마존 검색 결과 페이지를 수집하고, 특정 키워드에서 대상 ASIN의 유기적 순위를 계산하는 작업입니다. 스폰서드 광고와 유기적 결과를 분리하고, 마켓플레이스별 국가 지오타겟팅을 적용해 현지 사용자가 보는 순위를 재현합니다.

아마존 키워드 순위 추적에 프록시가 왜 필요한가요?

아마존은 반복 요청에 대해 CAPTCHA와 IP 차단을 적용하며, 검색 결과는 국가와 우편번호에 따라 현지화됩니다. 데이터센터 IP는 빠르게 차단되므로, 레지덴셜 프록시로 국가를 지정하고 스티키 세션으로 페이지네이션을 안정화해야 정확한 순위 데이터를 얻을 수 있습니다.

아마존 키워드 순위 추적에 어떤 프록시 유형이 가장 적합한가요?

레지덴셜 프록시가 가장 안정적입니다. 모바일 프록시는 신뢰도가 더 높지만 비용이 큽니다. 데이터센터 프록시는 빠르지만 아마존의 anti-bot에 쉽게 노출되어 키워드 순위 추적용으로는 권장하지 않습니다. 국가 지오타겟팅과 스티키 세션을 지원하는 것이 핵심입니다.

아마존 키워드 순위 추적 시 차단을 피하려면 어떻게 해야 하나요?

요청 속도를 제한하고, 지수 백오프 재시도를 적용하며, CAPTCHA 응답을 감지해 세션을 교체하세요. TLS 지문을 브라우저와 일치시키기 위해 curl_cffi나 Playwright를 사용하고, 동일 IP로 페이지 1~5를 연속 요청할 때는 스티키 세션을 유지한 뒤 세션을 로테이션하는 패턴이 효과적입니다.

ProxyHat 레지덴셜 프록시로 아마존 키워드 순위 추적 시스템을 구축할 준비가 되었다면 ProxyHat 문서에서 인증, 지오타겟팅, 세션 제어의 전체 파라미터를 확인하세요.

시작할 준비가 되셨나요?

AI 필터링으로 148개국 이상에서 5천만 개 이상의 레지덴셜 IP에 액세스하세요.

가격 보기레지덴셜 프록시
← 블로그로 돌아가기