본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 10. 15:35

Beaconmon이 Playwright 없이 대규모로 Shopify 경쟁사 스토어프론트를 모니터링하는 방법

요약

Playwright나 Headless Chrome 없이 cheerio와 undici를 활용하여 대규모 Shopify 스토어프론트를 효율적으로 모니터링하는 아키텍처를 소개합니다. BullMQ와 Postgres를 사용하여 변경 사항을 감지하고, 데이터 정규화를 통해 불필요한 알림을 방지하는 실질적인 방법론을 다룹니다.

핵심 포인트

  • Playwright 대신 cheerio를 사용하여 리소스 소모 최소화
  • BullMQ 기반의 스케줄링 및 워커 아키텍처 활용
  • 데이터 저장 전 정규화 과정을 통한 알림 정확도 향상
  • Shopify 공개 JSON 엔드포인트를 활용한 초기 카탈로그 매핑

Playwright 없이, 스크래핑 팜(scraping farms) 없이, 그리고 단 하나의 VPS도 태워 먹지 않고 대규모로 콘텐츠를 모니터링하는 실질적인 방법론.

이 포스트를 시작하게 만든 새벽 2시의 알림

Beaconmon을 사용하는 한 스킨케어 브랜드가 알림을 받고 잠에서 깨어났습니다. 주요 경쟁사가 베스트셀러 세럼들의 가격을 조용히 18% 인하하고 사이트 전체 무료 배송 프로모션을 진행하고 있었습니다. 보도 자료도 없었고, 공지사항도 없었습니다. 그저 평범한 화요일이었을 뿐입니다.

해당 팀은 경쟁사의 유료 광고가 트래픽을 유도하기 전에 가격을 재조정하고 배송 기준을 맞추었습니다. 그들이 이를 확인할 수 있었던 이유는 백그라운드 워커(background worker)가 해당 경쟁사의 스토어프론트(storefront)를 가져와서, cheerio를 사용하여 적절한 DOM 노드를 추출하고, 결과를 24시간 전의 스냅샷과 비교(diff)한 뒤, 해당 변경 사항을 높은 중요도로 점수화(score)했기 때문입니다.

이것이 제품의 전부입니다. 아래 내용은 우리가 이를 어떻게 대규모 환경에서 안정적으로 만드는지에 대한 설명입니다.

요약 (TL;DR)

  • BullMQ 워커가 정해진 일정에 따라 경쟁사의 HTML을 가져옵니다.
  • cheerio가 순위가 지정된 셀렉터 캐스케이드(selector cascade)를 사용하여 가격, 프로모션 및 제품 그리드(product-grid) 콘텐츠를 추출합니다.
  • 차이점(Diffs)은 먼저 규칙(rules)에 의해, 그다음 AI에 의해 정규화(normalize)되고 저장되며 점수화됩니다.
  • Playwright 없음, headless Chrome 없음, 스크래핑 팜(scraping farm) 없음.
  • 가장 중요한 교훈: 저장하기 전에 정규화하라. 그렇지 않으면 평생 공백(whitespace) 때문에 알림을 받게 될 것이다.

한 단락으로 보는 아키텍처

추적되는 모든 경쟁사는 is_competitor: true로 표시된 Monitor 레코드입니다. BullMQ 스케줄러는 설정된 간격(무료 플랜은 15분, Growth 플랜은 5분까지)에 따라 각 모니터에 대해 content-check 작업을 큐에 넣습니다. 워커는 작업을 가져와서 undici로 페이지를 가져오고, cheerio로 파싱하며, 텍스트를 정규화한 뒤 Postgres에 있는 마지막 스냅샷과 비교합니다. 변경 사항이 있으면 두 번째 작업이 중요도를 점수화하고 알림을 확산(fan out)시킵니다.

Playwright를 사용하지 않습니다. headless Chrome도 사용하지 않습니다. 의도적으로 HTML만 사용합니다. 단일 VPS는 3,000개의 동시 실행되는 Chromium 프로세스를 견뎌낼 수 없으며, 우리가 관심을 갖는 신호(가격 텍스트, 프로모션 문구, 제품 그리드 콘텐츠)는 거의 항상 초기 HTML 응답에 존재합니다.

1단계: 카탈로그를 한 번 매핑한 후에는 그대로 두기

