Temu 상품 및 가격 데이터 스크래핑: API vs HTML 트레이드오프
Temu는 전 세계 수십 개국에서 폭발적으로 성장한 이커머스 플랫폼이지만, 개발자에게 공식적인 공개 API를 제공하지 않습니다. Temu 상품 및 가격 데이터 스크래핑을 시도하려는 데이터 엔지니어와 가격 인텔리전스 팀은 두 가지 경로 중 하나를 선택해야 합니다: 변동성이 높은 HTML 카드를 파싱하거나, 내부 JSON 엔드포인트를 직접 호출하는 방법입니다.
HTML 파싱 경로는 /search_result.html 같은 페이지를 렌더링한 뒤 DOM에서 상품 카드를 추출합니다. 문제는 Temu가 React/Next.js 기반 SPA이며, CSS 클래스명이 빌드마다 해시화되어 변경된다는 점입니다. ._2w9v7x 같은 클래스명은 다음 배포에서 전혀 다른 값이 됩니다. 반면, 내부 JSON 엔드포인트인 /api/poppy/v1/search와 /api/poppy/v1/goods/detail는 구조화된 데이터를 반환하므로 파싱 안정성이 훨씬 높습니다.
하지만 JSON 엔드포인트에는 더 강력한 안티봇 보호가 적용됩니다. anti_content라는 서명된 토큰 헤더가 필수이며, 이 토큰 없이 요청하면 즉시 403이 반환됩니다. 결론은: JSON 엔드포인트를 우선적으로 타겟팅하되, 안티봇 스택을 완전히 이해하고 프록시 인프라를 갖춰야 합니다.
기술적 컨텍스트: 왜 이 문제가 존재하는가
Temu는 PDD Holdings(拼多多)의 자회사로, 중국 본사의 기술 스택을 기반으로 구축되었습니다. PDD는 중국 내수 시장에서 대규모 트래픽과 봇 공격에 대응해 온 경험이 있으며, 이 방어 체계가 Temu에도 적용되었습니다. 주요 방어 계층은 다음과 같습니다:
- Cloudflare Turnstile: 페이지 진입 시 클라이언트 측 JavaScript 챌린지를 실행하여 봇을 필터링합니다. 단순 HTTP 요청으로는 통과할 수 없습니다.
- anti_content 서명 토큰: 브라우저 환경에서 수집한 fingerprint 데이터를 암호화하여 생성한 토큰입니다. 이 토큰은 요청마다 갱신되며, 서버 측에서 검증합니다.
- TLS/HTTP2 지문(Fingerprinting): TLS 1.3 핸드셰이크의 ClientHello 필드와 HTTP/2 SETTINGS 프레임을 분석하여 비정상 클라이언트를 식별합니다. Python
requests라이브러리의 기본 TLS 지문은 즉시 플래그됩니다. - IP 평판 필터링: 데이터센터 IP 대역(AWS, GCP, Azure 등)은 Cloudflare의 IP 평판 데이터베이스에서 자동으로 위험 점수가 높게 책정됩니다.
이러한 다층 방어 때문에 단순한 HTTP 클라이언트로는 Temu 데이터에 접근할 수 없습니다. 브라우저 자동화(Selenium, Playwright)를 사용하면 안티봇을 통과할 수 있지만, 속도가 느리고 비용이 높습니다. 가장 효율적인 접근법은 브라우저 TLS 지문을 모방하는 라이브러리와 주거용 프록시를 조합하는 것입니다.
Temu 안티봇 스택 상세 분석
Cloudflare Turnstile와 anti_content 토큰
Cloudflare Turnstile은 사용자에게 보이지 않는 챌린지를 실행합니다. Cloudflare 공식 문서에 따르면, Turnstile은 클라이언트 환경의 30개 이상의 신호를 수집하여 봇 여부를 판단합니다. Temu는 이를 자체 anti_content 생성 로직과 결합합니다.
anti_content 토큰은 브라우저의 Canvas 렌더링 결과, WebGL 정보, 화면 해상도, 타임스탬프, 마우스 이벤트 패턴 등을 수집하여 암호화한 문자열입니다. 이 토큰은 X-Allow-Block-Type 헤더와 함께 전송되며, 서버에서 복호화 후 검증합니다. 토큰의 유효 기간은 약 5분이며, 재사용 시 거부됩니다.
실무에서는 anti_content 토큰을 직접 생성하는 것보다, Playwright로 한 번 페이지를 로드하여 토큰을 추출한 뒤 이후 요청에 재사용하는 하이브리드 방식이 효율적입니다. 토큰 추출 후 5분 이내에 최대한 많은 요청을 처리하는 배치 전략이 필요합니다.
TLS/HTTP2 지문과 curl_cffi
Python의 requests 라이브러리는 OpenSSL 기반의 TLS 핸드셰이크를 사용하지만, 실제 Chrome 브라우저는 BoringSSL을 사용합니다. 이 차이로 인해 JA3/JA4 지문이 달라져 봇으로 감지됩니다. curl_cffi 라이브러리는 libcurl-impersonate를 기반으로 Chrome의 TLS 핸드셰이크를 모방하므로, 이 문제를 해결할 수 있습니다.
또한 HTTP/2의 SETTINGS 프레임 순서, WINDOW_UPDATE 값, HEADERS 프레임의 의사 헤더 순서도 봇 탐지에 사용됩니다. curl_cffi는 이러한 HTTP/2 지문까지 모방하므로, Temu 스크래핑에 적합한 도구입니다.
데이터 위치 파악: 내장 상태와 JSON 엔드포인트
__NEXT_DATA__ 스타일 상태 블롭
Temu의 상품 페이지를 열고 소스를 확인하면, <script id="__NEXT_DATA__" type="application/json"> 태그 안에 직렬화된 JSON 상태 객체가 포함되어 있습니다. 이 객체에는 상품 정보, 가격, SKU 리스트, 이미지 URL, 리뷰 요약 등이 모두 들어 있습니다. 페이지를 한 번 로드하면 추가 AJAX 요청 없이도 대부분의 데이터를 추출할 수 있습니다.
추출 방법은 간단합니다:
import re, json
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
script_tag = soup.find('script', id='__NEXT_DATA__')
data = json.loads(script_tag.string)
# 상품 정보는 props.pageProps 내에 위치
product = data['props']['pageProps']['goods']
print(product['goods_id'], product['goods_name'])
JSON 엔드포인트: /api/poppy/v1/search와 goods/detail
검색 결과 페이지의 상품 그리드는 /api/poppy/v1/search 엔드포인트를 통해 데이터를 로드합니다. 상품 상세 페이지의 추가 정보(리뷰, SKU 재고 등)는 /api/poppy/v1/goods/detail에서 가져옵니다. 이 엔드포인트들은 다음과 같은 파라미터를 받습니다:
search_key: 검색 키워드page: 페이지 번호 (0부터 시작)pageSize: 페이지당 항목 수 (최대 50)goods_id: 상품 ID (detail 엔드포인트용)
응답은 JSON 배열 형태이며, 각 항목에는 goods_id, goods_name, min_normal_price, sku_list 등의 필드가 포함됩니다. hashed CSS 클래스(._3aBcD 등)와 data-uniqid 속성은 HTML 파싱 시에만 필요하므로, JSON 엔드포인트를 사용하면 이 문제를 우회할 수 있습니다.
속도 제한 임계값과 주거용 프록시의 필요성
Temu는 IP당 요청 빈도에 제한을 적용합니다. 실측 기준, 동일 IP에서 약 30~50회/분 이상의 요청이 발생하면 429 Too Many Requests 응답이 반환되며, 이후 해당 IP는 15분~1시간 동안 차단됩니다. 더 심각한 경우 Cloudflare 레벨에서 403 챌린지가 영구적으로 적용될 수 있습니다.
데이터센터 IP는 이 임계값이 더 낮습니다. 실측에서 AWS us-east-1 IP는 약 10회/분만에 차단되었습니다. 반면 주거용 IP는 일반 사용자 트래픽과 구분이 어려워 더 높은 허용치를 가집니다.
또한 지역별 가격 및 배송 차이가 존재합니다. Temu는 사용자의 IP 기반 지역에 따라 상품 가격, 배송비, 재고 여부를 다르게 표시합니다. 예를 들어 미국 사용자에게 보이는 가격과 영국 사용자에게 보이는 가격은 환율 단순 변환을 넘어 실제로 다를 수 있습니다. 따라서 정확한 가격 인텔리전스를 위해서는 도시 수준의 지역 타겟팅이 필요합니다.
ProxyHat의 주거용 프록시를 사용하면 국가 및 도시 수준의 지역 타겟팅이 가능합니다:
# 미국 전체
http://user-country-US:pass@gate.proxyhat.com:8080
# 독일 베를린
http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080
# 미국 뉴욕
http://user-country-US-city-newyork:pass@gate.proxyhat.com:8080
이를 통해 각 지역의 실제 사용자가 보는 가격을 정확히 수집할 수 있습니다. ProxyHat 지역 목록에서 지원하는 모든 국가와 도시를 확인할 수 있습니다.
Python 실습: curl_cffi + ProxyHat로 Temu 상품 페이지 수집
이제 실제 코드로 Temu 상품 페이지를 수집하고 JSON을 파싱하는 전체 예제를 살펴보겠습니다. curl_cffi의 Chrome 모방 모드와 ProxyHat 주거용 프록시를 조합합니다.
from curl_cffi import requests as cffi_requests
import json, re, time, random
# ProxyHat 주거용 프록시 설정 (미국 지역)
proxy_url = "http://user-country-US:pass@gate.proxyhat.com:8080"
proxies = {
"http": proxy_url,
"https": proxy_url,
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Referer": "https://www.temu.com/",
}
def fetch_product_page(goods_id: str, session_id: str = None):
"""Temu 상품 페이지를 가져와 __NEXT_DATA__를 추출합니다."""
# sticky session을 위한 프록시 URL 구성
if session_id:
proxy = f"http://user-country-US-session-{session_id}:pass@gate.proxyhat.com:8080"
else:
proxy = proxy_url
proxies = {"http": proxy, "https": proxy}
url = f"https://www.temu.com/goods.html?goods_id={goods_id}"
response = cffi_requests.get(
url,
headers=headers,
proxies=proxies,
impersonate="chrome131",
timeout=30,
)
if response.status_code != 200:
raise Exception(f"HTTP {response.status_code}")
# __NEXT_DATA__ 추출
match = re.search(
r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>',
response.text,
re.DOTALL
)
if not match:
raise Exception("__NEXT_DATA__ not found")
data = json.loads(match.group(1))
return data
def parse_product(data: dict) -> dict:
"""추출된 JSON에서 핵심 상품 정보를 파싱합니다."""
goods = data.get("props", {}).get("pageProps", {}).get("goods", {})
return {
"goods_id": goods.get("goods_id"),
"goods_name": goods.get("goods_name"),
"min_price": goods.get("min_normal_price", {}).get("amount"),
"currency": goods.get("min_normal_price", {}).get("currency"),
"sku_list": [
{
"sku_id": sku.get("sku_id"),
"price": sku.get("price", {}).get("amount"),
"stock": sku.get("stock"),
}
for sku in goods.get("sku_list", [])[:5] # 처음 5개만
],
"image_url": goods.get("thumb_url"),
"rating": goods.get("rating"),
"review_count": goods.get("review_count"),
}
# 실행 예제
try:
raw = fetch_product_page("0020999", session_id="temu-batch-001")
product = parse_product(raw)
print(json.dumps(product, indent=2, ensure_ascii=False))
except Exception as e:
print(f"Error: {e}")
샘플 응답 (truncated)
위 코드의 출력은 다음과 같은 구조를 가집니다:
{
"goods_id": "0020999",
"goods_name": "Wireless Bluetooth Earbuds Pro",
"min_price": "12.98",
"currency": "USD",
"sku_list": [
{
"sku_id": "SKU-001",
"price": "12.98",
"stock": 3421
},
{
"sku_id": "SKU-002",
"price": "14.99",
"stock": 1280
}
],
"image_url": "https://img.temu.com/jpg/0020999_001.jpg",
"rating": 4.6,
"review_count": 15823
}
Sticky 세션, 재시도, 페이지네이션 전략
Sticky 세션으로 장바구니/배송 일관성 유지
Temu는 세션 기반으로 장바구니와 배송 옵션을 관리합니다. IP가 요청마다 변경되면 세션 상태가 깨져 배송비 계산이나 재고 확인이 부정확해집니다. ProxyHat의 -session- 플래그를 사용하면 특정 시간 동안 동일한 IP를 유지할 수 있습니다:
# 동일 세션 ID로 sticky IP 유지
http://user-country-US-session-temu-cart-001:pass@gate.proxyhat.com:8080
Sticky 세션은 기본적으로 10분~30분 동안 동일 IP를 유지합니다. 장바구니 데이터 수집이나 다단계 체크아웃 흐름을 스크래핑할 때 필수적입니다.
재시도 로직
429 응답 시 지수 백오프(exponential backoff)를 사용한 재시도가 필요합니다. 각 재시도마다 새로운 프록시 IP로 전환하여 차단된 IP를 우회합니다:
import time
def fetch_with_retry(goods_id: str, max_retries: int = 5):
for attempt in range(max_retries):
try:
# 매 시도마다 새 세션 ID로 새 IP 할당
session_id = f"temu-retry-{attempt}-{int(time.time())}"
raw = fetch_product_page(goods_id, session_id=session_id)
return parse_product(raw)
except Exception as e:
wait = (2 ** attempt) + random.uniform(0, 1)
print(f"Attempt {attempt+1} failed: {e}. Waiting {wait:.1f}s")
time.sleep(wait)
raise Exception(f"Failed after {max_retries} retries")
페이지네이션
검색 결과 페이지네이션은 page 파라미터를 증가시키며 처리합니다. 각 페이지마다 새로운 프록시 IP를 사용하는 것이 권장됩니다. 페이지당 50개 항목을 수집하고, 페이지 간 2~5초의 랜덤 대기 시간을 두어 자연스러운 탐색 패턴을 모방합니다:
def scrape_search_results(keyword: str, max_pages: int = 10):
results = []
for page in range(max_pages):
session_id = f"temu-search-{keyword}-p{page}"
proxy = f"http://user-country-US-session-{session_id}:pass@gate.proxyhat.com:8080"
url = "https://www.temu.com/api/poppy/v1/search"
params = {"search_key": keyword, "page": page, "pageSize": 50}
response = cffi_requests.get(
url, params=params, headers=headers,
proxies={"http": proxy, "https": proxy},
impersonate="chrome131", timeout=30
)
if response.status_code == 200:
items = response.json().get("data", {}).get("items", [])
results.extend(items)
print(f"Page {page}: {len(items)} items")
else:
print(f"Page {page}: HTTP {response.status_code}")
time.sleep(10) # 차단 시 대기
continue
time.sleep(random.uniform(2, 5)) # 자연스러운 간격
return results
프록시 유형 비교: Temu 스크래핑에 무엇이 최적인가
| 프록시 유형 | 성공률 | 속도 | 비용 | 적합도 |
|---|---|---|---|---|
| 데이터센터 | 낮음 (~10%) | 빠름 (<100ms) | 저렴 | 부적합 — Cloudflare가 즉시 차단 |
| 주거용 (회전) | 높음 (~90%) | 보통 (200~500ms) | 중간 | 적합 — 검색/상품 페이지 수집 |
| 주거용 (Sticky) | 높음 (~85%) | 보통 (200~500ms) | 중간 | 적합 — 장바구니/배송 데이터 |
| 모바일 (4G/5G) | 매우 높음 (~95%) | 느림 (500ms~2s) | 높음 | 최적 — 가장 강력한 안티봇 우회 |
대량의 상품 카탈로그 수집에는 주거용 회전 프록시가 가장 비용 효율적입니다. 모바일 프록시는 성공률이 가장 높지만 비용이 부담될 수 있으므로, 차단이 빈번한 특정 작업에만 사용하는 것이 좋습니다. ProxyHat 가격 페이지에서 각 유형별 요금을 확인할 수 있습니다.
윤리와 이용약관: 경계선을 어디에 그을 것인가
Temu 스크래핑은 기술적으로 가능하지만, 법적·윤리적 경계를 명확히 이해해야 합니다.
- 공개 카탈로그 데이터만 수집: 검색 결과와 상품 페이지에 로그인 없이 접근 가능한 정보(가격, 상품명, 이미지, 리뷰 요약)는 공개 데이터로 간주할 수 있습니다.
- 계정 스크래핑 금지: 로그인이 필요한 페이지(주문 내역, 개인화된 추천, 위시리스트)는 스크래핑하지 마세요. 이는 미국 CFAA(Computer Fraud and Abuse Act) 위반 소지가 있으며, GDPR에 따른 개인정보 침해 문제도 발생할 수 있습니다.
- robots.txt 준수: Temu의
robots.txt를 확인하고, disallow된 경로는 스크래핑하지 않습니다. - 요청 빈도 제한: 서버에 부하를 주지 않는 수준의 요청 빈도를 유지합니다. 분당 30회 이하를 권장합니다.
- 공식 판매자 피드 우선: Temu 판매자인 경우, Temu Seller Center에서 제공하는 공식 API/피드를 사용하는 것이 가장 안전합니다. 판매자 권한이 있다면 이 경로를 최우선으로 고려하세요.
가격 인텔리전스 목적의 공개 데이터 수집은 일반적으로 허용되는 관행이지만, 수집한 데이터를 경쟁사에 재판매하거나 Temu의 비즈니스를 직접적으로 방해하는 방식으로 사용해서는 안 됩니다. 법적 조언이 필요한 경우 전문가에게 문의하세요.
Key Takeaways
- JSON 엔드포인트 우선:
/api/poppy/v1/search와__NEXT_DATA__블롭이 HTML 파싱보다 안정적입니다.- anti_content 토큰은 필수: JSON API 호출 시 서명된 토큰이 없으면 403이 반환됩니다. Playwright로 토큰을 추출하는 하이브리드 방식을 고려하세요.
- TLS 지문 모방:
curl_cffi의impersonate="chrome131"옵션으로 Chrome TLS 지문을 모방하세요. 일반requests는 즉시 차단됩니다.- 주거용 프록시 + 지역 타겟팅: 데이터센터 IP는 약 10회/분만에 차단됩니다. 주거용 IP와
-country-US지역 타겟팅으로 정확한 가격 데이터를 수집하세요.- Sticky 세션: 장바구니/배송 데이터 수집 시
-session-플래그로 IP 일관성을 유지하세요.- 윤리적 경계: 공개 카탈로그 데이터만 수집하고, 계정 스크래핑은 피하며, robots.txt를 준수하세요.
Temu 가격 스크래퍼를 구축할 준비가 되셨다면, 웹 스크래핑 사용 사례와 SERP 추적 사용 사례에서 추가 인사이트를 확인하세요. ProxyHat 문서에서 프록시 설정 전체 가이드를 볼 수 있습니다.






