Target.com은 겉보기엔 금방 scraping할 수 있을 것 같은 웹사이트 중 하나다. 그런데 실제로 해보면 이야기가 완전히 달라진다. 이미 Requests와 BeautifulSoup로 간단한 Python 스크립트를 짜서 Target 상품 페이지에 던져봤는데 가격 대신 None만 돌아온 경험이 있다면, 그건 너만 겪은 일이 아니다.
대형 리테일 사이트들을 대상으로 scraping 방법을 이것저것 테스트해 본 결과, Target은 거의 항상 가장 까다로운 케이스에 들어간다고 확실히 말할 수 있다. 에 달하는 만큼, 가격, 리뷰, 재고 상태, 평점 같은 상품 데이터는 정말 보물창고 수준이다. 하지만 React 기반의 client-side rendering과 Akamai 봇 탐지 조합 때문에, 단순한 접근 방식은 거의 바로 막힌다. 그래도 실제로 잘 작동하는 Python 방법은 세 가지가 있다. 여기서는 각각을 차근차근 보여주고, 왜 첫 시도가 거의 늘 실패하는지도 설명할 거고, Python이 굳이 들일 만큼 가치가 없다면 쓸 수 있는 No-Code 우회 방법도 함께 소개하겠다.
왜 Target.com 첫 Python scrape은 None을 돌려줄까
해결책으로 가기 전에, 먼저 문제부터 짚고 넘어가자. 초보자들이 가장 흔하게 짜는 코드는 대략 이런 식이다:
1import requests
2from bs4 import BeautifulSoup
3url = "https://www.target.com/p/some-product/-/A-12345678"
4response = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
5soup = BeautifulSoup(response.text, "html.parser")
6price = soup.select_one('[data-test="current-price"]')
7print(price) # None
결과는? None. 매번 똑같다.
이건 코드가 틀렸기 때문이 아니다. requests.get()이 Target에서 받아오는 HTML은 사실상 껍데기일 뿐이다. “JavaScript를 불러와서 실제 페이지를 렌더링하라”는 React 프레임만 내려주고 끝난다. 상품 가격, 평점, 리뷰, 재고 정보는 초기 페이지 로드 이후에 JavaScript로 따로 주입된다. Python의 Requests는 JavaScript를 실행하지 않으니까, 응답 안에는 그 요소들이 아예 존재하지 않는 셈이다.
개발자들이 forum에서 자주 부딪히는 지점도 בדיוק 이 문제다. 은 이렇게 요약한다. “어떤 요소가 None으로 나오는 이유는 JavaScript로 렌더링되기 때문이며, requests는 JavaScript 렌더링된 HTML을 가져올 수 없기 때문이다.” 도 같은 이야기를 한다. “Target URL에 HTTP 요청을 보내면 HTML 응답에서 실제 데이터가 빠져 있다.”
게다가 JavaScript 문제를 해결한다고 끝이 아니다. 두 번째 장벽도 있다. Akamai가 TLS handshake 단계에서 이미 네 요청이 진짜 브라우저처럼 보이는지 검사하고, Python requests는 HTML 한 바이트도 주고받기 전에 표시되는 경우가 많다. 이건 조금 뒤에서 더 설명하겠다.
왜 Target.com은 Python으로 scraping하기 그렇게 어려운가
Target은 단순히 “JavaScript가 많은 웹사이트”가 아니다. 여러 겹의 방어 체계를 가진 사이트고, 그 구조를 이해해야 어떤 scraping 방법을 써야 할지 판단할 수 있다.
상품 데이터는 JavaScript로 렌더링된다
Target.com은 React 기반이다. 실제 브라우저에서 상품 페이지나 검색 페이지를 열면 이런 순서로 진행된다:
- 서버가 최소한의 HTML 골격만 내려준다
- JavaScript 번들이 로드되고 실행된다
- 프런트엔드가 Target 내부 Redsky API를 호출한다
- 가격, 평점, 이미지, 재고 같은 상품 데이터가 DOM에 렌더링된다
여기서 2~4단계를 건너뛰면 — 바로 requests.get()이 그렇게 한다 — 너는 빈 페이지를 받게 된다. 에 따르면, Target에서 정적 HTTP request만으로는 전체 데이터의 약 만 확보할 수 있다. 나머지 70%는 JavaScript 실행이나 API 접근이 필요하다.
검색 결과 페이지는 더 까다롭다. 초기 HTML에는 몇 개 상품만 보이고, 나머지는 스크롤해야 더 불러온다.
Target의 Anti-Bot 방어는 “프록시만 쓰면 끝” 수준이 아니다
대부분의 scraping 가이드는 anti-bot 대책을 말할 때 “그냥 프록시 쓰면 돼” 정도로 끝낸다. 하지만 Target은 좀 더 자세히 봐야 한다.
TLS fingerprinting이 핵심이다. HTTPS handshake에서 클라이언트는 TLS 버전, cipher suite, 확장 항목, elliptic curve 정보를 담은 Client Hello 패킷을 보낸다. 여기서 JA3 fingerprint가 계산된다. Python requests는 — 8d9f7747675e24454cd9b7ed35c58707 — 를 만들어내고, 이 값은 anti-bot 데이터베이스에서 바로 잡힌다. Chrome은 GREASE 값과 함께 16개의 cipher suite를 정교하게 배열해서 보내지만, Python은 브라우저답지 않은 순서로 60개가 넘는 항목을 보낸다. 즉, HTTP 내용이 오가기 전부터 차단되는 셈이다.
IP 평판 점수도 본다. Akamai는 IP를 신뢰도 등급으로 분류한다. 에 따르면 데이터센터 IP는 “봇으로 사용될 가능성이 높아서 신뢰 점수가 크게 낮다.” 반면 residential IP는 더 높은 점수를 받는다. Target에서는 특히 데이터센터 IP 대역이 빠르게 의심받는다.
JavaScript fingerprinting도 있다. Akamai는 JavaScript를 주입해서 JS 엔진 정보, 하드웨어 성능, 운영체제, 폰트, 플러그인, 그리고 타이핑 속도나 마우스 움직임, 클릭 간격 같은 행동 데이터를 수집한다. 이걸 바탕으로 _abck 쿠키가 만들어진다. 이 상태 기반 fingerprint 토큰이 없으면 차단된다.
Rate limiting도 강하다. Target은 IP당 분당 대략 30~60 request 정도에서 429 오류를 내기 시작한다. 어떤 경우에는 실제로는 “Pardon Our Interruption” 차단 페이지가 담겨 있는 경우도 있다. 자동 탐지를 더 어렵게 만드는 방식이다.
로 평가했고, Akamai 우회 난이도는 특히 수준이라고 본다.
Python으로 Target.com을 scraping하는 3가지 방법을 직접 비교하면
세 가지 실전 방법을 한 번에 깔끔하게 비교해 둔 글은 잘 없다. 여기서는 실제로 쓸 만한 접근법만 솔직하게 나란히 정리했다:
| 기준 | Requests + BS4 | Selenium / Playwright | Redsky API |
|---|---|---|---|
| JavaScript 렌더링 가능 여부 | ❌ 불가 | ✅ 가능 | ✅ 가능(JSON) |
| 데이터 1건당 속도 | ⚡ 약 0.5~1초 | 🐢 약 5~10초 | ⚡ 약 0.5~1초 |
| Anti-Bot 위험도 | ⚠️ 높음(TLS fingerprint) | ⚠️ 중간 | ⚠️ 중간(Auth key 변경 가능) |
| 초기 설정 난이도 | 낮음 | 중간 | 중간~높음(Reverse engineering 필요) |
| 데이터 완성도 | 약 30%(정적 HTML만) | 약 95%(페이지 전체) | 약 90%(구조화된 JSON) |
| 가장 적합한 용도 | 정적 메타데이터, __TGT_DATA__ | 전체 상품 페이지, 리뷰 | 대량 상품 데이터 |
이제 각각의 방법을 하나씩 실제로 만들어 보자.
방법 1: Python Requests와 BeautifulSoup로 Target.com scraping하기
이 방법으로는 JavaScript로 렌더링된 검색 결과 가격을 바로 가져올 수는 없다. 그래도 가볍고 빠르며, 어디를 봐야 하는지만 알면 생각보다 많은 정보를 얻을 수 있다.
핵심은 Target이 일부 상품 데이터를 <script> 태그 안에 __TGT_DATA__와 __PRELOADED_QUERIES__ 형태로 심어 둔다는 점이다. 이 JSON 블록에는 상품명, 설명, 특징이 들어 있고, 개별 상품 페이지에서는 가격이 보이는 경우도 있다. 또 검색 결과 HTML에서 상품 제목과 URL은 직접 뽑아낼 수 있다.
Step 1: Python 환경 세팅하기
프로젝트 폴더를 만들고 필요한 패키지를 설치하자:
1mkdir target-scraper && cd target-scraper
2python -m venv venv
3source venv/bin/activate # On Windows: venv\Scripts\activate
4pip install requests beautifulsoup4 curl_cffi
여기서는 기본 requests 대신 curl_cffi를 쓰는 게 좋다. 이 도구는 브라우저의 TLS fingerprint를 흉내 내서, Target에서 바로 막힐 가능성을 크게 줄여 준다. 는 curl_cffi가 을 보였고, 기존 requests는 에 그쳤다고 보여준다. 거의 15배 차이다.
Step 2: Target 검색 결과 scraping하기
Target 검색 URL 패턴은 단순하다: https://www.target.com/s?searchTerm={keyword}
1from curl_cffi import requests as cureq
2from bs4 import BeautifulSoup
3import time, random
4headers = {
5 "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",
6 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
7 "Accept-Language": "en-US,en;q=0.9",
8}
9url = "https://www.target.com/s?searchTerm=bluetooth+headphones"
10resp = cureq.get(url, headers=headers, impersonate="chrome124")
11soup = BeautifulSoup(resp.text, "html.parser")
12# 상품 카드에는 이 data-test 속성이 들어간다
13cards = soup.find_all("div", {"data-test": "@web/site-top-of-funnel/ProductCardWrapper"})
14for card in cards:
15 link_tag = card.find("a")
16 title = link_tag.get_text(strip=True) if link_tag else "N/A"
17 href = "https://www.target.com" + link_tag["href"] if link_tag and link_tag.get("href") else "N/A"
18 print(f"{title} — {href}")
이렇게 하면 상품명과 URL은 얻을 수 있다. 가격은? 이 HTML에서 안 보일 가능성이 높다. 그건 정상이다.
Step 3: 상품 페이지에 들어 있는 JSON 데이터 꺼내기
개별 상품 페이지는 __TGT_DATA__ 스크립트 태그에 더 풍부한 데이터를 넣어 둔다:
1import re, json
2product_url = "https://www.target.com/p/some-product/-/A-12345678"
3resp = cureq.get(product_url, headers=headers, impersonate="chrome124")
4soup = BeautifulSoup(resp.text, "html.parser")
5# __TGT_DATA__ 스크립트를 찾는다
6scripts = soup.find_all("script")
7for script in scripts:
8 if script.string and "__TGT_DATA__" in script.string:
9 # 스크립트 안에서 JSON 추출
10 match = re.search(r'__TGT_DATA__\s*=\s*({.*?});?\s*$', script.string, re.DOTALL)
11 if match:
12 tgt_data = json.loads(match.group(1))
13 # JSON 구조에서 상품 정보를 찾는다
14 queries = tgt_data.get("__PRELOADED_QUERIES__", {})
15 # 페이지마다 구조가 조금씩 다르므로 출력해 보면서 확인해야 한다
16 print(json.dumps(queries, indent=2)[:500])
__TGT_DATA__ 안의 JSON에는 상품명, 설명, 특징, 그리고 종종 가격 데이터까지 들어 있다. 다만 구조가 페이지마다 달라서, 출력 결과를 먼저 보고 원하는 항목까지 타고 들어가야 한다.
Step 4: Pagination 처리하기
Target의 페이지네이션은 Nao 파라미터를 쓴다. 1페이지는 Nao=0, 2페이지는 Nao=24, 3페이지는 Nao=48 식으로 24개씩 움직인다:
1for page in range(0, 120, 24): # 첫 5페이지
2 paginated_url = f"https://www.target.com/s?searchTerm=bluetooth+headphones&Nao={page}"
3 resp = cureq.get(paginated_url, headers=headers, impersonate="chrome124")
4 # 파싱과 추출 처리 ...
5 time.sleep(random.uniform(2, 5)) # 너무 공격적으로 돌리지 말자
Step 5: scraping한 데이터 저장하기
1import csv
2with open("target_products.csv", "w", newline="", encoding="utf-8") as f:
3 writer = csv.DictWriter(f, fieldnames=["title", "url", "price", "description"])
4 writer.writeheader()
5 for product in products:
6 writer.writerow(product)
이 방법으로 얻는 것: 상품명, URL, 설명, 그리고 일부 임베디드 메타데이터. 안정적으로 못 얻는 것: 검색 결과 페이지의 동적 가격과 리뷰. 그건 방법 2나 3이 필요하다.
방법 2: Selenium 또는 Playwright로 Target.com scraping하기
Headless browser는 JavaScript를 렌더링하고, 동적 콘텐츠를 불러오며, 실제 사용자 동작도 어느 정도 흉내 낸다. 바로 이 방식으로 가격, 평점, 리뷰를 가져올 수 있다.
Selenium과 Playwright 중 무엇을 쓸지 고민된다면, — — 그리고 벤치마크상 (20페이지 기준 11초 vs 28초). 여기서는 커뮤니티와 튜토리얼이 많은 Selenium으로 설명하겠지만, 처음 시작한다면 Playwright가 더 좋은 선택이다.
Step 1: Selenium과 ChromeDriver 설치하기
1pip install selenium webdriver-manager
webdriver-manager를 쓰면 맞는 ChromeDriver 버전을 알아서 잡아준다. 덕분에 “ChromeDriver version mismatch” 같은 골칫거리를 피할 수 있다:
1from selenium import webdriver
2from selenium.webdriver.chrome.service import Service
3from selenium.webdriver.chrome.options import Options
4from webdriver_manager.chrome import ChromeDriverManager
5options = Options()
6options.add_argument("--headless=new")
7options.add_argument("--window-size=1920,1080")
8options.add_argument("--disable-blink-features=AutomationControlled")
9options.add_argument("--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")
10driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
Step 2: Target 페이지를 열고 렌더링을 기다리기
1from selenium.webdriver.common.by import By
2from selenium.webdriver.support.ui import WebDriverWait
3from selenium.webdriver.support import expected_conditions as EC
4driver.get("https://www.target.com/s?searchTerm=bluetooth+headphones")
5# 상품 카드가 렌더링될 때까지 기다린다 (explicit wait가 time.sleep보다 낫다)
6WebDriverWait(driver, 15).until(
7 EC.presence_of_element_located((By.CSS_SELECTOR, '[data-test="product-title"]'))
8)
명시적 대기(explicit wait)는 매우 중요하다. time.sleep(10)은 빠를 때는 시간만 낭비하고, 느릴 때는 부족하다. 결국 둘 다 별로다. WebDriverWait는 요소가 나타날 때까지 500ms 단위로 확인하다가, 타임아웃이 되면 멈춘다.
Step 3: 페이지를 아래로 스크롤해서 모든 상품 로드하기
Target은 스크롤할 때마다 상품을 더 불러온다. 스크롤 없이 보면 4~5개 상품만 보이고, 전체 페이지를 다 못 가져온다:
1import time
2last_height = driver.execute_script("return document.body.scrollHeight")
3for _ in range(10):
4 driver.execute_script("window.scrollBy(0, 300);")
5 time.sleep(1.5)
6 new_height = driver.execute_script("return document.body.scrollHeight")
7 if new_height == last_height:
8 break
9 last_height = new_height
에 따르면 1.5초 간격으로 10번 스크롤하면 8개 이상의 상품을 확보할 수 있지만, 스크롤하지 않으면 4~5개밖에 안 나온다. 스크롤 폭은 200~300px 정도로 두어야 사람처럼 보인다.
Step 4: 렌더링된 페이지에서 상품 데이터 추출하기
1products = []
2cards = driver.find_elements(By.CSS_SELECTOR, '[data-test="@web/site-top-of-funnel/ProductCardWrapper"]')
3for card in cards:
4 try:
5 title = card.find_element(By.CSS_SELECTOR, '[data-test="product-title"]').text
6 except:
7 title = "N/A"
8 try:
9 price = card.find_element(By.CSS_SELECTOR, '[data-test="current-price"]').text
10 except:
11 price = "N/A"
12 try:
13 link = card.find_element(By.CSS_SELECTOR, 'a[href*="/p/"]').get_attribute("href")
14 except:
15 link = "N/A"
16 products.append({"title": title, "price": price, "link": link})
17for p in products:
18 print(f'{p["title"]} — {p["price"]}')
Target에서 자주 쓰이는 data-test selector는 2026년 기준으로 다음과 같다:
| 데이터 항목 | 셀렉터 |
|---|---|
| 상품 카드 | data-test="@web/site-top-of-funnel/ProductCardWrapper" |
| 상품명 | data-test="product-title" |
| 현재 가격 | data-test="current-price" |
| 평점 | data-test="rating-value" |
| 리뷰 수 | data-test="rating-count" |
Step 5: 상품 리뷰 scraping하기
개별 상품 페이지를 열고, 리뷰 영역까지 스크롤한 뒤 리뷰 데이터를 뽑아내면 된다:
1from bs4 import BeautifulSoup
2driver.get("https://www.target.com/p/some-product/-/A-12345678")
3# 아래로 스크롤해서 리뷰 로드
4for _ in range(5):
5 driver.execute_script("window.scrollBy(0, 500);")
6 time.sleep(2)
7soup = BeautifulSoup(driver.page_source, "html.parser")
8reviews = soup.find_all("div", {"data-test": "review-card--text"})
9for review in reviews:
10 print(review.get_text(strip=True)[:100])
리뷰는 Bazaarvoice를 통해 로드되고, pagination(최대 51페이지), 최신순 정렬, 사진이 있는 리뷰만 보는 필터까지 지원한다. 는 Selenium 기준으로 데이터 1건당 약 5.1초 정도 걸린다고 보여준다.
마지막에는 브라우저를 꼭 닫자:
1driver.quit()
방법 3: Target.com을 Redsky API로 scraping하기
Target 프런트엔드는 내부 API인 redsky.target.com을 통해 거의 모든 데이터를 불러온다. 이 API를 Python으로 직접 호출하면 HTML 파싱, 브라우저, JavaScript 렌더링이 전혀 필요 없다. 응답은 가격, 평점, 리뷰, 이미지, 재고, fulfillment, 스펙, variant 정보까지 담긴 깔끔한 JSON이다. 대량 데이터 작업이라면 이게 단연 가장 빠르고 안정적이다.
Step 1: Chrome DevTools로 Redsky API 찾기
대부분의 튜토리얼은 이 단계를 건너뛰지만, 직접 찾는 방법은 어렵지 않다:
- Chrome에서 Target 상품 페이지를 연다
- DevTools(F12) → Network 탭을 연다
- Fetch/XHR로 필터링한다
- 페이지를 새로고침한다
redsky.target.com또는redsky.a]target.com으로 가는 request를 찾는다- request를 클릭해서 Request URL과 Headers를 확인한다
대략 이런 URL이 보일 거다:
1https://redsky.target.com/redsky_aggregations/v1/web/pdp_fulfillment_v1?key=9f36aeafbe60771e321a7cc95a78140772ab3e96&tcin=12345678&store_id=2148&zip=55401
핵심 파라미터는 다음과 같다:
key— API key(고정형, 로테이션되지 않음, 엔드포인트마다 다를 수 있음)tcin— Target.com item number(8자리 상품 ID)store_id— Target 매장 IDzip— fulfillment 데이터를 위한 우편번호
API key는 request URL 안에 query parameter로 들어 있으니 바로 읽어낼 수 있다.
Step 2: Python으로 Redsky API에 직접 요청하기
1from curl_cffi import requests as cureq
2import json
3API_KEY = "9f36aeafbe60771e321a7cc95a78140772ab3e96" # DevTools에서 추출
4TCIN = "12345678"
5url = f"https://redsky.target.com/redsky_aggregations/v1/web/pdp_fulfillment_v1?key={API_KEY}&tcin={TCIN}&store_id=2148&zip=55401"
6headers = {
7 "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",
8 "Accept": "application/json",
9 "Origin": "https://www.target.com",
10 "Referer": "https://www.target.com/",
11 "Sec-Fetch-Site": "same-site",
12 "Sec-Fetch-Mode": "cors",
13 "Sec-Fetch-Dest": "empty",
14}
15resp = cureq.get(url, headers=headers, impersonate="chrome124")
16data = resp.json()
17# JSON 응답에서 상품 정보 꺼내기
18product = data.get("data", {}).get("product", {})
19title = product.get("item", {}).get("product_description", {}).get("title", "N/A")
20price = product.get("price", {}).get("formatted_current_price", "N/A")
21rating = product.get("ratings_and_reviews", {}).get("statistics", {}).get("rating", {}).get("average", "N/A")
22print(f"{title} — {price} — Rating: {rating}")
HTML 파싱이 전혀 필요 없다. 응답이 구조화되어 있어서 빠르고 깔끔하다.
Step 3: API로 검색 결과 scraping하기
product_summary_with_fulfillment_v1 엔드포인트는 여러 TCIN을 한 번에 받는다:
1tcins = ["12345678", "23456789", "34567890"]
2tcin_str = ",".join(tcins)
3search_url = f"https://redsky.target.com/redsky_aggregations/v1/web/product_summary_with_fulfillment_v1?key={API_KEY}&tcins={tcin_str}&store_id=2148&zip=55401"
4resp = cureq.get(search_url, headers=headers, impersonate="chrome124")
5results = resp.json()
6for item in results.get("data", {}).get("product_summaries", []):
7 title = item.get("title", "N/A")
8 price = item.get("price", {}).get("formatted_current_price", "N/A")
9 print(f"{title} — {price}")
TCIN은 검색 결과 HTML에서 상품 URL의 /A-XXXXXXXX 부분을 읽거나, __TGT_DATA__ 안에서 꺼내면 된다.
Step 4: 병렬 request로 속도 높이기
1from concurrent.futures import ThreadPoolExecutor
2import time, random
3def fetch_product(tcin):
4 url = f"https://redsky.target.com/redsky_aggregations/v1/web/pdp_fulfillment_v1?key={API_KEY}&tcin={tcin}&store_id=2148&zip=55401"
5 time.sleep(random.uniform(2, 5))
6 resp = cureq.get(url, headers=headers, impersonate="chrome124")
7 return resp.json()
8tcin_list = ["12345678", "23456789", "34567890", "45678901"]
9with ThreadPoolExecutor(max_workers=3) as executor:
10 results = list(executor.map(fetch_product, tcin_list))
병렬화는 너무 과하게 하지 않는 게 좋다. 3~5개 thread 정도에, 2~5초 랜덤 pause를 두는 정도가 안전하다. Target의 rate limit은 IP당 분당 약 수준이다.
Redsky API를 쓸 때 꼭 알아둘 점
실무 파이프라인으로 쓰기 전에, 아래 사항은 꼭 이해하고 있어야 한다:
- API key는 고정형이지만 엔드포인트별로 다르다. 서로 다른 Redsky endpoint는 서로 다른 key를 쓴다. 자주 바뀌지는 않지만, Target이 원하면 언제든 교체할 수 있다.
- 문서화되지 않은 내부 API다. Target Engineering 팀이 했기 때문에 법적 위험은 줄지만, SLA가 있는 공식 public API는 아니다.
- 상품 variant(색상, 사이즈)는 각각 별도 TCIN이다. 각 variant는 따로 조회해야 한다.
Sec-Fetch-*헤더가 없으면 바로 차단될 수 있다. 이건 자주 놓치는 부분이라서,Sec-Fetch-Site,Sec-Fetch-Mode,Sec-Fetch-Dest는 항상 넣어야 한다.
큰 규모로 Target.com을 scraping하면서 차단을 피하는 팁
아래 실무 원칙은 어떤 방법을 쓰든 production 수준에서 공통으로 적용된다.
Datacenter가 아닌 residential proxy를 돌려라
Target의 Akamai는 데이터센터 IP 대역을 금방 알아본다. 장기 scraping에서는 residential proxy가 사실상 필수다. 가격은 제각각인데, 시작하고, 수준이며, 볼륨이 커질수록 대략 3~4달러/GB까지 내려갈 수 있다.
IP는 50~100 request마다 바꾸거나, 프록시 풀에서 지원한다면 매 request마다 교체하는 편이 낫다.
curl_cffi로 TLS fingerprint를 흉내 내라
이건 가장 효과가 큰 단일 조치다. requests의 drop-in 대체재로 바로 쓸 수 있다:
1from curl_cffi import requests as cureq
2# 기본 requests — 보호된 사이트에서 성공률 12%
3# resp = requests.get(url, headers=headers)
4# curl_cffi — 성공률 92%
5resp = cureq.get(url, headers=headers, impersonate="chrome124")
는 GitHub 스타 8,200개 이상을 가진 라이브러리로, chrome99부터 chrome146까지의 Chrome 버전은 물론 Safari, Edge, 모바일 변형까지 지원한다. 동기 모드에서는 .
현실적인 request 속도와 헤더를 써라
- 랜덤 pause: request 사이에 2~7초 정도 랜덤 대기
- User-Agent 로테이션: 실제 브라우저 User-Agent 5~10개 풀을 만들어 돌리기
- Session warmup: 상품 페이지를 바로 치지 말고 먼저
target.com홈을 방문해 쿠키를 쌓기 - 헤더 일관성:
Sec-Ch-Ua는 주장하는 브라우저 버전과 맞아야 하고,Sec-Ch-Ua-Platform도 운영체제와 맞아야 한다. 어긋나면 바로 티 난다. - Session 유지: 여러 request 사이에 쿠키를 유지하자. rotating residential proxy 환경에서 안정적인 48시간 session을 권장한다.
코드 없이 Target.com을 scrape하고 싶다면: Thunderbit No-Code 대안
솔직히 말해서 Target.com은 programmatic scraping이 꽤 어려운 리테일 사이트다. JavaScript rendering, Akamai TLS fingerprinting, datacenter proxy 탐지, ChromeDriver 버전 문제까지 한꺼번에 걸린다. Python을 배우는 데는 정말 좋은 연습이지만, 실제 업무에서 Target 상품 데이터를 빨리 뽑아야 한다면 그만큼의 수고가 꼭 가치 있는 건 아니다.
데이터는 필요하지만 엔지니어링 프로젝트까지 만들고 싶지는 않다면, 이 이런 번거로운 부분을 자동으로 처리해 준다.
Thunderbit가 Target.com의 난관을 어떻게 풀어주는가
Thunderbit의 AI Web Scraper는 브라우저 안에서 바로 실행된다. 그래서 JavaScript가 자연스럽게 렌더링된다. Selenium 세팅도, headless browser 설정도, ChromeDriver 관리도 필요 없다. 브라우저 자체가 scraper가 된다.
흐름은 간단하다:
- 을 설치하고 Target 상품 페이지나 검색 페이지를 연다
- **“AI Suggest Fields”**를 클릭한다 — Thunderbit가 페이지를 읽고 상품명, 가격, 평점, 이미지 URL 같은 컬럼을 제안한다
- **“Scrape”**를 클릭한다 — 렌더링된 페이지에서 데이터를 몇 초 만에 뽑아낸다
프록시 설정도 필요 없고, TLS fingerprint를 흉내 낼 필요도 없고, None과 싸울 필요도 없다.
Target 상품 목록과 상세 페이지를 함께 scraping하기
특히 유용한 건 multi-page workflow다. Target 검색 결과 페이지를 scrape해서 상품 리스트를 만들고, 그다음 Subpage Scraping으로 각 상품 URL을 자동 방문해 설명, 전체 리뷰, 스펙 같은 상세 데이터를 표에 덧붙일 수 있다. Pagination 코드를 직접 쓰거나 브라우저 session을 관리할 필요가 없다.
결과는 Excel, Google Sheets, Airtable, Notion으로 바로 내보낼 수 있다. csv.writer boilerplate도 없고, 파일 인코딩 때문에 골치 아플 일도 없다.
반복되는 Target.com scraping 자동화하기
가격 모니터링이나 재고 추적처럼 계속 돌아가는 작업이라면, Thunderbit의 Scheduled Scraper가 특히 편하다. “매주 월요일 오전 9시”처럼 자연어로 일정을 적어두면 된다. Cron job도, 서버 세팅도, VPS에서 Python 스크립트를 계속 띄워둘 필요도 없다. 을 해야 하는 이커머스 팀에는 특히 유용하다. 가 이미 자동 가격 scraping을 쓰고 있고, 가격 인텔리전스의 평균 ROI는 에 달한다.
Target.com scraping에 어떤 방법을 언제 써야 할까
아래는 빠른 의사결정 가이드다:
| 상황 | 추천 방법 |
|---|---|
| Python을 배우는 중이고 작은 프로젝트다 | 방법 1: Requests + BS4(정적 데이터 및 __TGT_DATA__용) |
| 가격과 리뷰가 포함된 전체 상품 페이지가 필요하다 | 방법 2: Selenium / Playwright |
| 대량의 상품 데이터를 뽑아야 한다 | 방법 3: Redsky API |
| 코드 없이 빨리 데이터를 얻고 싶다 | Thunderbit (No-Code) |
| 반복적인 가격 모니터링이 필요하다 | Thunderbit Scheduled Scraper 또는 Redsky API + cron |
| 한 번만 조사하는 비기술 팀이다 | Thunderbit — 솔직히 가장 빠른 방법이다 |
프로덕션용 데이터 파이프라인을 만들 거라면, 속도와 안정성의 균형은 방법 3, 즉 Redsky API가 가장 좋다. 한 번만 리서치하려는 경우이거나 팀에 Python 경험이 없다면, Thunderbit가 시간을 엄청 아껴준다. 그리고 Web Scraping 자체를 배우는 중이라면, 방법 1 → 방법 2 → 방법 3 순서로 올라가는 게 각 단계에서 실전 감각을 익히는 데 가장 자연스럽다.
Target.com scraping의 법적·윤리적 측면
짧지만 중요한 이야기다. Target의 robots.txt에는 120개가 넘는 Disallow 경로가 있지만, 놀랍게도 /p/(상품 페이지)와 /c/(카테고리 페이지)는 막지 않는다. 즉 상품과 카테고리 페이지는 crawling이 허용된 셈이다. 반면 장바구니, 계정, 결제 페이지는 차단되어 있다.
다만 Target의 이용약관은 자동화된 접근을 금지한다. 그렇지만 Redsky API가 은 API 기반 데이터 수집의 법적 위험을 낮춰 준다.
중요한 판례도 있다:
- (Ninth Circuit, 2022): 공개 데이터 scraping은 CFAA 위반이 아니라고 봄
- (2024): Meta가 패소했고, 법원은 공개 데이터 scraping에 CFAA 위반이 없다고 판단함
상업용 대규모 scraping 프로젝트라면 법률 자문을 받는 게 좋다. 마켓 리서치, 가격 비교, 공개 데이터 기반의 개인 프로젝트라면 비교적 받아들일 만한 범위 안에 있다. 다만 항상 rate limit을 지키고 Target 서버에 무리를 주지 말아야 한다.
결론과 핵심 요약
Target.com이 까다로운 건 이유가 있다. 단순한 Requests + BeautifulSoup 방식이 실패하는 이유는 Target이 JavaScript로 데이터를 렌더링하고, Akamai가 응답 전에 TLS handshake를 fingerprinting하기 때문이다. 하지만 올바른 방법을 쓰면 데이터 추출은 충분히 가능하다.
신뢰도 순으로 정리한 세 가지 방법:
- Redsky API — 대량 데이터에 가장 빠르고 안정적이며, 깔끔한 JSON을 돌려준다. 대신 DevTools로 endpoint를 reverse-engineering해야 한다.
- Selenium / Playwright — JavaScript를 렌더링해서 페이지 전체를 얻을 수 있다. 느리지만 완전하다.
- Requests + BeautifulSoup — 정적 HTML과 embedded
__TGT_DATA__JSON에 한정된다. 빠르지만 불완전하다.
가장 큰 기술적 팁:
- 일반
requests대신curl_cffi를 써서 anti-bot 우회 성공률을 - Datacenter IP보다 residential proxy를 써라 — 데이터센터 IP는 바로 잡힌다
- 모든 request에
Sec-Fetch-*헤더를 넣어라 — 빠지면 바로 차단될 수 있다 - 먼저 홈페이지를 방문하는 session warmup은 성공률을 확실히 높여 준다
그리고 Python이 네 상황에 비해 너무 번거롭다면, 이 JavaScript rendering, anti-bot 대응, 데이터 export까지 자동으로 맡아준다. 을 써 보고, 몇 시간 걸릴 일을 몇 분 안에 끝낼 수 있는지 확인해 봐라.
더 많은 가이드와 데이터 추출 팁은 나 에서 볼 수 있다.
FAQs
Target.com은 Python Requests와 BeautifulSoup만으로 scraping할 수 있나?
부분적으로는 가능하다. 상품 페이지의 __TGT_DATA__ 스크립트 태그에서 상품명, URL, 일부 embedded JSON 데이터를 뽑을 수는 있다. 하지만 검색 결과 페이지의 가격, 평점, 리뷰, 재고는 JavaScript로 로드되므로 정적 HTTP request에서는 나오지 않는다. 완전한 데이터를 원하면 Selenium/Playwright나 Redsky API를 써야 한다.
Target.com scraper에서 가격이 왜 None으로 나오나?
Target은 첫 페이지 로드 뒤에 JavaScript로 가격 데이터를 불러온다. requests.get()을 쓰면 JavaScript가 실행되기 전의 HTML 골격만 받게 되고, 상품 데이터가 DOM에 들어가기 전 상태가 된다. 그래서 가격 요소 자체가 응답에 없다. JavaScript를 렌더링하는 headless browser(Selenium 또는 Playwright)를 쓰거나, JSON 데이터를 위해 Redsky API를 직접 호출하거나, 같은 도구를 사용해서 렌더링된 브라우저 페이지에서 바로 scraping하면 된다.
Target.com scraping은 합법인가?
공개된 데이터 scraping은 현재 미국 판례상 대체로 허용된다(hiQ v. LinkedIn, Meta v. Bright Data). Target의 robots.txt도 상품 및 카테고리 페이지 crawling을 허용한다. 다만 Target의 이용약관은 자동화된 접근을 금지하므로 회색지대가 존재한다. 공개 데이터로 하는 시장조사나 가격 비교는 보통 무난한 편이지만, 대규모 상업 프로젝트라면 법률 자문을 받는 것이 좋다.
Target의 Redsky API는 뭐고, 어떻게 접근하나?
Redsky는 Target 프런트엔드가 상품 데이터를 불러올 때 쓰는 내부 API다. 문서화된 public API나 API key 등록 시스템은 아니고, React 앱이 상품 페이지를 렌더링할 때 사용하는 백엔드에 가깝다. Chrome DevTools에서 Network 탭의 XHR/Fetch 요청을 보면 redsky.target.com으로 향하는 request를 찾을 수 있다. API key는 request URL의 query parameter로 들어 있다. Target Engineering은 이 API가 의도적으로 공개 접근 가능하다고 확인했다.
Target.com scraping에서 차단을 피하려면 어떻게 해야 하나?
가장 효과적인 단일 방법은 일반 Python requests 대신 curl_cffi를 써서 브라우저 TLS fingerprint를 흉내 내는 것이다. 이것만으로도 성공률이 . 그 외에도 residential proxy 사용, User-Agent 로테이션, request 사이 2~7초의 랜덤 pause, Sec-Fetch-* 헤더 전송, 그리고 홈페이지 방문으로 session을 예열하는 것이 중요하다. 아니면 같은 도구를 써서 anti-bot 대응을 설정 없이 처리할 수도 있다.
더 읽어보기