스크래핑

SERP API로 일일 키워드 순위 모니터링 자동화하기

수집 작업을 예약하는 것은 쉬운 부분이다. 여기 일일 SERP 호출을 신뢰하고, 경고를 설정하고, 클라이언트에게 설명할 수 있는 순위 시리즈로 바꾸는 파이프라인이 있다.

Chris Collins

Chris Collins

2026년 9월 1일 · 6 분 소요

규모가 큰 순위 추적 자동화는 API 호출을 감싼 크론 작업처럼 보이고, 첫 주에는 실제로 그렇다. 문제는 나중에 나타난다. 한 번은 다른 시간에 실행되어 비교할 수 없는 두 번의 실행, 차트가 이상해 보이기 전까지 아무도 알아채지 못하는 시리즈에서 빠진 하루, 노이즈에 반응해 발생하는 알림, 그리고 왜 당신의 숫자가 자신들의 것과 다른지 묻는 클라이언트.

이 중 어느 것도 데이터 수집 자체의 문제는 아니다. 이는 파이프라인 문제이며, 데이터에 구멍이 난 6개월치 데이터를 갖기 전에 설계해 둘 가치가 있다. 이제 구축 방법을 살펴본다.

측정 규칙을 먼저 정하라

코드를 작성하기 전에, 두 측정값을 비교 가능하게 만드는 변수들을 고정해야 한다. 순위는 이 변수들 없이는 의미가 없기 때문이다: 위치, 기기, 언어, 그리고 검색이 개인화되었는지 여부. 이러한 선택은 실행마다 바뀌는 옵션이 아니라 작업의 상수가 되어야 한다.

이 변수들에 조금이라도 변동이 생기면 데이터에는 실제로는 일어나지 않은 순위 변동으로 나타나기 때문이다. 월요일에는 한 도시에서, 화요일에는 다른 도시에서 측정한 키워드는 마치 움직인 것처럼 보인다. 전체 논리는 정확한 키워드 순위 측정에 설명되어 있으며, 요약하면 절대적인 정밀도보다 일관성이 더 중요하다는 것이다.

이 규칙을 스키마로 기록해 두어야 하는데, 클라이언트가 당신의 숫자가 무엇을 의미하는지 물을 때 건네줄 자료이기도 하기 때문이다:

# one row per keyword per market: this is the unit of measurement
TARGET = {
    "keyword":  "residential proxies",
    "country":  "de",
    "city":     "berlin",        # optional, only where local intent matters
    "language": "de",
    "device":   "desktop",
}

SERP API를 통해 데이터를 가져오라

수집 계층을 프록시 위에 직접 구축할 수도 있고, 파싱된 결과를 반환하는 API를 호출할 수도 있다. 매일 실행되는 모니터링 작업에는 API 방식이 실제로 시간을 많이 잡아먹는 두 가지 유지보수 부담을 없애준다: 결과 페이지 레이아웃 변경을 계속 따라가는 것, 그리고 수집 인프라를 운영하는 것.

SERP API는 위의 파라미터를 받아 구조화된 결과를 돌려주므로, 작업은 요청과 저장이 되며 데이터 가져오기와 파서 작업이 아니게 된다:

import requests, datetime as dt

def fetch_serp(t):
    r = requests.get("https://serp.shifter.io/v1", timeout=45, params={
        "api_key":  API_KEY,
        "q":        t["keyword"],
        "gl":       t["country"],      # market
        "hl":       t["language"],     # interface language
        "location": t.get("city"),
        "device":   t["device"],
    })
    r.raise_for_status()
    return r.json()

이러한 파라미터는 계속 바뀌므로 블로그 글을 신뢰하기보다 API 문서에서 현재 파라미터 이름을 확인해야 한다. 레지덴셜 IP를 통해 직접 수집을 실행하는 대안은 정확한 순위 추적에 레지덴셜 프록시가 필요한 이유에서 다루며, 여기서 트레이드오프는 통제력과 유지보수 부담 사이의 균형이다.

순위뿐 아니라 결과 전체를 저장하라

