Shopify 스토어 URL 뒤에 /products.json만 붙여 보세요. API 키도, 로그인도, 골치 아픈 HTML 분석도 없이 정리된 JSON 데이터가 그대로 내려와요. 이커머스 데이터 쪽에서 일하는 사람이면 거의 다 아는 공공연한 비밀이에요.
저는 Thunderbit 공식 웹사이트 팀에서 사람들이 웹 데이터를 어떻게 뽑아 쓰는지 매일 들여다봐요. 그중에서도 Shopify는 단골 주제예요. 경쟁사 가격을 따라잡으려는 세일즈 팀, 카탈로그를 비교하는 운영팀, 새 공급업체를 찾는 구매 담당자까지 찾는 사람이 다양하거든요. Shopify 판매자는 482만 곳에 달하고 미국 이커머스의 약 30%를 차지하니, 건질 상품 데이터의 양도 어마어마하죠.
이 글에서는 처음부터 끝까지 다 짚어 봐요. 엔드포인트가 어떤 데이터를 주는지, 상품 수천 개를 페이지네이션으로 가져오는 법, 차단 안 당하게 속도를 조절하는 법, pandas로 중첩 JSON을 깔끔한 CSV·Excel로 펴는 법까지요. 남들이 잘 안 다루는 /collections.json·/meta.json도 소개하고, 코드 없이 바로 쓰고 싶은 분을 위한 노코드 방법도 보여 드릴게요.
Shopify의 /products.json 엔드포인트란? 왜 이렇게 긁기 쉬운가
Shopify 스토어에는 누구나 열 수 있는 {store-url}/products.json 엔드포인트가 하나씩 있어요. 여기서 구조화된 상품 데이터가 바로 나와요. API 키도 OAuth도 인증도 다 필요 없어요. 스토어 URL 끝에 /products.json만 붙이면 카탈로그 전체가 JSON 배열로 떨어져요.
지금 바로 allbirds.com/products.json이나 zoologistperfumes.com/products.json을 브라우저에 쳐보세요. 상품명, 가격, 변형, 이미지, 태그가 JSON으로 정리돼 보일 거예요.
HTML 테마를 직접 파싱하는 쪽은 어떨까요? 스토어마다 구조가 다르고, 중첩도 깊고, 판매자가 테마만 바꿔도 레이아웃이 통째로 달라져요. 둘을 나란히 놓으면 차이가 확 드러나요.
HTML 방식(번거로움):
<div class="product-card__info">
<h3 class="product-card__title">
<a href="/products/classic-blue-jeans">Classic Blue Jeans</a>
</h3>
<span class="price price--on-sale" data-product-price>$149.00</span>
</div>
JSON 방식(깔끔함):
{
"title": "Classic Blue Jeans",
"handle": "classic-blue-jeans",
"vendor": "Hiut Denim",
"variants": [{"price": "149.00", "sku": "HD-BLU-32", "available": true}]
}
일관성으로 보나 안정성으로 보나 파싱 편의로 보나 JSON이 압도적이에요. 이 엔드포인트는 핵심 쿼리 파라미터 두 개를 받아요. 페이지당 최대 250개를 정하는 ?limit=(기본값은 30개), 그리고 페이지를 넘기는 ?page=예요. 뒤에 나올 코드에서 이 둘을 적극적으로 써요.
여기서 짚고 갈 점. 이건 공개 스토어프론트 엔드포인트지 Shopify Admin API가 아니에요. Admin API는 스토어 주인의 액세스 토큰이 있어야 하고, 주문·재고·고객 정보를 줘요. 반대로 공개 /products.json은 누구나 읽을 수 있는 상품 데이터만 보여 줘요. 이 차이는 뒤에서 더 자세히 다룰 텐데, 실무에서 정말 자주 헷갈리는 지점이거든요.
한 가지 주의: 모든 스토어가 이 엔드포인트를 여는 건 아니에요. 테스트해 보니 약 71%에서 유효한 JSON이 나왔고(allbirds.com, gymshark.com, colourpop.com, kyliecosmetics.com 등 동작), 커스텀 설정 스토어 일부는 404를 뱉었어요(hiutdenim.co.uk, bombas.com). 확인은 간단해요. {store-url}/products.json을 브라우저에 직접 쳐서 결과만 보면 돼요.
왜 Python으로 Shopify를 긁을까? 실전 비즈니스 활용
굳이 왜 하느냐. 답은 ROI예요. 미국 소매업체의 81%가 경쟁 정보용으로 자동 가격 스크래핑을 이미 돌리고 있어요. 2020년의 34%에서 크게 뛴 수치죠. 게다가 가격 전략을 1%만 다듬어도 이익이 평균 11.1% 늘어난다는 연구도 있어요. 그냥 숫자가 아니라 통장에 찍히는 돈예요.
제가 현장에서 제일 자주 보는 활용은 이런 것들이에요.
| 활용 사례 | 혜택을 받는 대상 | 얻을 수 있는 정보 |
|---|---|---|
| 경쟁사 가격 모니터링 | 이커머스 운영팀 | 경쟁사 카탈로그 전반의 가격 변동, 할인, 비교를 통한 가격 추적 |
| 상품 리서치 및 소싱 | 구매 / 머천다이징 | 상품 기능, 변형, 소재, 재고 가능 여부 비교 |
| 리드 생성 | 세일즈 팀 | 스토어 카탈로그에서 공급업체명, 브랜드 정보, 연락처 추출 |
| 시장 및 카테고리 분석 | 마케팅 팀 | 상품 구성, 태그, 컬렉션 구조, 포지셔닝 파악 |
| 재고 및 판매 가능 여부 추적 | 공급망 팀 | 변형 단위 재고 상태(available: true/false)의 시간별 모니터링 |
| 신규 상품 감지 | 제품 팀 | created_at 타임스탬프로 경쟁사의 신규 출시 탐지 |
이런 일엔 Python이 딱이에요. 스크래핑 개발자의 69.6%가 주력 언어로 Python을 써요. requests(HTTP), pandas(데이터 가공), httpx(비동기) 같은 생태계 덕에, "URL 하나"에서 "완성된 스프레드시트"까지 80줄도 안 되는 코드로 끝나거든요.
products.json 전체 필드 참고표: 한눈에 보기
다른 튜토리얼은 보통 title, id, handle 정도만 보여 주고 넘어가요. 그런데 Shopify JSON 응답에는 상품·변형·이미지·옵션을 통틀어 40개가 넘는 필드가 들어 있어요. 코드를 짜기 전에 뭐가 있는지 알아 두면, 나중에 다시 긁느라 시간 버릴 일이 없어요.
아래 표는 2026년 4월 16일에 실시간 /products.json 응답을 받아 정리한 거예요. 이 구조는 엔드포인트를 여는 스토어라면 어디서나 똑같아요.
상품 수준 필드
| 필드 | 데이터 유형 | 예시 값 | 비즈니스 활용 |
|---|---|---|---|
id | 정수 | 123456789 | 중복 제거를 위한 고유 상품 식별자 |
title | 문자열 | "Classic Blue Jeans" | 카탈로그 및 비교용 상품명 |
handle | 문자열 | "classic-blue-jeans" | URL 슬러그—상품 페이지 링크는 {store}/products/{handle} 형식으로 구성 |
body_html | 문자열(HTML) 또는 null | Our best-selling... | 콘텐츠 분석 및 SEO 리서치를 위한 상품 설명 |
vendor | 문자열 | "Hiut Denim" | 리드 생성이나 소싱을 위한 브랜드/공급업체명 |
product_type | 문자열 | "Jeans" | 시장 분석용 카테고리 분류 |
created_at | ISO 날짜/시간 | "2024-01-15T10:30:00-05:00" | 상품이 추가된 시점 추적(신규 출시 감지) |
updated_at | ISO 날짜/시간 | "2025-03-01T08:00:00-05:00" | 최근 카탈로그 변경 감지 |
published_at | ISO 날짜/시간 | "2024-01-16T00:00:00-05:00" | 스토어프론트에 상품이 공개된 시점 확인 |
tags | 문자열 배열 | ["organic", "women", "straight-leg"] | SEO, 분류, 트렌드 파악을 위한 키워드/태그 분석 |
variants | 객체 배열 | (아래 변형 필드 참고) | 변형별 가격, SKU, 판매 가능 여부 |
images | 객체 배열 | (아래 이미지 필드 참고) | 카탈로그 및 시각 분석용 이미지 URL |
options | 객체 배열 | [{"name": "Size", "values": ["S","M","L"]}] | 상품 구성(사이즈, 색상, 소재) 파악 |
변형 수준 필드(각 상품 내부 중첩)
| 필드 | 데이터 유형 | 예시 | 활용 사례 |
|---|---|---|---|
id | 정수 | 987654321 | 고유 변형 식별자 |
title | 문자열 | "32 / Blue" | 변형 표시명 |
sku | 문자열 | "HD-BLU-32" | 재고 시스템과의 SKU 매칭 |
price | 문자열 | "185.00" | 가격 모니터링(문자열이라 계산할 땐 float 변환 필요) |
compare_at_price | 문자열 또는 null | "200.00" | 원래 가격—할인 추적에 필수 |
available | 불리언 | true | 재고 가능 여부(공개적으로 확인 가능한 유일한 재고 지표) |
weight | 실수 | 1.2 | 배송/물류 분석 |
option1, option2, option3 | 문자열 | "32", "Blue", null | 개별 옵션 값 |
created_at, updated_at | ISO 날짜/시간 | — | 변형 수준 변경 추적 |
이미지 수준 필드
| 필드 | 데이터 유형 | 예시 | 활용 사례 |
|---|---|---|---|
id | 정수 | 111222333 | 고유 이미지 식별자 |
src | 문자열(URL) | "https://cdn.shopify.com/..." | 이미지 직접 다운로드 링크 |
alt | 문자열 또는 null | "Front view of jeans" | 접근성 분석을 위한 대체 텍스트 |
position | 정수 | 1 | 이미지 순서 |
width, height | 정수 | 2048, 2048 | 이미지 크기 |
공개 엔드포인트에 없는 항목
미리 알아 둬야 할 함정이 하나 있어요. 공개 /products.json에는 inventory_quantity가 없어요. 보안상의 이유로 2017년 12월에 공개 JSON 엔드포인트에서 빠졌거든요. 공개로 얻을 수 있는 재고 신호는 각 변형의 불리언 available(true 또는 false)뿐이에요. 실제 재고 수량을 보려면 스토어 주인 자격으로 인증한 Admin API가 필요해요.
코드를 짜기 전에 이 표부터 보고 필요한 필드를 추려 두세요. 가격 모니터링이 목적이면 variants[].price, variants[].compare_at_price, variants[].available이 핵심이에요. 리드 생성이 목적이면 vendor, product_type, tags에 집중하면 돼요. 용도에 맞게 걸러야 CSV도 훨씬 깔끔해져요.
products.json을 넘어서: 컬렉션, 메타, 그 밖의 엔드포인트
경쟁 글 대부분이 이쪽 엔드포인트를 빼먹어요. 진지하게 경쟁 정보를 파려면 은근히 쓸모가 많은데도요.
/collections.json — 스토어 전체 카테고리
스토어의 컬렉션(카테고리)을 제목·handle·설명·상품 수까지 묶어 돌려줘요. zoologistperfumes.com, allbirds.com, gymshark.com에서 확인했더니 다들 유효한 JSON이 나왔어요.
{
"collections": [
{
"id": 308387348539,
"title": "Attars",
"handle": "attars",
"published_at": "2026-03-29T12:20:32-04:00",
"products_count": 1,
"image": { "src": "https://cdn.shopify.com/..." }
}
]
}
경쟁사가 카탈로그를 어떻게 쪼개 놨는지 궁금하면 이 엔드포인트를 보면 돼요.
/collections/{handle}/products.json — 카테고리별 상품
특정 컬렉션으로 걸러진 상품을 줘요. 구조는 /products.json과 같은데 범위가 한 카테고리로 좁혀져요. 경쟁사의 "Sale"이나 "New Arrivals" 컬렉션만 따로 지켜보고 싶을 때 아주 유용해요.
/meta.json — 스토어 수준 메타데이터
스토어 이름·설명·통화·국가, 그리고 특히 쓸 만한 published_products_count를 돌려줘요. 이 수치만 있으면 필요한 페이지 수를 미리 정확히 계산할 수 있어요. ceil(published_products_count / 250)이면 끝이죠. 빈 응답이 나올 때까지 페이지 번호를 무작정 올릴 필요가 없어져요.
어떤 엔드포인트를 써야 할까?
| 원하는 것 | 엔드포인트 | 인증 필요? |
|---|---|---|
| 전체 상품(공개) | /products.json | 아니요 |
| 특정 카테고리의 상품 | /collections/{handle}/products.json | 아니요 |
| 스토어 메타데이터 + 상품 수 | /meta.json | 아니요 |
| 전체 컬렉션(카테고리) | /collections.json | 아니요 |
| 주문/매출 데이터(자신의 스토어만) | Admin API /orders.json | 예(API 키) |
| 재고 수량(자신의 스토어만) | Admin API /inventory_levels.json | 예 |
커뮤니티 단골 질문이 "경쟁사가 몇 개 팔았는지 긁을 수 있나요?"인데, 답은 짧아요. 공개 엔드포인트로는 안 돼요. 판매 데이터와 실제 재고 수량은 인증된 Admin API, 즉 스토어 주인 권한이 있어야만 보여요. 공개 엔드포인트가 주는 건 상품 카탈로그 데이터뿐예요.

