키워드 리서치 도구에 매월 수십만 원을 지불하고 있습니까? 구글은 검색 결과 페이지 자체에 무료 키워드 데이터를 풍부하게 노출합니다 — 자동완성 추천, People Also Ask(PAA) 질문, 그리고 관련 검색어가 그것입니다. 본 가이드에서는 구글 People Also Ask 및 자동완성 스크래핑으로 키워드 리서치하기 (Python 가이드)를 주제로, 하나의 시드 키워드를 수백 개의 롱테일 키워드로 확장하는 방법을 코드 중심으로 설명합니다.
구글 PAA 및 자동완성 스크래핑으로 키워드 리서치하기: 세 가지 데이터 소스
구글은 검색자의 의도를 파악하기 위한 세 가지 공개 신호를 제공합니다. 각 소스는 서로 다른 검색 의도 단계에 매핑됩니다:
| 데이터 소스 | 엔드포인트 / 위치 | 검색 의도 | 데이터 형식 |
|---|---|---|---|
| 자동완성 (Autocomplete) | suggestqueries.google.com/complete/search?client=chrome&q= |
탐색적 / 정보 탐색 | JSON (client=chrome) 또는 XML |
| People Also Ask | SERP 내 아코디언 블록 | 질문형 정보 탐색 | HTML (렌더링 필요) |
| 관련 검색어 | SERP 하단 | 비교 / 트랜잭셔널 | HTML 텍스트 |
자동완성은 사용자가 검색창에 입력하는 순간 실시간으로 추천을 반환하므로, 롱테일 키워드 발굴에 가장 빠르고 효율적입니다. PAA는 질문 형태의 키워드를 제공하므로 FAQ 콘텐츠 기획에 직접 활용할 수 있습니다. 관련 검색어는 SERP 하단에 고정되어 있어 한 번의 요청으로 8~10개의 확장 키워드를 얻을 수 있습니다.
왜 이 데이터가 무료임에도 수집이 어려운가
구글 자동완성 엔드포인트는 공개되어 있지만, 빠른 속도로 반복 호출하면 두 가지 문제가 발생합니다:
- IP 기반 속도 제한: 동일 IP에서 초당 약
10 requests/sec를 넘기면 429 Too Many Requests 또는 캡차 페이지가 반환됩니다. - 로케일 편향: 자동완성 추천은 요청 IP의 지리적 위치에 따라 달라집니다. 한국 IP로 독일 로컬 키워드를 수집하면 결과가 왜곡됩니다.
이 문제를 해결하기 위해 국가/도시 단위 지역 타겟팅이 가능한 주거 프록시(residential proxy)가 필요합니다. 주거 IP는 실제 통신사가 할당한 IP 대역을 사용하므로, 구글은 해당 요청을 일반 사용자의 검색으로 인식합니다.
ProxyHat 주거 프록시 설정
ProxyHat 주거 프록시는 게이트웨이 호스트 gate.proxyhat.com을 통해 HTTP 및 SOCKS5 프로토콜을 지원합니다. 사용자 이름에 국가 및 도시 타겟팅 플래그를 포함할 수 있습니다:
# HTTP 주거 프록시 — 독일 베를린 IP
http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080
# HTTP 주거 프록시 — 미국 뉴욕 IP
http://user-country-US-city-newyork:pass@gate.proxyhat.com:8080
# SOCKS5 주거 프록시 — 일본 도쿄 IP
socks5://user-country-JP-city-tokyo:pass@gate.proxyhat.com:1080
세션 고정이 필요한 경우 user-session-abc123 플래그를 추가하여 동일 IP를 유지할 수 있습니다. PAA 확장처럼 여러 페이지 로드가 필요한 작업에 유용합니다. 자세한 프록시 구성은 ProxyHat 공식 문서를 참조하세요.
코드 예제 1: curl로 구글 자동완성 JSON 가져오기
가장 간단한 형태의 자동완성 스크래핑은 curl 한 줄로 가능합니다. client=chrome 파라미터를 사용하면 JSON 형식으로 응답을 받을 수 있습니다:
# 독일 베를린 IP로 "best vpn" 자동완성 수집
curl -x "http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080" \
"https://suggestqueries.google.com/complete/search?client=chrome&q=best+vpn" \
-H "Accept: application/json" \
--max-time 10
응답은 [query, [suggestions], ...] 형식의 JSON 배열입니다. client=firefox를 사용하면 XML 형식으로 받을 수 있지만, JSON이 파싱하기 더 쉽습니다.
코드 예제 2: httpx + ProxyHat으로 자동완성 수집 (Python)
이제 Python httpx를 사용하여 자동완성 추천을 수집하는 함수를 작성합니다. 재시도 로직과 타임아웃을 포함하여 프로덕션 환경에 적합하게 만듭니다:
import httpx
import time
import logging
from typing import list
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
PROXY_URL = "http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080"
AUTOCOMPLETE_URL = "https://suggestqueries.google.com/complete/search"
def fetch_autocomplete(
query: str,
proxy: str = PROXY_URL,
gl: str = "de",
hl: str = "de",
max_retries: int = 3,
) -> list[str]:
"""구글 자동완성 추천 키워드 리스트를 반환합니다."""
params = {
"client": "chrome",
"q": query,
"gl": gl, # 지역 (예: de, us, kr)
"hl": hl, # 언어 (예: de, en, ko)
}
proxies = {"http://": proxy, "https://": proxy}
for attempt in range(max_retries):
try:
with httpx.Client(proxy=proxy, timeout=15.0) as client:
resp = client.get(AUTOCOMPLETE_URL, params=params)
resp.raise_for_status()
data = resp.json()
# data[1]이 추천 키워드 배열
suggestions = data[1] if len(data) > 1 else []
logger.info(f"[{query}] {len(suggestions)}개 추천 수집")
return suggestions
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
wait = 2 ** attempt
logger.warning(f"429 rate limit — {wait}s 대기")
time.sleep(wait)
else:
logger.error(f"HTTP {e.response.status_code}: {e}")
raise
except httpx.RequestError as e:
logger.error(f"요청 실패 (attempt {attempt+1}): {e}")
time.sleep(2 ** attempt)
logger.error(f"[{query}] 최대 재시도 초과")
return []
# 실행 예시
if __name__ == "__main__":
results = fetch_autocomplete("best vpn for streaming", gl="de", hl="de")
for r in results:
print(r)
gl 및 hl 파라미터와 프록시의 국가/도시 타겟팅을 일치시키면 해당 지역의 실제 사용자가 보는 추천 키워드를 정확히 수집할 수 있습니다. httpx 공식 문서에서 프록시 설정 및 타임아웃 구성에 대한 자세한 내용을 확인할 수 있습니다.
코드 예제 3: 시드 키워드를 롱테일로 확장하는 a-z 접두사 전략
하나의 시드 키워드를 수백 개의 롱테일 키워드로 확장하려면, 알파벳 접두사와 수식어(modifier)를 조합하여 자동완성을 반복 호출합니다. 이 기법은 Google Keyword Planner의 알파벳 확장과 동일한 원리입니다:
import string
import itertools
import time
from typing import list
# 위에서 정의한 fetch_autocomplete 함수 재사용
def expand_seed_keyword(
seed: str,
proxy: str = PROXY_URL,
gl: str = "de",
hl: str = "de",
delay: float = 1.0,
) -> list[str]:
"""시드 키워드를 a-z 접두사 및 수식어로 확장합니다."""
all_keywords = set()
# 1단계: 원본 키워드
prefixes = [""]
# 2단계: a-z 알파벳 접두사
prefixes += [c for c in string.ascii_lowercase]
# 3단계: 일반적인 수식어 접두사
modifiers = [
"best", "how to", "what is", "why", "cheap",
"free", "top", "review", "vs", "alternative",
"for beginners", "2025", "near me",
]
prefixes += modifiers
for prefix in prefixes:
query = f"{prefix} {seed}".strip() if prefix else seed
suggestions = fetch_autocomplete(query, proxy=proxy, gl=gl, hl=hl)
all_keywords.update(suggestions)
time.sleep(delay) # 정중한 속도 제한
logger.info(f"시드 '{seed}' → {len(all_keywords)}개 키워드 확장")
return sorted(all_keywords)
# 실행 예시
if __name__ == "__main__":
keywords = expand_seed_keyword("vpn", gl="de", hl="de", delay=0.8)
print(f"총 {len(keywords)}개 키워드 수집")
for kw in keywords[:20]:
print(f" {kw}")
이 방식으로 "vpn"이라는 시드 키워드에서 약 300~500개의 롱테일 키워드를 얻을 수 있습니다. delay 값을 0.8초 이상으로 유지하면 429 응답을 거의 받지 않습니다.
코드 예제 4: Playwright로 PAA 아코디언 확장 및 질문 추출
PAA 블록은 자바스크립트 렌더링이 필요하므로 httpx만으로는 수집할 수 없습니다. Playwright를 사용하여 PAA 아코디언을 클릭하고, 펼쳐지는 중첩 질문을 재귀적으로 수집합니다:
from playwright.sync_api import sync_playwright
from typing import list
import time
import logging
logger = logging.getLogger(__name__)
def scrape_paa(
query: str,
proxy_server: str = "gate.proxyhat.com:8080",
proxy_user: str = "user-country-US-city-newyork",
proxy_pass: str = "pass",
max_expansions: int = 15,
) -> list[dict]:
"""PAA 질문과 답변 출처를 재귀적으로 수집합니다."""
questions = []
seen = set()
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
proxy={
"server": f"http://{proxy_server}",
"username": proxy_user,
"password": proxy_pass,
},
)
context = browser.new_context(
viewport={"width": 1280, "height": 800},
locale="en-US",
)
page = context.new_page()
page.goto(f"https://www.google.com/search?q={query}", wait_until="networkidle")
time.sleep(2)
for i in range(max_expansions):
# PAA 질문 엘리먼트 찾기
paa_items = page.query_selector_all("div[role='button'] span")
# 실제 선택자는 구글 마크업 변경에 따라 조정 필요
paa_questions = [
el.inner_text()
for el in paa_items
if el.inner_text().endswith("?") and len(el.inner_text()) > 10
]
for q in paa_questions:
if q not in seen:
seen.add(q)
questions.append({"question": q, "source": ""})
logger.info(f"PAA 질문 발견: {q}")
# 첫 번째 PAA 항목 클릭하여 새 질문 펼치기
try:
first_paa = page.query_selector("div[role='button']")
if first_paa:
first_paa.click()
time.sleep(1.5)
except Exception as e:
logger.warning(f"클릭 실패: {e}")
break
browser.close()
logger.info(f"총 {len(questions)}개 PAA 질문 수집")
return questions
# 실행 예시
if __name__ == "__main__":
paa_results = scrape_paa("what is vpn", max_expansions=10)
for item in paa_results:
print(f"Q: {item['question']}")
PAA 아코디언은 클릭할 때마다 새로운 중첩 질문이 하단에 추가됩니다. max_expansions을 15로 설정하면 약 30~50개의 질문형 키워드를 수집할 수 있습니다. 구글의 HTML 구조는 주기적으로 변경되므로 선택자를 정기적으로 업데이트해야 합니다.
코드 예제 5: 비동기 동시 수집으로 처리량 최적화
수백 개의 키워드를 순차적으로 수집하면 시간이 너무 오래 걸립니다. httpx.AsyncClient와 asyncio.Semaphore를 사용하여 동시 요청 수를 제어하면서 처리량을 높입니다:
import asyncio
import httpx
import logging
from typing import list
logger = logging.getLogger(__name__)
PROXY_URL = "http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080"
AUTOCOMPLETE_URL = "https://suggestqueries.google.com/complete/search"
async def fetch_suggestions_async(
client: httpx.AsyncClient,
query: str,
gl: str = "de",
hl: str = "de",
) -> list[str]:
"""비동기 자동완성 수집"""
params = {"client": "chrome", "q": query, "gl": gl, "hl": hl}
try:
resp = await client.get(AUTOCOMPLETE_URL, params=params)
resp.raise_for_status()
data = resp.json()
return data[1] if len(data) > 1 else []
except Exception as e:
logger.error(f"[{query}] 실패: {e}")
return []
async def batch_fetch(
queries: list[str],
proxy: str = PROXY_URL,
concurrency: int = 5,
gl: str = "de",
hl: str = "de",
) -> dict[str, list[str]]:
"""동시성 제한으로 배치 수집"""
sem = asyncio.Semaphore(concurrency)
results = {}
async with httpx.AsyncClient(proxy=proxy, timeout=15.0) as client:
async def limited_fetch(q: str):
async with sem:
suggestions = await fetch_suggestions_async(client, q, gl, hl)
results[q] = suggestions
logger.info(f"[{q}] {len(suggestions)}개 수집")
await asyncio.sleep(0.5) # 정중한 간격
await asyncio.gather(*[limited_fetch(q) for q in queries])
return results
# 실행 예시
if __name__ == "__main__":
seed_queries = ["best vpn", "cheap vpn", "vpn for gaming", "vpn review"]
all_results = asyncio.run(batch_fetch(seed_queries, concurrency=5))
total = sum(len(v) for v in all_results.values())
print(f"총 {total}개 키워드 수집")
concurrency=5로 설정하면 초당 약 5 requests/sec를 유지하면서 429 응답을 회피할 수 있습니다. ProxyHat 주거 프록시는 회전 IP를 제공하므로, 더 높은 동시성(예: 10~15)도 가능하지만, 안정성을 위해 5~8을 권장합니다. ProxyHat 요금제에서 동시 세션 수에 맞는 플랜을 확인하세요.
코드 예제 6: 중복 제거, 의도 클러스터링, CSV 내보내기
수집된 키워드를 바로 사용하면 중복이 많고 의도별로 정리되지 않습니다. 간단한 휴리스틱으로 의도를 분류하고 CSV로 내보냅니다:
import csv
import re
from typing import list
from collections import defaultdict
def classify_intent(keyword: str) -> str:
"""간단한 휴리스틱으로 검색 의도를 분류합니다."""
kw_lower = keyword.lower()
if any(w in kw_lower for w in ["buy", "price", "cheap", "deal", "discount"]):
return "transactional"
elif any(w in kw_lower for w in ["best", "top", "review", "vs", "comparison"]):
return "commercial"
elif any(w in kw_lower for w in ["how", "what", "why", "when", "guide", "tutorial"]):
return "informational"
else:
return "navigational"
def dedupe_and_cluster(keywords: list[str]) -> dict[str, list[str]]:
"""중복 제거 후 의도별로 클러스터링합니다."""
unique = set(kw.strip().lower() for kw in keywords if kw.strip())
clusters = defaultdict(list)
for kw in sorted(unique):
intent = classify_intent(kw)
clusters[intent].append(kw)
return dict(clusters)
def export_to_csv(
keywords: list[str],
output_path: str = "keywords.csv",
) -> None:
"""키워드와 의도 분류를 CSV로 내보냅니다."""
clusters = dedupe_and_cluster(keywords)
with open(output_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["keyword", "intent", "keyword_length"])
for intent, kws in clusters.items():
for kw in kws:
writer.writerow([kw, intent, len(kw.split())])
logger.info(f"{output_path}에 {sum(len(v) for v in clusters.values())}개 키워드 저장")
# 실행 예시
if __name__ == "__main__":
sample_keywords = [
"best vpn for streaming", "how to use vpn", "buy vpn cheap",
"vpn vs proxy", "what is vpn", "vpn price comparison",
"best vpn", "how to use vpn", # 중복
]
export_to_csv(sample_keywords, "keywords.csv")
이 CSV는 콘텐츠 팀이 FAQ 페이지를 기획하거나, 랜딩 페이지의 H2/H3 구조를 설계할 때 직접 활용할 수 있습니다. 의도별로 분류된 키워드는 SERP 추적 전략 수립에도 유용합니다.
윤리와 컴플라이언스
구글 자동완성 및 PAA 데이터는 공개적으로 접근 가능하지만, 대량 수집 시에는 다음 원칙을 지켜야 합니다:
- 속도 제한 준수: 요청 간
0.5~1초간격을 유지하여 구글 서버에 부하를 주지 않습니다. - robots.txt 확인: 구글 robots.txt를 확인하여 허용된 경로만 수집합니다.
- 공식 API 우선: 대규모 키워드 리서치가 필요하다면
Google Keyword Planner나Semrush API등 공식 API를 우선 고려합니다. - 개인정보 보호: 수집된 데이터에 개인 식별 정보가 포함되지 않도록 확인합니다. GDPR 및 CCPA 요구사항을 준수합니다.
스크래핑은 공개 데이터를 활용하는 합법적인 기술이지만, 대상 플랫폼의 이용약관(ToS)을 반드시 검토해야 합니다. 상업적 목적의 대량 수집은 법적 검토가 필요할 수 있습니다.
ProxyHat 통합 및 관련 리소스
본 가이드의 모든 코드 예제는 ProxyHat 주거 프록시를 통해 차단 없이 실행할 수 있습니다. 웹 스크래핑 사용 사례 페이지에서 더 다양한 스크래핑 시나리오를 확인하세요. ProxyHat은 190개 이상의 국가를 지원하며, 도시 단위 지역 타겟팅으로 로컬 키워드 리서치에 최적화되어 있습니다.
핵심 요약
- 구글 자동완성, PAA, 관련 검색어는 무료 키워드 리서치의 세 가지 핵심 소스입니다.
- a-z 접두사 및 수식어 조합으로 하나의 시드 키워드를
300~500개의 롱테일로 확장할 수 있습니다. - PAA 아코디언은 Playwright로 클릭 확장하여 재귀적으로 질문형 키워드를 수집합니다.
- 주거 프록시의 국가/도시 타겟팅으로 로케일 편향 없는 정확한 로컬 키워드를 수집합니다.
- 수집된 키워드는 중복 제거 → 의도 클러스터링 → CSV 내보내기 파이프라인으로 정리합니다.
- 요청 간
0.5~1초간격을 유지하고, 대규모 수집 시 공식 API를 우선 고려합니다.