가장 흔한 설계 실수이자 되돌리기 비용이 큰 실수는 키워드별, 일별로 단 하나의 숫자만 저장하는 것이다.

전체 순위 URL 목록, 결과 기능의 존재 여부와 위치, 그리고 원본 페이로드를 저장하라. 이유는 세 가지다. 경쟁사의 움직임은 당신 자신의 움직임을 해석 가능하게 만드는 맥락이다. AI 개요가 상단에 나타나는 것과 같은 기능 변화는 숫자 자체는 바꾸지 않으면서도 그 위치의 가치를 바꾼다. 그리고 지난 화요일의 SERP를 나중에 다시 수집할 수는 없으므로, 저장하지 않은 것은 영원히 사라진다.

CREATE TABLE serp_snapshot (
  id          BIGSERIAL PRIMARY KEY,
  keyword     TEXT NOT NULL,
  country     TEXT NOT NULL,
  city        TEXT,
  device      TEXT NOT NULL,
  captured_at TIMESTAMPTZ NOT NULL,
  results     JSONB NOT NULL,        -- full ranked list
  features    JSONB NOT NULL,        -- ai overview, local pack, shopping
  raw         JSONB                  -- keep it, storage is cheaper than regret
);
CREATE INDEX ON serp_snapshot (keyword, country, device, captured_at DESC);

이렇게 하면 순위는 저장된 원본 레코드가 아니라 스냅샷에서 파생되므로, 파싱 수정 사항을 미래 실행뿐 아니라 과거 기록에도 다시 적용할 수 있다.

비교 가능성을 위한 스케줄링

매일 같은 시간에, 안정적으로 유지하는 시간대 안에서 실행하라. 결과는 하루 중에도 변동하며, 시간대가 일정하지 않은 시리즈에는 원인을 특정할 수 없는 변동성이 생긴다.

모든 키워드를 한꺼번에 실행하지 말고 그 시간대 안에 분산시켜라. 한꺼번에 몰아서 요청하면 소스에도 부담을 주고 제한에 걸릴 가능성도 커지는 반면, 마감 시한이 몇 시간 뒤라면 분산하는 데 비용이 들지 않는다. 요청 사이에 지터를 추가하고, 동시성을 제한하며, 4분이 아니라 한 시간 정도 걸리도록 하라.

import random, time

def run_daily(targets, window_seconds=3600):
    gap = window_seconds / max(len(targets), 1)
    for t in targets:
        snapshot = fetch_with_retries(t)
        store(t, snapshot)
        time.sleep(gap * random.uniform(0.7, 1.3))   # spread, with jitter

”변화 없음”과 “데이터 없음”을 구분하라

신뢰할 수 있는 시리즈와 오해를 부르는 시리즈를 가르는 세부 사항이 바로 이것이다. 데이터 가져오기가 실패했는데 아무것도 기록하지 않으면, 나중에 이 데이터를 보는 사람은 아무 변화가 없었던 날과 구분할 수 없는 공백을 보게 된다. 그러면 “어제”와의 비교가 알아채지 못한 채 이틀치를 건너뛰게 되고 실제로 일어나지 않은 변동을 보고하게 된다.

모든 시도를 명시적인 결과와 함께 기록하고, 후속 분석이 공백을 넘어 비교하지 못하도록 만들어라:

def fetch_with_retries(t, attempts=3):
    for i in range(attempts):
        try:
            data = fetch_serp(t)
            if is_valid(data):                     # sanity-check the payload
                return {"status": "ok", "data": data}
            record_status(t, "invalid")            # parsed, but not a real SERP
        except requests.RequestException:
            record_status(t, "error")
        time.sleep(2 ** i * random.uniform(0.5, 1.5))
    return {"status": "failed", "data": None}      # written as a failure, not skipped

예외를 잡는 것만큼이나 페이로드를 검증하는 것도 중요하다. 응답이 성공적으로 도착하더라도 사용 가능한 결과 페이지가 아닐 수 있기 때문이며, 이는 차단되거나 가짜인 콘텐츠 탐지에서 다루는 일반적인 문제다.