Python으로 Shopify 긁는 법: 단계별 세팅
- 난이도: 초급
- 예상 소요 시간: 약 15분(세팅 + 첫 스크래핑)
- 준비물: Python 3.11 이상,
pip, 터미널, 그리고 긁을 Shopify 스토어 URL
1단계: Python과 라이브러리 설치
먼저 Python 3.11 이상이 깔려 있는지 확인하세요(pandas 3.0.x가 이걸 요구해요). 그다음 필요한 라이브러리 두 개를 설치해요.
pip install requests pandas
Excel로 내보낼 거면 이것도 추가하세요.
pip install openpyxl
스크립트 맨 위에는 이 import를 넣어요.
import requests
import pandas as pd
import time
import random
import json
실행했을 때 import 에러가 안 나야 정상이에요. pandas에서 버전 오류가 뜬다면 Python을 3.12로 올리세요.
2단계: /products.json에서 상품 데이터 가져오기
아래는 스토어 URL을 받아 엔드포인트를 호출하고 파싱된 JSON을 돌려주는 기본 함수예요.
def fetch_products_page(store_url, page=1, limit=250):
"""Shopify 스토어에서 상품 한 페이지를 가져옵니다."""
url = f"{store_url.rstrip('/')}/products.json"
params = {"limit": limit, "page": page}
headers = {
"User-Agent": "Mozilla/5.0 (compatible; ProductResearch/1.0)"
}
response = requests.get(url, params=params, headers=headers, timeout=30)
response.raise_for_status()
return response.json().get("products", [])
핵심만 짚으면 이래요.
limit=250: Shopify가 페이지당 허용하는 최대치예요. 기본값이 30이라, 250으로 명시하면 요청 횟수를 최대 8배까지 줄예요.User-Agent헤더: 꼭 현실적인 값으로 넣으세요. User-Agent 없는 요청은 Shopify 봇 방지에 더 잘 걸려요.timeout=30: 요청 하나가 무한정 매달리는 일을 막아 줘요.
아는 스토어로 한번 돌려 보세요.
products = fetch_products_page("https://allbirds.com")
print(f"Fetched {len(products)} products")
print(f"First product: {products[0]['title']}")
Fetched 250 products와 첫 상품명이 찍히면 잘 된 거예요.
3단계: 페이지네이션으로 모든 상품 긁기
요청 한 번으로는 최대 250개까지만 와요. 그런데 웬만한 스토어는 그보다 상품이 훨씬 많죠(Allbirds만 해도 1,420개 이상). 응답이 빌 때까지 페이지를 계속 돌려야 해요.
def scrape_all_products(store_url, delay=1.0):
"""페이지네이션을 처리하면서 Shopify 스토어의 모든 상품을 스크래핑합니다."""
all_products = []
page = 1
while True:
print(f"Fetching page {page}...")
products = fetch_products_page(store_url, page=page, limit=250)
if not products:
print(f"No more products. Total: {len(all_products)}")
break
all_products.extend(products)
print(f" Got {len(products)} products (total so far: {len(all_products)})")
page += 1
# 예의를 지키기 위해 요청 사이에 잠시 대기
time.sleep(delay + random.uniform(0, 0.5))
return all_products
products가 비어서 돌아오면 끝까지 왔다는 신호예요.
time.sleep()에 랜덤 지터를 살짝 더하면, Shopify의 비공식 속도 제한(~초당 2회) 아래로 머무는 데 도움이 돼요.
팁: /meta.json을 먼저 받아 두면 전체 상품 수를 미리 알 수 있어요. 필요한 페이지 수도 pages = ceil(product_count / 250)으로 딱 떨어지게 계산되죠. 마지막에 빈 요청 하나를 더 보내는 낭비를 피할 수 있어요.
4단계: 필요한 필드만 추려 내기
모든 상품을 Python 딕셔너리 리스트로 받았으면, 이제 필요한 필드만 뽑으면 돼요. 아래는 가격 모니터링에 제일 흔히 쓰는 필드를 가져오는 코드예요.
def extract_product_data(products):
"""상품에서 핵심 필드를 추출하고 변형을 평탄화합니다."""
rows = []
for product in products:
for variant in product.get("variants", []):
rows.append({
"product_id": product["id"],
"title": product["title"],
"handle": product["handle"],
"vendor": product.get("vendor", ""),
"product_type": product.get("product_type", ""),
"tags": ", ".join(product.get("tags", [])),
"created_at": product.get("created_at", ""),
"variant_id": variant["id"],
"variant_title": variant.get("title", ""),
"sku": variant.get("sku", ""),
"price": variant.get("price", ""),
"compare_at_price": variant.get("compare_at_price", ""),
"available": variant.get("available", ""),
"image_url": product["images"][0]["src"] if product.get("images") else ""
})
return rows
이러면 변형마다 한 행씩 생겨요. 가격 비교엔 이 형태가 제일 편해요. 가령 "Classic Blue Jeans" 하나가 변형 12개(사이즈 6 × 색상 2)를 가질 수 있고, 변형마다 가격과 재고 상태가 다 다를 수 있으니까요.
pandas로 Shopify 데이터를 CSV·Excel로 내보내기
다른 튜토리얼은 원시 JSON을 파일에 그냥 쏟아 붓고 끝내는 경우가 많아요. 개발자한테야 괜찮지만, 금요일까지 스프레드시트가 필요한 이커머스 분석가에겐 별 쓸모가 없죠.
문제는 Shopify JSON이 중첩 구조라는 점이에요. 상품 하나에 변형이 여럿이고, 변형마다 가격·SKU·재고 상태가 제각각이거든요. 이걸 행과 열로 바꾸려면 pandas 손질이 필요해요.
중첩 JSON을 깔끔한 테이블로 펴기
쓰임새에 따라 두 가지 방식이 있어요.
옵션 A: 변형별 1행(가격 모니터링·재고 추적에 최적)
# 4단계의 extract_product_data 함수 사용
products = scrape_all_products("https://allbirds.com")
rows = extract_product_data(products)
df = pd.DataFrame(rows)
print(f"DataFrame shape: {df.shape}")
print(df.head())
이러면 각 행이 고유한 상품-변형 조합이 되는 평면 테이블이 나와요. 상품 500개에 상품당 변형이 평균 4개라면, 약 2,000행짜리 DataFrame이 만들어져요.
옵션 B: 상품 요약별 1행(카탈로그 개요에 최적)
def summarize_products(products):
"""변형 전체의 최소/최대 가격을 포함해 상품당 1행으로 요약합니다."""
rows = []
for product in products:
prices = [float(v["price"]) for v in product.get("variants", []) if v.get("price")]
rows.append({
"product_id": product["id"],
"title": product["title"],
"vendor": product.get("vendor", ""),
"product_type": product.get("product_type", ""),
"variant_count": len(product.get("variants", [])),
"min_price": min(prices) if prices else None,
"max_price": max(prices) if prices else None,
"any_available": any(v.get("available", False) for v in product.get("variants", [])),
"tags": ", ".join(product.get("tags", []))
})
return rows
CSV, Excel, Google Sheets로 내보내기
# CSV 내보내기(Excel이 특수문자를 잘 처리하도록 utf-8-sig 사용)
df.to_csv("shopify_products.csv", index=False, encoding="utf-8-sig")
# Excel 내보내기(openpyxl 필요)
df.to_excel("shopify_products.xlsx", index=False, engine="openpyxl")
print("shopify_products.csv 및 shopify_products.xlsx로 내보냈습니다")
Google Sheets엔 gspread 라이브러리를 서비스 계정과 함께 쓸 수 있어요. 다만 솔직히 대부분은 CSV로 저장한 뒤 Google Drive에 올리는 쪽이 더 빠르고 단순해요.
운영 환경용 Python 스크래핑: 속도 제한, 재시도, 차단 회피
기본 스크립트는 작은 스토어에선 잘 돌아가요. 그런데 상품이 5,000개를 넘거나 여러 스토어를 연달아 긁기 시작하면 얘기가 달라져요.
Shopify 속도 제한과 차단 동작 이해하기
Shopify 공개 JSON 엔드포인트엔 Admin API의 leaky bucket 같은 공식 속도 제한이 명시돼 있진 않아요. 다만 직접 테스트한 결과는 이래요.
- 안전 구간: 스토어당 초당 약 2회 요청
- 완만한 한계: 시간당이 아니라 분당 약 40회 요청부터 제한이 걸리기 시작
- HTTP 429: "Too Many Requests"—흔한 속도 제한 응답
- HTTP 430: Shopify 고유의 차단 코드(단순 속도 제한이 아님)
- HTTP 403 또는 CAPTCHA 리다이렉트: 일부 스토어의 Cloudflare 추가 보호
AWS Lambda나 Google Cloud Run 같은 공유 클라우드에서 보내는 요청은 특히 잘 막혀요. 그 IP 대역의 악용 비율이 높기 때문예요.
Shopify를 안정적으로 긁는 법
내 컴퓨터에서만 되는 수준에서 실제 운영 가능한 수준까지 올리는 길을 정리하면 이래요.
| 수준 | 기법 | 신뢰성 |
|---|---|---|
| 기본 | requests.get() + ?page= | 대규모 카탈로그에서 깨질 수 있고 차단될 수 있음 |
| 중급 | requests.Session() + ?limit=250 + time.sleep(1) + 429 재시도 | 대부분의 스토어에서 잘 동작 |
| 고급 | 비동기 httpx + User-Agent 순환 + 지수 백오프 | 운영 환경용, 1만 개 이상 상품도 대응 가능 |
중급 수준(대부분에게 추천):
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_session():
"""속도 제한에 대응하는 자동 재시도 로직이 있는 세션을 만듭니다."""
session = requests.Session()
retries = Retry(
total=5,
backoff_factor=1, # 대기: 0.5초, 1초, 2초, 4초, 8초
status_forcelist=[429, 430, 500, 502, 503, 504],
respect_retry_after_header=True
)
session.mount("https://", HTTPAdapter(max_retries=retries))
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
})
return session
Retry 설정이 지수 백오프로 429 응답을 알아서 처리해 줘요. backoff_factor=1이면 재시도 사이 대기가 0.5초 → 1초 → 2초 → 4초 → 8초로 늘어나요. requests.Session()을 재사용하면 연결 풀링이 적용돼, 같은 도메인에 여러 번 요청할 때 오버헤드도 줄어들고요.
User-Agent 순환: 여러 스토어를 긁는다면 현실적인 브라우저 User-Agent 문자열 3~5개를 번갈아 쓰세요. 속이려는 게 아니라, 매 요청마다 똑같은 헤더만 보내는 봇처럼 안 보이게 하려는 거예요.
CSV 내보내기까지 다 담은 완성형 Shopify 스크래핑 스크립트
아래는 지금까지 내용을 한데 합친, 복사해서 바로 쓸 수 있는 전체 스크립트예요. 주석 빼면 실제 코드는 약 75줄 정도고, Allbirds(1,420개), ColourPop(2,000개 이상), Zoologist Perfumes(소규모 카탈로그)에서 테스트했어요.
import requests
import pandas as pd
import time
import random
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_session():
"""속도 제한 대응용 재시도 로직이 있는 세션을 만듭니다."""
session = requests.Session()
retries = Retry(
total=5,
backoff_factor=1,
status_forcelist=[429, 430, 500, 502, 503, 504],
respect_retry_after_header=True
)
session.mount("https://", HTTPAdapter(max_retries=retries))
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/125.0.0.0 Safari/537.36"
})
return session
def scrape_shopify(store_url, delay=1.0):
"""/products.json을 통해 Shopify 스토어의 모든 상품을 스크래핑합니다."""
session = create_session()
all_products = []
page = 1
base_url = f"{store_url.rstrip('/')}/products.json"
while True:
print(f" Page {page}...", end=" ")
resp = session.get(base_url, params={"limit": 250, "page": page}, timeout=30)
resp.raise_for_status()
products = resp.json().get("products", [])
if not products:
break
all_products.extend(products)
print(f"{len(products)} products (total: {len(all_products)})")
page += 1
time.sleep(delay + random.uniform(0, 0.5))
return all_products
def flatten_to_variants(products):
"""중첩된 상품 JSON을 변형별 1행 구조로 평탄화합니다."""
rows = []
for p in products:
base = {
"product_id": p["id"],
"title": p["title"],
"handle": p["handle"],
"vendor": p.get("vendor", ""),
"product_type": p.get("product_type", ""),
"tags": ", ".join(p.get("tags", [])),
"created_at": p.get("created_at", ""),
"updated_at": p.get("updated_at", ""),
"image_url": p["images"][0]["src"] if p.get("images") else "",
}
for v in p.get("variants", []):
row = {**base}
row["variant_id"] = v["id"]
row["variant_title"] = v.get("title", "")
row["sku"] = v.get("sku", "")
row["price"] = v.get("price", "")
row["compare_at_price"] = v.get("compare_at_price", "")
row["available"] = v.get("available", "")
rows.append(row)
return rows
if __name__ == "__main__":
STORE_URL = "https://allbirds.com" # 대상 스토어로 바꾸세요
OUTPUT_CSV = "shopify_products.csv"
OUTPUT_EXCEL = "shopify_products.xlsx"
print(f"Scraping {STORE_URL}...")
products = scrape_shopify(STORE_URL)
print(f"\nTotal products scraped: {len(products)}")
print("Flattening to variant-level rows...")
rows = flatten_to_variants(products)
df = pd.DataFrame(rows)
print(f"DataFrame: {df.shape[0]} rows x {df.shape[1]} columns")
df.to_csv(OUTPUT_CSV, index=False, encoding="utf-8-sig")
df.to_excel(OUTPUT_EXCEL, index=False, engine="openpyxl")
print(f"\nExported to {OUTPUT_CSV} and {OUTPUT_EXCEL}")
실행은 python scrape_shopify.py로 하면 돼요. Allbirds 기준 약 45초쯤 걸리고, 변형 단위로 5,000행 이상이 담긴 CSV가 생겨요. 터미널 출력은 대략 이런 모양이에요.
Scraping https://allbirds.com...
Page 1... 250 products (total: 250)
Page 2... 250 products (total: 500)
...
Page 6... 170 products (total: 1420)
Total products scraped: 1420
Flattening to variant-level rows...
DataFrame: 5680 rows x 14 columns
Exported to shopify_products.csv and shopify_products.xlsx
Python 없이 클릭 두 번으로 Shopify 긁기: Thunderbit 노코드 방법
누구나 Python 설치하고, import 오류 잡고, 스크래핑 스크립트를 관리하고 싶어 하진 않아요. 내일 아침까지 경쟁사 가격이 필요한 세일즈 담당자라면 Python은 좀 과하죠.
그래서 저희가 만든 게 Chrome 확장으로 돌아가는 AI 웹 스크래퍼, Thunderbit 공식 웹사이트예요. 코드도 API 키도 환경 세팅도 필요 없어요.
Thunderbit Chrome 확장 프로그램 사용해보기
Thunderbit가 Shopify를 긁는 방식
Thunderbit엔 Shopify 상품 페이지에 맞춰 미리 세팅된 전용 Shopify Scraper 템플릿이 있어요. Thunderbit Chrome 확장 프로그램 다운로드 페이지에서 설치한 뒤 Shopify 스토어로 가서 "Scrape"만 누르면 돼요. 템플릿이 상품명·설명·가격·변형·이미지·공급업체 정보를 알아서 뽑아 줘요.
템플릿이 딱 안 맞는 스토어(커스텀 테마, 특이한 레이아웃 등)라도 괜찮아요. Thunderbit의 AI 필드 추천이 페이지를 읽고 열 이름을 자동으로 만들어 주거든요. 이 열은 직접 고칠 수도 있고, "compare_at_price가 설정된 상품만 추출"처럼 조건을 줄 수도 있어요.
Python 스크립트가 하던 일을 그대로 대신하는 기능은 이래요.
- 서브페이지 스크래핑: 상품 상세 페이지를 하나씩 자동 방문해 전체 설명·리뷰·변형 정보를 표에 채워 줘요. Python으로 여러 페이지를 도는 것과 같은 일이지만 코드는 안 짜도 돼요.
- 자동 페이지네이션: 클릭형 페이지네이션도, 무한 스크롤도 별도 설정 없이 처리해요.
- 예약 스크래핑: 정기 가격 모니터링용으로 "매주 월요일 오전 9시" 같은 반복 작업을 걸어 둘 수 있어요. cron도 서버도 필요 없어요.
- 무료 내보내기: 모든 플랜에서 CSV·Excel·Google Sheets·Airtable·Notion으로 내보내요.
Python 스크립트 vs. Thunderbit: 솔직한 비교
| 항목 | Python 스크립트 | Thunderbit(노코드) |
|---|---|---|
| 설정 시간 | 15~60분(환경 + 코드) | 약 2분(Chrome 확장 설치) |
| 코딩 필요 여부 | 예(Python) | 아니요 |
| 커스터마이징 | 무제한 | AI 추천 필드 + 사용자 프롬프트 |
| 페이지네이션 처리 | 직접 코딩 필요 | 자동 |
| 내보내기 형식 | 직접 구현(CSV/Excel) | CSV, Excel, Google Sheets, Airtable, Notion(무료) |
| 예약 실행 | cron 작업 + 호스팅 | 내장 스케줄러 |
| 속도 제한 대응 | 재시도/백오프 직접 구현 | 자동 처리 |
| 적합한 대상 | 개발자, 대규모 데이터 파이프라인 | 비즈니스 사용자, 빠른 추출, 반복 모니터링 |
완전한 제어가 필요하거나 큰 데이터 파이프라인에 끼워 넣어야 한다면 Python이에요. 빠르게 뽑고 코드 관리는 하기 싫다면 Thunderbit가 맞고요. 가격 비교용 웹 스크래핑을 더 파고들고 싶으면 관련 가이드를 따로 참고하세요.

