워싱턴 D.C.의 한 Safeway 매장에 있는 Lucerne 계란 한 판(12개입) 가격이 Instacart에서 $3.99, $4.28, $4.59, $4.69, $4.79로 각각 표시되었습니다. 같은 매장, 같은 시각이었지만 구매자에 따라 달랐습니다. 이 다섯 가지 숫자 중 어떤 것을 가격 패널에 반영해야 할까요?
과제
식료품 분야는 가격 인텔리전스가 단순히 "상품 페이지를 가져와 가격을 파싱하는" 방식을 벗어나는 영역입니다. 식료품 가격은 특정 매장에 귀속되고, 배송 구역에 따라 달라지며, 점점 더 조회하는 사용자가 누구인지에 따라 결정됩니다.
먼저 매장부터 살펴보겠습니다. Scrapfly의 식료품 가격 비교 가이드에 따르면, 1갤런의 우유 가격이 한 Walmart에서는 $3.98, 20마일 떨어진 다른 매장에서는 $4.29로 책정되어 있으며, 매장 위치 정보가 없는 세션에는 실제 매장과 일치하지 않는 기본 데이터가 제공될 수 있다고 경고합니다. ScrapeInsight의 2026 식료품 배송 가이드에서도 한 단계 더 세분화된 동일한 현상을 지적합니다. 한 배송 구역에서는 $3.99이지만 몇 마일 떨어진 곳에서는 $4.49입니다. Bright Data의 ShopGrok 사례 연구에서는 지난 4년 동안 호주 소매 유통 가격이 우편번호에 따라 달라지는 구조로 변모했다고 설명합니다.
다음은 구매자 요인입니다. 2025년 12월, Groundwork Collaborative, Consumer Reports, More Perfect Union은 4개 도시에서 437명의 구매자를 대상으로 실시간 테스트를 진행하여, 동일한 매장에서 같은 시각에 똑같은 Instacart 장바구니를 채우도록 했습니다. 그 결과 품목의 74%가 둘 이상의 가격으로 표시되었습니다. 가격 차이가 발생한 경우 최저가와 최고가의 격차는 평균 13%에 달했고, 전체 장바구니 기준으로는 약 7%의 차이를 보였습니다.
이러한 요인들이 결합되면 식료품 데이터 수집 비용을 증가시키는 오류가 발생합니다. 매장 컨텍스트를 놓치더라도 시스템이 중단되지는 않습니다. 사이트에서 오류를 반환하지도 않습니다. 단지 요청하지 않은 매장에 대한 정상적인 가격을 반환할 뿐이며, 이는 시스템의 모든 null 검사를 통과하는 SKU, 숫자, 타임스탬프를 온전히 갖추고 있습니다.
FourA는 이전에 차단이 데이터 포인트처럼 보이는 현상에 대해 다룬 적이 있습니다. 이번 사례는 페이지 자체가 실제 가격 페이지가 맞기 때문에 감지하기가 더 어렵습니다. 단지 의도한 매장의 데이터가 아닐 뿐입니다.
응답 데이터 내에서 매장 검증하기
대부분의 수집기는 매장 선택 정보(cookie, 쿼리 매개변수, header)를 전송한 후 이를 신뢰합니다. 더 안정적인 방법은 응답 자체를 통해 이를 검증하는 것입니다. 임베디드 데이터의 매장 ID나 픽업 배너의 매장명처럼 페이지에 렌더링 대상 매장이 명시되어 있다면, 해당 식별자를 성공 여부의 기준으로 삼을 수 있습니다. FourA에서는 단 한 번의 Proxy Finder 호출로 이를 처리할 수 있습니다.
import requests
r = requests.post(
"https://api.foura.ai/api/proxy",
headers={"X-API-Key": "pk_live_..."},
json={
"maxTries": 6,
"request": {
"method": "GET",
"url": "https://grocer.example/product/0001234",
"headers": [["Cookie", "store=1234"]],
"validate": {
"status": {"accept": [200]},
"data": {
"accept": ["\"storeId\":\"1234\""],
"fail": ["Just a moment", "Access Denied"]
}
}
}
}
).json()
if "error" in r:
report = r.get("attemptReport", {})
print(report.get("summary", r["error"]))
if report.get("contentRejected", 0) > report.get("defense", 0) + report.get("noResponse", 0):
print("the site answered for another store: check the store selection")
else:
page, exit_id = r["data"], r["proxy"]
해당 request에는 잘못 설정하기 쉬운 세 가지 세부사항이 있습니다.
accept는 문자열 중 하나라도 페이지에 나타나면 통과 처리합니다. 매장 마커 옆에 가격 마커를 함께 두면, 잘못된 매장 페이지에도 가격이 존재하므로 그대로 통과해 버립니다. 매장 마커는 accept에 단독으로 두고, 챌린지 문자열은 fail에 넣으십시오.
직접 전달하는 Cookie header는 재시도 동작 방식을 바꿉니다. 제공된 브라우저를 사이트가 거부하면 Proxy Finder는 통상 다음 시도에서 다른 브라우저 패밀리로 전환합니다. 그러나 직접 설정한 cookie가 첨부되면 달라집니다. 세션은 이를 획득한 시그니처에 바인딩되므로, 자체 cookie를 전달하는 request는 시작할 때의 브라우저를 그대로 유지합니다. 특정 체인 사이트가 브라우저에 민감하다면 로테이션에 의존하기보다 browser profiles를 통해 명시적으로 하나를 선택하십시오.
또한 실패한 작업은 어떤 오류가 발생했는지 알려줍니다. 실패한 모든 Proxy Finder response는 attemptReport를 포함합니다. defense는 봇 감지가 식별된 응답 수를 집계하고, noResponse는 사이트에 도달하지 못한 exit 수를 집계하며, contentRejected는 봇 감지 없이 HTTP 200으로 반환되었으나 콘텐츠 규칙에 의해서만 제외된 페이지 수를 집계합니다. 식료품 수집 작업에서 이 마지막 수치는 명확한 의미를 갖습니다. 사이트가 응답했으나 타겟 매장의 데이터가 아니라는 뜻입니다. exit를 늘려도 이는 해결되지 않습니다. 기타 집계 항목은 attempt report guide에서 다룹니다.
Hold the Shopper Still, or Measure the Spread
Instacart 분석 결과는 두 번째 변수를 유발하며, 이를 올바르게 처리하는 방법은 두 가지입니다.
가격 패널의 경우 매장별 수집을 단일 identity로 유지하십시오. 성공했던 exit(불투명 ID인 r["proxy"])를 동일한 cookie와 함께 Single을 통해 재생하고, 해당 ID와 X-FourA-Request-Id response header를 모든 행에 함께 저장하십시오. 가격이 급변할 때 실제 매장의 가격 변동인지 세션 변경에 따른 것인지 구분할 수 있습니다.
가격 조사 목적이라면 의도적으로 반대 방식을 적용하십시오. 동일 매장의 동일 SKU를 여러 독립 세션에 걸쳐 샘플링하고, 첫 번째 응답 대신 전체 분포를 저장하십시오. 쇼핑객에게 다섯 가지 가격이 표시되는 환경에서 단 하나의 수치만 보고하는 패널은 정확한 것이 아니라 운이 좋았을 뿐입니다.
Results
2,500개 SKU에 대해 120개 경쟁 매장을 매일 벤치마킹하는 지역 체인의 사례를 가정해 보겠습니다(업계 벤치마크 기반 예시 시나리오). 이는 매일 300,000건의 페이지 조회를 의미하며, 특정 시점에 일부는 잘못된 매장으로 반환됩니다. cookie 포맷 변경, 매장 폐점, 혹은 사이트가 cookie 대신 IP 위치를 우선시하기 시작하는 경우가 이에 해당합니다.
- 잘못된 매장 페이지는 수집 단계에서 실패합니다. 데이터 행으로 저장되지 않으므로 패널의 가격 변동은 해당 매장의 실제 변동을 의미합니다.
- 실패 원인이 분류되어 전달됩니다. 높은
contentRejected수치는 매장 선택 담당자에게, 높은defense수치는 접근 문제로, 높은noResponse수치는 exit 문제로 전달됩니다. 세 명의 담당자가 각자 해결책을 맡으므로 원인을 추측하느라 오전 시간을 낭비하지 않습니다. - 모든 행을 추적할 수 있습니다. 관측값마다 exit ID와 request ID가 연결되어 있어 "이 가격 급등이 실제인가?"라는 질문을 단순 조회 작업으로 바꿉니다.
- 가격 편차를 수치로 확보할 수 있습니다. 여러 세션에 걸쳐 샘플링하는 SKU의 경우, 단일 측정값 대신 실제 쇼핑객이 마주하는 가격 범위를 보고할 수 있습니다.
한계점: 회원 전용 가격과 멤버십 가격은 계정 로그인이 필요하며 로그인된 쇼핑객 기준 실험은 익명 세션에 전혀 표시되지 않습니다. 이를 수집하는 것은 엔지니어링 문제이기 전에 서비스 이용약관의 문제입니다. 또한 매장 식별자의 정확도는 이를 어떻게 정의했는지에 달려 있습니다. 메모리가 아닌 페이지 소스에서 가져오고, 체인 웹사이트가 개편될 때마다 다시 확인해야 합니다.
핵심 요점
예전의 식료품 가격은 제품 자체의 단일 정보였습니다. 이제는 제품, 매장, 쇼핑객이 결합된 정보이며 첫 번째 항목만 기록하는 수집기는 스키마만 깔끔한 노이즈를 만들어낼 뿐입니다.
쇼핑객별 맞춤 가격이 계속 확산된다면 식료품 데이터를 구매하는 고객의 질문은 "가격이 얼마인가?"에서 "이곳의 가격은 얼마이며 편차 범위는 어느 정도인가?"로 바뀝니다. 따라서 신뢰할 수 있는 수집기는 exit 수가 가장 많은 곳이 아니라, 모든 request가 대상 매장을 명시하고 이를 증명할 수 있는 수집기일 것입니다.