의미 있는 변동에만 알림을 보내라

위치 변화가 있을 때마다 무조건 알림을 보내는 단순한 방식은 끊임없이 울리게 되고, 아무도 믿지 않는 알림 시스템은 알림이 없는 것보다 더 나쁘다.

세 가지 규칙이 알림을 유용하게 만든다. 위치에 비례하는 임계값을 요구하라. 3위에서 6위로의 이동은 47위에서 50위로의 이동보다 훨씬 중요하기 때문이다. 지속성을 요구하라. 즉 연속된 두세 번의 실행에서 나타나야 한다. 하루짜리 일시적인 변동은 정상이기 때문이다. 그리고 1페이지 진입과 이탈에는 별도로 알림을 보내라. 이 경계는 실제 트래픽에 영향을 미치기 때문이다.

그리고 사람들이 흔히 잊어버리는 알림 하나를 추가하라: 수집 자체의 상태. 어떤 시장에서 3일 전부터 데이터가 조용히 들어오지 않고 있다면, 이는 어떤 순위 변동보다도 더 시급한 문제이며, 스크래핑 파이프라인 모니터링에 따라 실행 결과를 1급 데이터로 추적하고 있어야만 눈에 보인다.

데이터가 스스로 설명하게 만들어라

두 가지를 추가하면 순위 표가 클라이언트에게 보여줄 수 있는 자료로 바뀐다.

자신의 타임라인에 주석을 달아라: 배포, 콘텐츠 게시, 마이그레이션. “왜 떨어졌는가”에 대한 조사의 절반은 그 주에 나간 릴리스로 결론이 나며, 주석이 달린 차트는 이를 즉시 찾아낸다.

키워드별 위치만이 아니라 포트폴리오 수준의 변동도 계산하라. 서로 관련 없는 많은 키워드에 걸친 광범위한 변동은 한 페이지가 밀려난 것과는 다른 사건이기 때문이다. 이것이 SERP 변동성과 알고리즘 업데이트 탐지에서 다루는 변동성 분석이며, 자신의 순위만이 아니라 전체 결과 집합을 저장했기 때문에만 가능한 일이다.

책임감 있게 수집하라

공개된 검색 결과에 한정하고, 각 엔진의 서비스 약관을 준수하며, 속도 제한을 장애물로 여기지 말고 정중하게 속도를 조절하라. 매일 실행되는 모니터링 작업이 공격적으로 굴 이유는 없다. 마감 시한은 다음날 아침이므로 작업을 분산하는 데는 비용이 들지 않는다.

결론

크론 작업은 쉬운 부분이다. 위치, 기기, 언어, 개인화가 우연한 변수가 아니라 상수가 되도록 측정 규칙을 먼저 정하라. 순위뿐 아니라 전체 결과 집합과 원본 페이로드를 저장하라. 경쟁사의 맥락과 결과 기능이야말로 숫자를 해석 가능하게 만드는 요소이며, 나중에 다시 돌아가서 수집할 수 없기 때문이다. 일정한 시간에 실행하고, 시간대 안에서 지터를 두어 분산시켜라. 실패를 명시적으로 기록해서 공백이 안정성으로 잘못 해석되는 일이 없도록 하라. 지속적이고 위치에 비례하는 변동과 수집 상태에 대해 알림을 보내라. 그리고 자신의 변경 사항에 주석을 달아, 차트가 사람들이 실제로 묻는 질문에 답하도록 하라.

수집 계층은 파싱된 결과를 유지보수 없이 원한다면 SERP API가 될 수 있고, 직접 운영하고 싶다면 국가 및 도시 타겟팅이 가능한 레지덴셜 프록시가 될 수 있으며, 소규모의 빈번한 확인 작업에 적합한 GB당 가격이 제공된다.

시작할 준비가 되셨나요?

205M개 이상의 IP, 195개 이상의 국가를 지원하는 Shifter의 레지덴셜 프록시를 $0.75/GB부터 이용해보세요.

시작하기