Shopify 스토어 스크래핑 팁과 모범 사례
도구가 뭐든 아래 원칙은 그대로예요.
- 무조건
?limit=250을 쓰세요. 페이지당 기본 30개로 두면 같은 데이터를 받는데 요청만 8배로 늘어나요. - 스토어를 존중하세요: 요청 사이에 1~2초 지연을 넣으세요. 서버를 너무 빠르게 두드리면 매너에도 안 맞고 차단 위험도 커져요.
robots.txt부터 확인하세요: Shopify 기본robots.txt는/products.json을 막지 않아요. 그래도 스토어에 따라 커스텀 규칙이 붙을 수 있으니, 대규모로 긁기 전엔 확인하는 게 좋아요.- 원본 JSON을 먼저 로컬에 저장하고 처리하세요. 나중에 파싱 로직이 바뀌어도 다시 긁을 필요가 없어요. 펴기 전에
json.dump(all_products, open("raw_data.json", "w"))처럼 저장해 두면 두고두고 편해요. product.id로 중복을 제거하세요. 페이지 경계에서 같은 상품이 두 번 나오는 일이 있어요.df.drop_duplicates(subset=["product_id", "variant_id"])로 금방 정리돼요.- 가격은 계산 전에 float로 바꾸세요. Shopify는 가격을 숫자가 아니라 문자열("185.00")로 줘요.
- 엔드포인트 변경을 지켜보세요:
/products.json은 오랫동안 안정적이었지만, 이론상 Shopify가 제한할 가능성은 있어요. 스크래퍼가 갑자기 404를 뱉으면 먼저 스토어부터 직접 확인하세요.
더 탄탄한 스크래퍼를 만드는 법은 웹 스크래핑 도구 모범 사례 가이드를 참고하세요.
Shopify 스크래핑의 법적·윤리적 고려사항
짧지만 챙겨야 할 부분이에요.
/products.json 엔드포인트가 주는 건 누구나 볼 수 있는 상품 데이터예요. 방문자가 스토어를 둘러볼 때 보는 정보와 똑같죠. Shopify 이용약관에는 "서비스"에 접근할 때 "자동화된 수단"을 쓰지 말라는 문구가 있는데, 이건 플랫폼 자체(관리 대시보드, 결제)를 가리키는 것이지 공개 스토어프론트 데이터까지 포함한다고 보긴 어려워요. 2026년 4월 기준 Shopify 스크래핑 관련 소송은 아직 한 건도 없어요.
공개 데이터 스크래핑을 뒷받침하는 주요 판례도 있어요. hiQ v. LinkedIn 사건은 공개 접근 가능한 데이터의 스크래핑이 CFAA 위반이 아니라고 봤고, Meta v. Bright Data(2024)는 약관 제한이 로그인 상태에서만 적용된다고 판단했어요.
모범 사례는 이래요.
- 공개 접근 가능한 상품 데이터만 긁기
- 개인 정보나 고객 데이터는 긁지 않기
robots.txt와 속도 제한 지키기- 개인 정보를 다룬다면 개인정보보호법(미국이면 CCPA, EU면 GDPR) 준수하기(상품 카탈로그 데이터는 비개인 정보)
- 명확한 User-Agent 문자열로 자신을 밝히기
- 자기 Shopify 스토어를 Admin API로 긁는 건 언제나 문제없음
더 자세한 내용은 웹 스크래핑의 법적 쟁점 글에서 확인하세요.
결론 및 핵심 요약
Shopify 공개 /products.json은 이커머스 데이터 추출을 놀랄 만큼 쉽게 만들어 줘요. 흐름은 단순해요. /products.json을 붙이고 → Python으로 가져오고 → ?limit=250&page=로 페이지를 넘기고 → pandas로 펴고 → CSV나 Excel로 내보내면 끝이에요.
이 가이드가 다른 글보다 깊게 들어간 부분은 이래요.
- 완전한 필드 참고표: 코드 한 줄 짜기 전에 어떤 데이터가 있는지 정확히 파악(상품·변형·이미지 전반 40개+ 필드)
- 추가 엔드포인트:
/collections.json과/meta.json으로 카테고리 수준 인사이트와 스토어 메타데이터 확보 - 운영 가능한 기법: 세션 재사용, 지수 백오프, User-Agent 헤더,
?limit=250으로 실제 환경의 속도 제한 대응 - 제대로 된 CSV/Excel 내보내기: 원시 JSON 덤프가 아니라 pandas로 변형 단위 데이터를 펴기
- 노코드 방법: 코드보다 속도가 먼저라면 Thunderbit
단발성이든 반복이든 Shopify 데이터를 코드 없이 뽑고 싶다면 Thunderbit Chrome 확장 프로그램 다운로드 페이지를 써보세요. Shopify Scraper 템플릿이 페이지네이션부터 내보내기까지 다 처리해요. 여러 스토어를 대규모로 묶어 커스텀 파이프라인을 짜고 싶다면, 이 글의 Python 스크립트가 완전한 제어권을 줄 거예요.
영상 튜토리얼은 Thunderbit YouTube 채널에서 볼 수 있고, 관련 기술로는 Amazon 상품 스크래핑과 웹사이트 데이터를 Excel로 추출하기 가이드를 참고하세요.
Shopify 스크래핑을 위해 Thunderbit 사용해보기 Get Started Free
자주 묻는 질문
products.json으로 어떤 Shopify 스토어든 긁을 수 있나요?
대부분의 스토어는 기본적으로 이 엔드포인트를 열어요. 테스트해 보니 약 71%에서 유효한 JSON이 나왔어요. 다만 커스텀 설정이나 Cloudflare, 헤드리스 구조 같은 추가 보안층이 있는 스토어는 404를 뱉거나 요청을 막을 수 있어요. 빠른 확인법은 {store-url}/products.json을 브라우저에 직접 쳐보는 거예요. JSON이 보이면 정상이에요.
Shopify 스토어를 긁는 건 합법인가요?
가격·상품명·이미지·설명 같은 공개 상품 데이터는 보통 접근 가능하고, hiQ v. LinkedIn 같은 판례도 공개 정보 스크래핑을 뒷받침해요. 다만 각 스토어의 이용약관과 현지 법률은 꼭 확인하세요. 개인 정보나 고객 데이터는 모으지 말고, 속도 제한도 지켜야 해요.
Shopify 스토어에서 상품을 몇 개까지 긁을 수 있나요?
총개수에 하드 리미트는 없어요. ?limit=250&page=로 페이지를 넘기면 카탈로그 전체를 다 가져와요. 상품이 25,000개 넘는 대형 스토어라면 세션 재사용과 지연으로 속도 제한을 피하세요. /meta.json이 정확한 상품 수를 미리 알려 주니 페이지 수를 예측하는 데 도움이 돼요.
products.json과 Shopify Admin API의 차이는 뭔가요?
/products.json은 공개 엔드포인트라 인증 없이 누구나 읽기 전용 상품 데이터를 봐요. 반면 Admin API는 스토어 주인의 액세스 토큰이 있어야 하고, 주문·재고 수량·고객 데이터·쓰기 권한까지 줘요. 판매 데이터나 실제 재고 수량이 필요하면 Admin API 접근이 있어야 해요(즉 스토어 주인이거나 허가를 받아야 해요).
Python 없이 Shopify를 긁을 수 있나요?
물론이에요. Thunderbit 공식 웹사이트 같은 도구를 쓰면 Chrome 확장으로 코딩 없이 Shopify 스토어를 긁어요. 페이지네이션도 알아서 처리하고, CSV·Excel·Google Sheets·Airtable·Notion으로 바로 내보내요. 다른 언어가 편한 개발자라면 JavaScript·Ruby·Go 등 HTTP 요청과 JSON 파싱이 되는 언어 무엇으로든 같은 /products.json 엔드포인트를 쓸 수 있어요.
더 알아보기


