Target.com은 보기엔 단순해서 크롤링도 쉬울 것 같지만, 직접 해보기 전까지는 아니에요. Requests와 BeautifulSoup로 간단한 Python 스크립트를 짜서 Target 상품 페이지를 돌렸는데 가격 필드가 None으로 돌아오는 경험이 있다면, 아주 흔한 상황이에요.
대부분의 주요 리테일 사이트를 대상으로 크롤링 방식을 테스트해 본 결과, Target은 늘 손꼽히게 까다로운 편이었어요. 에 달하는 만큼 상품 데이터, 가격, 평점, 재고, 리뷰가 가득한 보물창고이긴 하지만, React 기반의 클라이언트 렌더링과 Akamai의 봇 탐지 조합 때문에 순진한 방식은 거의 즉시 막혀요. 그래도 실제로 통하는 Python 방법은 3가지가 있어요. 각각을 하나씩 살펴보고, 왜 첫 시도가 늘 실패하는지 설명한 다음, Python이 굳이 값어치를 못 하는 상황에서 쓸 수 있는 노코드 우회 방법도 보여드릴게요.
왜 Target.com을 Python으로 처음 크롤링하면 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이에요. 매번 그렇습니다.
이건 코드 버그가 아니에요. Target에서 requests.get()으로 받아오는 HTML은 사실상 껍데기에 불과해요. “이 JavaScript를 로드해서 실제 페이지를 그려라”라고 말하는 React 셸이죠. 상품 가격, 평점, 리뷰, 재고 정보는 초기 페이지 로드가 끝난 뒤에 JavaScript가 나중에 주입해요. Python의 Requests 라이브러리는 JavaScript를 실행하지 않기 때문에, 그런 요소들은 응답 안에 아예 존재하지 않는 거예요.
개발자 포럼에는 이 벽에 부딪힌 이야기들이 넘쳐나요. 한 은 아주 직설적으로 말해요. “요소가 None으로 보이는 이유는 Javascript로 렌더링되기 때문이며, requests는 Javascript로 렌더링된 HTML을 가져올 수 없기 때문이다.” 도 이를 확인해 줘요. “Target URL로 HTTP 요청을 보내면 HTML 응답에 의미 있는 데이터가 없다.”
그리고 JavaScript 문제를 해결하더라도 한 단계가 더 있어요. Target의 Akamai 봇 탐지는 TLS 핸드셰이크를 지문처럼 식별해서, HTML 한 바이트가 오가기 전부터 Python requests 라이브러리를 차단해요. 이 부분은 조금 뒤에 더 자세히 설명할게요.
Python으로 Target.com 크롤링이 특히 어려운 이유
Target은 단순히 “JavaScript를 쓰는 웹사이트”가 아니에요. 여러 겹의 방어 시스템이 있고, 각 층을 이해해야 어떤 방식이 맞는지 판단할 수 있어요.
JavaScript로 렌더링되는 상품 데이터
Target.com은 React 기반으로 만들어져 있어요. 실제 브라우저에서 상품 페이지나 검색 페이지를 열면 이런 순서로 진행돼요:
- 서버가 최소한의 HTML 셸을 보냄
- JavaScript 번들이 로드되고 실행됨
- 프런트엔드가 Target의 내부 Redsky API를 호출함
- 상품 데이터(가격, 평점, 이미지, 재고)가 DOM에 렌더링됨
2~4단계를 건너뛰면 — 정확히 requests.get()이 하는 일이 바로 그거예요 — 빈 페이지를 받게 돼요. 에 따르면 정적 HTTP 요청만으로는 Target에서 이용 가능한 데이터의 약 정도만 가져올 수 있어요. 나머지 70%는 JavaScript 실행이나 API 접근이 필요해요.
검색 결과 페이지는 더 심해요. 초기 HTML에는 몇 개의 상품만 들어 있고, 나머지는 스크롤할 때 로드돼요.
Target의 안티봇 방어: 흔한 “프록시 쓰세요” 조언보다 훨씬 복잡해요
대부분의 크롤링 가이드는 안티봇 대응을 그냥 “프록시 쓰면 돼요” 정도로 넘겨버려요. 그런데 Target의 방어는 좀 더 구체적으로 봐야 해요.
TLS 핑거프린팅(가장 중요한 부분). HTTPS 핸드셰이크 동안 클라이언트는 TLS 버전, 암호 스위트, 확장, 타원곡선 정보가 담긴 “Client Hello” 패킷을 보내요. 이 정보는 JA3 지문으로 해시돼요. Python requests 라이브러리는 — 8d9f7747675e24454cd9b7ed35c58707 — 를 만들어내서, 안티봇 데이터베이스가 즉시 잡아내요. Chrome은 GREASE 값이 포함된 16개의 암호 스위트를 정교하게 정렬해서 보내지만, Python은 브라우저와 다른 순서로 60개 이상을 보내요. 이 차이 때문에 HTTP 내용이 오가기 전에 차단돼요.
IP 평판 점수. Akamai는 IP를 신뢰도 등급별로 분류해요. 처럼 데이터센터 IP는 “봇에 사용될 가능성이 높다”며 큰 폭의 음수 신뢰 점수를 받아요. 반면 주거용 IP는 양의 점수를 받아요. 특히 Target에서는 데이터센터 IP 대역이 바로 플래그 처리돼요.
JavaScript 핑거프린팅. Akamai는 JavaScript를 주입해 JS 엔진 사양, 하드웨어 성능, OS 정보, 폰트, 플러그인, 행동 데이터(입력 속도, 마우스 움직임, 클릭 타이밍)를 수집해요. 이를 통해 _abck 쿠키라는 상태형 지문 토큰이 생성돼요. 유효한 _abck가 없으면 요청이 차단돼요.
속도 제한. Target은 IP당 분당 대략 30~60건 수준에서 429 오류를 발생시켜요. 일부 사용자는 실제로는 “Pardon Our Interruption” 차단 페이지를 담고 있는 을 받았다고도 해요. 자동 탐지를 더 까다롭게 만드는 방식이죠.
해요. 특히 Akamai 우회 난이도는 이에요.
Python으로 Target.com을 크롤링하는 3가지 방법 한눈에 보기
세 가지 가능한 방법을 한곳에서 비교한 글은 의외로 많지 않아요. 아래에서 솔직하게 평가해볼게요.
| 기준 | Requests + BS4 | Selenium / Playwright | Redsky API |
|---|---|---|---|
| JS 렌더링 처리 | ❌ 아니요 | ✅ 예 | ✅ 예(JSON) |
| 항목당 속도 | ⚡ 약 0.5~1초 | 🐢 약 5~10초 | ⚡ 약 0.5~1초 |
| 안티봇 위험 | ⚠️ 높음(TLS 지문) | ⚠️ 중간 | ⚠️ 중간(인증 키 변경 가능) |
| 설정 복잡도 | 낮음 | 중간 | 중간~높음(역공학 필요) |
| 데이터 완성도 | 약 30%(정적 HTML만) | 약 95%(전체 페이지) | 약 90%(구조화 JSON) |
| 추천 용도 | 정적 메타데이터, __TGT_DATA__ | 전체 상품 페이지, 리뷰 | 대규모 상품 데이터 |
이제 하나씩 만들어볼게요.
방법 1: Python Requests와 BeautifulSoup로 Target.com 크롤링하기
이 방법으로는 검색 결과 페이지에서 JavaScript로 렌더링되는 가격을 가져오지는 못해요. 그래도 빠르고 가볍고, 어디를 봐야 하는지 알면 생각보다 많은 정보를 뽑아낼 수 있어요.
핵심은 Target이 일부 상품 데이터를 <script> 태그 안에 숨겨둔다는 점이에요. 이 안에는 __PRELOADED_QUERIES__가 들어 있는 __TGT_DATA__ 변수가 있어요. 이 JSON 덩어리에는 상품명, 설명, 특징, 그리고 개별 상품 페이지에서는 종종 가격까지 들어 있어요. 검색 결과 HTML에서 상품 제목과 URL도 가져올 수 있어요.
1단계: Python 환경 설정하기
프로젝트 폴더를 만들고 의존성을 설치하세요:
1mkdir target-scraper && cd target-scraper
2python -m venv venv
3source venv/bin/activate # Windows에서는: venv\Scripts\activate
4pip install requests beautifulsoup4 curl_cffi
여기서는 표준 requests 대신 curl_cffi를 쓰는 게 좋아요. 브라우저 TLS 지문을 가장 효과적으로 흉내 내서 Target에서 차단당할 확률을 줄여줘요. curl_cffi는 을 보이는 반면, 표준 requests는 정도예요. 15배 개선인 셈이죠.
2단계: Target 검색 결과 크롤링하기
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에서는 아마 안 나올 거예요. 그건 예상한 결과예요.
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 구조에는 상품명, 설명, 특징, 그리고 종종 가격 정보까지 들어 있어요. 정확한 중첩 구조는 페이지마다 다르므로, 출력을 확인하면서 필요한 위치를 찾아 들어가야 해요.
4단계: 페이지네이션 처리하기
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)) # 예의 있게 요청하기
5단계: 크롤링한 데이터 저장하기
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 크롤링하기
헤드리스 브라우저는 JavaScript를 렌더링하고, 동적 콘텐츠를 불러오며, 실제 사용자 행동도 흉내 내요. 가격, 평점, 리뷰를 가져오려면 이 방법이 필요해요.
Selenium과 Playwright 중 어떤 걸 쓸지 고민된다면, — 예요 — 벤치마크상 속도도 (20페이지 기준 11초 vs. 28초). 여기서는 커뮤니티가 더 크고 튜토리얼이 많은 Selenium을 보여드릴게요. 하지만 새로 시작한다면 Playwright가 더 좋은 선택이에요.
1단계: Selenium과 ChromeDriver 설치하기
1pip install selenium webdriver-manager
webdriver-manager는 ChromeDriver 버전을 자동으로 맞춰줘서 “ChromeDriver 버전이 맞지 않음” 같은 골칫거리를 없애줘요:
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)
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# 상품 카드가 렌더링될 때까지 기다리기 (명시적 대기 > time.sleep)
6WebDriverWait(driver, 15).until(
7 EC.presence_of_element_located((By.CSS_SELECTOR, '[data-test="product-title"]'))
8)
명시적 대기는 정말 중요해요. time.sleep(10)은 빠른 로딩에서는 시간을 낭비하고, 느린 로딩에서는 충분하지 않아요. 양쪽 다 손해예요. WebDriverWait은 요소가 나타나거나 타임아웃이 날 때까지 500ms마다 계속 확인해요.
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번 스크롤하면 스크롤하지 않았을 때의 4~5개 대신 8개 이상의 상품을 가져올 수 있어요. 각 스크롤은 사람처럼 보이도록 200~300px 정도가 좋아요.
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 선택자는 다음과 같아요(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" |
5단계: 상품 리뷰 크롤링하기(보너스)
개별 상품 페이지로 이동해 리뷰 섹션까지 스크롤한 뒤 리뷰 데이터를 추출하세요:
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 연동으로 로드되며, 페이지네이션(최대 51페이지), 최신순 정렬, 사진만 보기 필터를 지원해요. 기준 Selenium은 항목당 약 5.1초 정도예요.
작업이 끝나면 브라우저를 닫는 것도 잊지 마세요:
1driver.quit()
방법 3: Redsky API로 Target.com 크롤링하기
Target 프런트엔드는 내부 API인 redsky.target.com에서 모든 데이터를 가져와요. Python으로 직접 호출할 수 있고, HTML 파싱도 브라우저도 JavaScript 렌더링도 필요 없어요. 응답은 가격, 평점, 리뷰, 이미지, 재고, 배송, 사양, 옵션까지 포함하는 40개 이상의 필드를 담은 깔끔한 JSON이에요. 대량 상품 데이터라면 속도와 안정성 면에서 이 방법이 압도적으로 좋아요.
1단계: Chrome DevTools로 Redsky API 찾기
대부분의 튜토리얼은 이 부분을 그냥 건너뛰어요. 직접 API를 찾는 방법은 이래요:
- Chrome에서 Target 상품 페이지 하나를 엽니다
- DevTools(F12)를 열고 Network 탭으로 이동합니다
- Fetch/XHR로 필터링합니다
- 페이지를 새로고침합니다
redsky.target.com또는redsky.a]target.com으로 가는 요청을 찾습니다- 하나를 클릭해서 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 키(정적이며 자주 바뀌지 않음. 엔드포인트마다 다른 키를 사용)tcin— Target.com Item Number(8자리 상품 ID)store_id— Target 매장 위치zip— 배송/수령 정보를 위한 우편번호
요청 헤더에서 API 키를 추출하세요. URL 안에 쿼리 파라미터로 포함돼 있어요.
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\}")
HTML 파싱이 전혀 필요 없어요. 응답은 구조화돼 있고, 깔끔하고, 빠르죠.
3단계: API로 상품 검색 결과 크롤링하기
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__에 포함된 JSON에서 추출하면 돼요.
4단계: 병렬 요청으로 규모 확장하기
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개 스레드에 요청마다 2~5초 정도의 랜덤 지연을 두는 게 좋아요. Target의 속도 제한은 IP당 예요.
Redsky API에 대한 중요한 주의사항
이걸 프로덕션 파이프라인으로 만들기 전에, 몇 가지 알아둘 점이 있어요:
- API 키는 정적이지만 엔드포인트별로 다릅니다. Redsky의 각 엔드포인트는 서로 다른 키를 사용해요. 자주 바뀌진 않지만 Target이 언제든 바꿀 수 있어요.
- 문서화되지 않은 내부 API예요. Target 엔지니어링 팀은 했기 때문에 법적 위험은 줄어들지만, SLA가 있는 공식 퍼블릭 API는 아니에요.
- 상품 옵션(색상, 크기)은 각각 고유한 TCIN을 가집니다. 각 옵션을 따로 조회해야 해요.
Sec-Fetch-*헤더가 빠지면 즉시 차단될 수 있어요. 자주 놓치는 부분이니Sec-Fetch-Site,Sec-Fetch-Mode,Sec-Fetch-Dest는 항상 포함하세요.
차단 없이 대규모로 Target.com을 크롤링하는 팁
아래 방법들은 어떤 방식이든 프로덕션 규모에 적용할 수 있어요.
데이터센터가 아니라 주거용 프록시를 사용하세요
Target의 Akamai 구현은 데이터센터 IP 대역을 보는 즉시 표시해요. 지속적인 크롤링에는 주거용 프록시가 사실상 필수예요. 가격은 천차만별인데, , 시작하고, 대량 사용 시 GB당 $3~4 수준까지 내려가요.
프록시 풀을 지원한다면 50~100건마다, 아니면 가능하면 매 요청마다 IP를 바꾸세요.
curl_cffi로 TLS 지문을 흉내 내세요
가장 효과가 큰 단일 변화예요. requests의 드롭인 대체제로 쓰면 돼요:
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")
는 chrome99부터 chrome146까지의 Chrome 버전은 물론 Safari, Edge, 모바일 변형도 지원해요. 동기 모드에서는 tls_client보다 .
현실적인 요청 속도와 헤더를 설정하세요
- 랜덤 지연: 요청 사이에 2~7초 간격을 두세요(고정 간격이 아니라 랜덤이어야 해요)
- User-Agent 순환: 실제 브라우저 User-Agent 문자열 5~10개를 풀로 두고 돌리세요
- 세션 워밍업: 상품 페이지를 바로 치지 말고 먼저
target.com홈을 방문해서 쿠키를 만들어요 - 헤더 일관성:
Sec-Ch-Ua는 실제 User-Agent가 주장하는 브라우저 버전과 일치해야 해요.Sec-Ch-Ua-Platform도 OS와 맞아야 해요. 불일치는 바로 티가 나요. - 세션 유지: 세션 내에서는 요청 사이에 쿠키를 유지하세요. 주거용 프록시를 돌릴 때 48시간 세션 안정성을 권장해요.
코드 없이 Target.com을 크롤링하기: Thunderbit로 해결하는 방법
Target.com은 정말로 프로그램으로 크롤링하기 까다로운 리테일 사이트 중 하나예요. JavaScript 렌더링, Akamai의 TLS 지문 식별, 데이터센터 프록시 감지, ChromeDriver 버전 문제까지 — 신경 쓸 게 많아요. Python을 배우는 입장이라면 좋은 연습이지만, 실제 업무에 필요한 Target 상품 데이터라면 시간 대비 효율이 잘 안 나올 수 있어요.
엔지니어링 프로젝트 없이 데이터를 바로 얻고 싶은 분이라면 가 어려운 부분을 자동으로 처리해 줘요.
Thunderbit가 Target.com의 난제를 처리하는 방식
Thunderbit의 AI 웹 스크래퍼는 브라우저 안에서 작동하므로 JavaScript를 자연스럽게 렌더링해요. Selenium 설정도 없고, 헤드리스 브라우저 구성도 없고, ChromeDriver 버전 관리도 필요 없어요. 브라우저 자체가 스크래퍼예요.
흐름은 간단해요:
- 를 설치하고 Target 상품 페이지나 검색 페이지로 이동해요
- "AI Suggest Fields"를 클릭하면 Thunderbit가 페이지를 읽고 열 이름(Product Title, Price, Rating, Image URL 등)을 제안해요
- "Scrape"를 클릭하면 렌더링된 페이지에서 바로 데이터가 몇 초 만에 추출돼요
프록시 설정도 필요 없고, TLS 지문을 흉내 낼 필요도 없고, None 결과를 마주할 일도 없어요.
Target 상품 목록과 상세 페이지 크롤링하기
여러 페이지를 다루는 작업이 특히 편해요. Target 검색 결과 페이지에서 상품 목록을 먼저 가져온 뒤, Subpage Scraping으로 각 상품 URL을 자동 방문해 상세 페이지 데이터까지 테이블에 채워 넣을 수 있어요. 페이지네이션 코드를 짜거나 브라우저 세션을 관리할 필요가 없어요.
결과는 Excel, Google Sheets, Airtable, Notion으로 바로 내보낼 수 있어요. csv.writer 보일러플레이트도, 파일 인코딩 문제도 없어요.
반복되는 Target.com 크롤링 자동화하기
가격 모니터링이나 재고 추적이 계속 필요하다면 Thunderbit의 Scheduled Scraper로 일정을 평범한 문장으로 적기만 하면 돼요(예: “매주 월요일 오전 9시”). 크론 작업도 없고, 서버 설정도 없고, VPS에서 Python 스크립트를 계속 띄워 둘 필요도 없어요. 특히 을 추적하는 이커머스 팀에 유용해요. 가 이미 자동 가격 크롤링을 사용하고 있고, 가격 인텔리전스의 평균 ROI는 이에요.
Target.com 크롤링에는 어떤 방법을 언제 써야 할까요
간단한 의사결정 기준은 아래와 같아요:
| 상황 | 추천 방법 |
|---|---|
| Python을 배우는 중이고, 작은 프로젝트예요 | 방법 1: Requests + BS4(정적 데이터와 __TGT_DATA__용) |
| 가격과 리뷰가 포함된 전체 상품 페이지가 필요해요 | 방법 2: Selenium / Playwright |
| 대량의 상품 데이터를 규모 있게 추출해야 해요 | 방법 3: Redsky API |
| 코딩 없이 빠르게 데이터가 필요해요 | Thunderbit (노코드) |
| 반복적인 가격 모니터링이 필요해요 | Thunderbit Scheduled Scraper 또는 Redsky API + cron |
| 한 번만 하는 리서치 프로젝트이고, 비기술 팀이 사용해요 | Thunderbit — 솔직히 가장 빠른 길이에요 |
프로덕션 데이터 파이프라인을 만든다면 방법 3(Redsky API)이 속도와 안정성 면에서 가장 좋아요. 단발성 리서치이거나 팀에 Python 전문성이 없다면 Thunderbit가 시간을 크게 아껴줘요. 웹 스크래핑을 배우는 중이라면 방법 1 → 방법 2 → 방법 3 순서가 각 단계마다 실제로 뭔가를 배우게 해 주는 자연스러운 흐름이에요.
Target.com 크롤링 시 법적·윤리적 고려사항
짧게라도 짚고 넘어갈 가치가 있어요. Target의 robots.txt에는 대략 120개 이상의 Disallow 경로가 있지만, 중요한 점은 /p/(상품)나 /c/(카테고리)는 차단하지 않는다는 거예요. 즉 상품 및 카테고리 페이지는 크롤링이 명시적으로 허용돼요. 반면 장바구니, 계정, 결제 페이지는 제한돼 있어요.
Target의 이용약관은 자동화된 접근을 금지해요. 다만 Redsky API가 라는 점( Target 엔지니어링 확인)은 API 기반 데이터 수집의 법적 위험을 낮춰줘요.
알아둘 만한 주요 판례는 다음과 같아요:
- (제9순회항소법원, 2022): 공개 데이터 크롤링은 CFAA 위반이 아님
- (2024): Meta 패소 — 공개 데이터 크롤링에 대해 CFAA 위반이 아니라고 판단
대규모 상업용 크롤링이라면 변호사와 상담하세요. 공개 데이터를 활용한 시장 조사, 가격 비교, 개인 프로젝트라면 법적으로 비교적 안전한 편이에요. 다만 항상 속도 제한을 존중하고 Target 서버에 과부하를 주지 마세요.
결론과 핵심 요약
Target.com이 까다롭다는 평가는 괜히 나온 게 아니에요. 순진한 Requests + BeautifulSoup 방식은 Target이 상품 데이터를 JavaScript로 렌더링하고, 응답을 받기도 전에 Akamai가 TLS 핸드셰이크를 지문으로 식별하기 때문에 실패해요. 하지만 올바른 방법을 쓰면 추출 자체는 어렵지 않아요.
신뢰도 순으로 정리한 3가지 방법:
- Redsky API — 대량 데이터에 가장 빠르고 안정적이며, 깔끔한 JSON을 돌려줘요. 다만 DevTools로 API 엔드포인트를 역공학해야 해요.
- Selenium / Playwright — JavaScript 렌더링을 처리해서 페이지에 있는 모든 내용을 가져올 수 있어요. 느리지만 포괄적이에요.
- Requests + BeautifulSoup — 정적 HTML과 포함된
__TGT_DATA__JSON에 한정돼요. 빠르지만 완전하지 않아요.
가장 큰 기술적 개선 포인트:
- 표준
requests대신curl_cffi를 써서 안티봇 우회율을 높이세요 - 주거용 프록시는 필수예요 — 데이터센터 IP는 즉시 플래그 처리돼요
- 모든 요청에
Sec-Fetch-*헤더를 포함하세요 — 빠지면 바로 차단돼요 - 세션 워밍업(먼저 홈페이지만 방문하기)은 성공률을 크게 높여줘요
그리고 여러분의 사용 사례에 Python이 굳이 값어치를 못 한다면, 이 JavaScript 렌더링, 안티봇 대응, 데이터 내보내기를 자동으로 처리해 줘요. 을 써 보고, 몇 시간 걸릴 일을 몇 분 만에 끝낼 수 있는지 확인해 보세요.
더 많은 크롤링 가이드와 데이터 추출 팁은 나 을 확인해 보세요.
자주 묻는 질문
Python Requests와 BeautifulSoup만으로 Target.com을 크롤링할 수 있나요?
부분적으로는 가능해요. 상품 페이지의 __TGT_DATA__ 스크립트 태그에서 상품 제목, URL, 일부 포함된 JSON 데이터를 추출할 수 있어요. 하지만 검색 결과 페이지의 가격, 평점, 리뷰, 재고는 JavaScript로 렌더링되므로 정적 HTTP 요청으로는 보이지 않아요. 완전한 데이터를 원한다면 Selenium/Playwright나 Redsky API를 사용하세요.
왜 제 Target.com 스크래퍼는 가격을 None으로 돌려주나요?
Target은 초기 페이지 로드 후 JavaScript로 가격 데이터를 불러와요. requests.get()을 사용하면 JavaScript가 실행되기 전의 사전 렌더링된 HTML 셸을 받게 돼요. 그래서 가격 요소는 응답에 문자 그대로 존재하지 않아요. JavaScript를 렌더링하는 헤드리스 브라우저(Selenium 또는 Playwright)를 쓰거나, Redsky API를 직접 호출해 JSON 데이터를 가져오거나, 렌더링된 브라우저 페이지에서 크롤링하는 같은 도구를 사용하세요.
Target.com을 크롤링하는 건 합법인가요?
현재 미국 판례법상 공개 데이터 크롤링은 일반적으로 허용돼요(hiQ 대 LinkedIn, Meta 대 Bright Data). Target의 robots.txt도 상품 및 카테고리 페이지 크롤링을 허용해요. 다만 Target의 이용약관은 자동 접근을 금지하므로 회색지대가 있어요. 공개 데이터를 활용한 시장 조사와 가격 비교라면 법적으로 비교적 타당한 편이에요. 대규모 상업 운영이라면 변호사와 상담하세요.
Target의 Redsky API는 무엇이고 어떻게 접근하나요?
Redsky는 Target 프런트엔드의 상품 데이터를 뒷받침하는 내부 API예요. 문서와 가입용 API 키가 있는 공개 API가 아니라, React 앱이 상품 페이지를 렌더링하기 위해 호출하는 백엔드예요. Chrome DevTools를 열고 Network 탭을 XHR/Fetch로 필터링한 뒤 redsky.target.com으로 가는 요청을 찾으면 엔드포인트를 발견할 수 있어요. API 키는 요청 URL의 쿼리 파라미터로 포함돼 있어요. Target 엔지니어링 팀은 이 API가 의도적으로 외부에 공개된 것이라고 확인했어요.
Target.com 크롤링 시 차단을 피하려면 어떻게 해야 하나요?
가장 효과가 큰 한 가지 변화는 표준 Python requests 대신 curl_cffi를 써서 브라우저 TLS 지문을 흉내 내는 거예요. 이것만으로 성공률이 올라가요. 그 외에도 주거용 프록시 사용(데이터센터 금지), User-Agent 문자열 순환, 요청 사이 2~7초 랜덤 지연, 모든 Sec-Fetch-* 헤더 포함, 홈페이지만 먼저 방문해 세션을 워밍업하는 방식이 좋아요. 또는 별도 설정 없이 안티봇 대응을 자동으로 처리하는 같은 도구를 쓰셔도 돼요.
더 알아보기