Shopify는 모든 스토어의 제품 카탈로그에 대해 공개 JSON 엔드포인트 (public JSON endpoint)를 제공합니다. 우리는 설정 시점에 이를 한 번 가져와 내부 제품 맵 (internal product map)을 구축합니다.

type ProductMap = Record<string, {
  title: string;
  variants: Variant[];
...

이 맵 덕분에 우리는 "3,000개의 URL" 대신 "3,000개의 제품"이라고 말할 수 있습니다. 우리는 매 체크 주기마다 해당 엔드포인트를 폴링 (poll) 하지 않습니다. 그것은 시드 (seed)이지, 하트비트 (heartbeat)가 아닙니다.

하트비트는 라이브 HTML을 대상으로 하는 cheerio입니다.

2단계: 셀렉터 캐스케이드 (The Selector Cascade)

Shopify 테마는 표준화되어 있지 않지만, 일정한 패턴이 있습니다. Debut, Dawn, 그리고 대부분의 서드파티 (third-party) 테마는 가격 관련 클래스 이름 (class names)의 작은 어휘를 공유합니다. 우리는 순위가 매겨진 셀렉터 (selector) 목록을 시도하고 첫 번째 일치 항목을 가져옵니다.

const PRICE_SELECTORS = [
  '.price__sale',
  '.price-item--sale',
...

만약 일치하는 것이 없다면, main으로 폴백 (fallback) 합니다. 폴백 일치는 기록되지만 "감지됨 (detected)"으로 표시되지는 않으므로, 특정 셀렉터의 미스는 오탐 (false-positive) 이벤트를 생성하지 않습니다.

이 차이가 중요합니다. 우리는 _"우리가 원하는 것을 찾았다"_와 _"무언가를 추적했고 그것이 변경되었다"_를 분리합니다. 이 두 상태를 하나로 합치면 알림 (alert) 품질이 떨어집니다.

동일한 패턴이 공지 바 (announcement bars), 컬렉션 그리드 (collection grids), 세일 페이지 (sale pages)에도 적용됩니다. 각 프리셋 (preset)은 순위가 매겨진 셀렉터 목록과 폴백으로 구성됩니다.

{
  id: 'announcement-bar',
  selectors: [
...

3단계: 실제 콘텐츠 체크 (The Actual Content Check)

가져오기 (fetch) 및 파싱 (parse) 과정은 약 30줄 정도입니다. 중요한 부분은 정규화 (normalization)와 대량 처리 시 커넥션 풀링 (connection pooling)을 위해 node-fetch 대신 undici를 사용하는 것입니다.

import { request } from 'undici';
import * as cheerio from 'cheerio';

...

정규화 단계는 보기보다 중요합니다. Shopify 테마는 CDN 에지 (edge) 및 A/B 테스트 변형 (variants)에 따라 공백 (whitespace)을 일관성 없게 렌더링합니다. 줄당 수평 공백을 축소하고 빈 칸을 제거하지 않으면, 트래픽이 높은 스토어의 경우 몇 시간마다 오탐 디프 (false-positive diffs)가 발생합니다. 정규화를 적용하면 실제로 무언가 변경될 때까지 디프는 조용하게 유지됩니다.

4단계: 변경 사항 점수 매기기 (Score What Changed)

모든 디프 (diff)가 동일한 가치를 지니지는 않습니다. 경쟁사가 '회사 소개 (About)' 문구를 업데이트하는 것은 노이즈에 불과합니다. 하지만 경쟁사가 주력 SKU (핵심 상품)의 가격을 18% 인하하는 것은 구매 신호 (buying signal)입니다.

우리는 두 가지 계층으로 점수를 매깁니다.

1순위: 규칙 기반 (Rules First) (빠르고, 무료이며, 지연 시간이 없음)

case 'price_changed': {
  const delta = Math.abs(next - old) / old;
  if (delta >= 0.05) return 'high';
...

2순위: AI 기반 (AI Second) (Growth 및 Scale 플랜 전용, 실패 시 기본값 유지)

디프 (diff) 텍스트는 채점 기준을 설명하는 짧은 시스템 프롬프트와 함께 Claude로 전송됩니다. Claude는 4초의 하드 타임아웃 (hard timeout) 내에 low, medium, 또는 high를 반환합니다. 만약 모델에 접속할 수 없거나 플랜에 AI가 포함되어 있지 않은 경우, 규칙 기반 (rule-based) 점수가 사용됩니다. 시스템은 AI 계층 때문에 프로세스가 차단되는 일이 절대 없습니다.

데이터가 실제로 가능하게 하는 것들

이 부분이 바로 이 서비스를 사용하는 스토어들에게 중요한 지점입니다.

두려움이 아닌 맥락을 바탕으로 한 가격 재설정 (Repricing with context, not fear). 새벽 2시에 경쟁사의 가격 인하가 high로 태깅된 것을 확인했을 때, 당신에게는 디프 (diff), 타임스탬프 (timestamp), 그리고 이전 가격 정보가 있습니다. 당신은 추측하는 것이 아니라, 결정을 내리는 것입니다.

프로모션 캘린더 재구성 (Promo calendar reconstruction). 6주간의 공지 바 (announcement-bar) 스냅샷을 통해 경쟁사가 언제 세일을 진행하는지, 얼마나 오래 지속하는지, 그리고 어떤 문구 (copy)를 사용하는지 알 수 있습니다. 이는 직접 구축할 필요가 없는 콘텐츠 캘린더가 됩니다.

신상품 출시 속도 (New-arrival velocity). 컬렉션 그리드 (collection-grid) 모니터는 경쟁사가 얼마나 빠르게 제품을 출시하는지 알려줍니다. 주 2회 출시한다면 실제 구매력이 있거나 주목할 만한 생산 운영 능력을 갖추고 있다는 뜻입니다. 월 1회 출시한다면 재고를 정리하는 수준임을 의미합니다.

소싱을 위한 재고 신호 (Stock signal for sourcing). 경쟁사의 베스트셀러 품목이 품절 (out-of-stock)되는 이벤트는 공급망 (supply chain)의 공백을 나타낼 수 있습니다. 이는 자신의 재고를 늘리거나, 그들의 미충족 수요를 겨냥한 타겟 광고를 실행할 수 있는 기회의 창입니다.

이 중 그 어떤 것도 대규모 크롤링 (crawling at scale)을 수행하거나 서비스 약관 (terms of service)을 위반할 필요가 없습니다. 브라우저가 렌더링할 것과 동일한 HTML을, 선언된 유저 에이전트 (user-agent)를 사용하여 적절한 간격으로 정중하게 가져오는 것뿐입니다.

과거의 나에게 해주고 싶은 세 가지 교훈

1. 어려운 부분은 큐 (queue)가 아니라 셀렉터 캐스케이드 (selector cascade)이다

Shopify의 테마 생태계는 단 하나의 정답인 셀렉터 (selector)가 존재하지 않을 만큼 방대합니다. 이름이 지정된 폴백 (fallback)을 포함한 순위 목록과, "매칭됨 (matched)"과 "폴백됨 (fell back)" 사이의 명확한 구분이 올바른 모델입니다. 이를 잘못 설계하면 알림 (alert)의 품질이 무너집니다.

2. 저장하기 전에 항상 정규화 (Normalize) 하세요.

가공되지 않은 HTML (raw HTML)을 저장한 뒤 나중에 차이점 (diffing)을 비교하는 방식은 매력적으로 들릴 수 있습니다. 하지만 CDN이 주입한 속성 (attributes), 인라인 스크립트 (inline scripts) 내의 논스 값 (nonce values), 그리고 공백의 변화 (whitespace drift) 등으로 인해 모니터링 대상의 상당수에서 매번 알림이 발생하게 된다는 사실을 깨닫기 전까지는 말입니다. 정규화된 텍스트 (normalized text)를 저장하세요.

3. 어디에서든 폴백 (Fail open) 하도록 설계하세요.

AI 레이어는 폴백합니다. 셀렉터 캐스케이드 (selector cascade)도 폴백합니다. 체크 작업 자체도 오류 유형을 기록하고 무한히 재시도하는 대신 다음 단계로 넘어갑니다. 자체 의존성 (dependencies)에 문제가 생겼을 때 노이즈를 생성하는 모니터링 제품은 무용지물보다 못합니다.

마무리

유사한 것을 구축 중이거나, Shopify 또는 WooCommerce 스토어를 운영하며 이를 실제로 확인하고 싶다면, Beaconmon의 얼리 액세스 (early access)를 신청하세요. 아키텍처 (architecture), 셀렉터 전략 (selector strategies), 또는 AI 레이어가 제 역할을 하는 지점과 규칙 (rules)만으로 충분한 지점에 대해 기꺼이 이야기 나누겠습니다.

@haimanot_getu에서 저를 찾아주세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0