구글에 내 블로그가 검색되지 않는 이유 — Google Indexing API 설정 완벽 가이드
글은 있는데 구글에는 없다
블로그를 만드는 데는 생각보다 시간이 오래 걸렸다. Astro를 고르고, Cloudflare Pages에 배포 설정을 잡고, 테마를 손보고, 첫 포스트를 올렸다. 배포는 성공했고 주소에 접속하면 글이 잘 나왔다. 그런데 구글에 검색하면 아무것도 나오지 않았다.
“블로그 이름”을 검색해도, 포스트 제목을 그대로 붙여넣어도 결과가 없었다. 처음에는 배포가 잘못됐나 싶어 다시 확인했고, robots.txt가 차단하고 있나 싶어 열어봤고, Cloudflare 설정이 문제인가 의심했다. 전부 정상이었다. 단지 구글이 아직 내 블로그를 “발견”하지 못한 것이었다.
이 상황은 신규 블로그라면 거의 누구나 겪는다. 구글은 매일 수십억 개의 페이지를 크롤링하지만, 새로 생긴 사이트가 크롤링 대상에 올라가기까지는 며칠에서 몇 주까지 걸릴 수 있다. 기다리는 것 외에 방법이 없는 것처럼 보이지만, Google Indexing API를 쓰면 이 과정을 대폭 단축할 수 있다. 실제로 20개 포스트 전부를 색인 요청한 뒤, 대부분이 수 시간 내로 구글 검색에 등장했다.
1. 구글은 내 블로그를 어떻게 찾는가
구글이 웹을 인덱싱하는 과정은 세 단계다: 크롤링(Crawling) → 색인 생성(Indexing) → 검색 결과 제공(Serving).
크롤링 단계에서 구글봇(Googlebot)이 링크를 따라 웹을 돌아다니며 새 페이지를 찾는다. 문제는 내 블로그가 아직 아무도 링크를 걸지 않은 상태라면, 구글봇이 내 사이트까지 “도달”하는 경로 자체가 없다는 것이다. sitemap을 제출하면 구글에게 “이 주소들을 봐달라”고 신호를 보낼 수 있지만, 이 신호가 실제 크롤링으로 이어지는 시점은 구글이 알아서 정한다.
색인 생성 단계에서는 크롤링된 페이지의 내용을 분석하고 검색 색인에 추가한다. 크롤링이 됐다고 해서 즉시 색인이 생성되는 것도 아니다. 페이지 품질, 중복 여부, 서버 응답 속도 등 여러 요소가 영향을 미친다.
Indexing API는 이 과정에서 구글에게 “지금 당장 이 URL을 크롤링해달라”는 요청을 직접 보내는 수단이다. 공식적으로는 JobPosting이나 BroadcastEvent 구조화 데이터가 포함된 페이지를 위한 API지만1, 실제로는 일반 블로그 포스트 URL을 보내도 구글이 응답해 크롤링을 수행한다. 이는 많은 개발자와 블로거들이 경험적으로 확인한 사실이고, 실제로 20개 포스트 모두 색인이 잡혔다.
단, API를 사용한다고 해서 색인이 보장되는 것은 아니다. 구글이 요청을 받아 크롤링할 확률을 높이는 것이지, 색인 생성 자체를 강제하는 수단은 아니다. 페이지 품질과 콘텐츠 신뢰도가 뒷받침되어야 실제 검색 결과에 등장한다.
2. 준비물 확인
설정을 시작하기 전에 필요한 것들을 정리하면 다음과 같다.
- Google 계정
- Google Search Console에 등록된 사이트 (소유권 인증 완료)
- Google Cloud Console 프로젝트
- Python 3.x 환경 (
google-auth,requests라이브러리) - 배포된 Astro 블로그의
sitemap-0.xmlURL
3. Google Search Console 소유권 인증
Indexing API를 쓰려면 먼저 Search Console에서 사이트 소유권을 인증해야 한다. 이미 완료된 경우라면 이 단계는 건너뛰면 된다.
Google Search Console에 접속해 “속성 추가”를 클릭한다. 속성 유형은 두 가지다.
- 도메인 속성:
logdew.com형태. DNS TXT 레코드로만 인증 가능. 모든 서브도메인과 http/https를 통합 관리할 수 있어 권장된다. - URL 접두사 속성:
https://logdew.com/형태. HTML 파일 업로드, 메타 태그, Google Analytics 등 다양한 방법으로 인증 가능.
Cloudflare Pages 기준으로 DNS TXT 레코드 방식이 가장 깔끔하다. Cloudflare 대시보드 → 해당 도메인 → DNS → 레코드 탭에서 TXT 레코드를 추가하면 된다. 이름 필드는 @ (루트 도메인), 값 필드에 Search Console이 제공한 인증 코드를 붙여넣으면 된다.
레코드 추가 후 Search Console로 돌아가 “확인” 버튼을 누르면 즉시 또는 수 분 내로 인증이 완료된다. DNS 전파가 느린 경우 최대 48시간이 걸릴 수도 있지만, 보통은 훨씬 빠르다.
4. Sitemap 제출
Search Console에서 소유권 인증이 완료되었다면 sitemap을 등록한다. 왼쪽 사이드바에서 Sitemaps를 클릭하고, “새 사이트맵 추가” 입력란에 sitemap URL을 입력한다.
Astro의 @astrojs/sitemap 패키지는 기본적으로 sitemap-index.xml과 sitemap-0.xml을 함께 생성한다2. Search Console에는 sitemap-index.xml을 제출하면 된다. 예컨대 https://logdew.com/sitemap-index.xml을 입력하면 Google이 이 인덱스 파일을 읽고 내부의 sitemap-0.xml까지 자동으로 파악한다.
제출 후 상태가 “성공”으로 바뀌면 sitemap은 등록된 것이다. 그러나 등록과 색인은 다르다. 상태가 성공이어도 “검색됨 – 현재 색인이 생성되지 않음” 상태로 며칠이 지나는 경우가 흔하다. 이 단계에서 Indexing API가 필요해진다.
5. Google Cloud Console: 서비스 계정 생성
Indexing API는 OAuth 2.0 서비스 계정으로 인증한다. 개인 구글 계정이 아니라, 프로그래밍 방식으로 API에 접근하기 위한 “기계 전용 계정”이다.
5-1. 프로젝트 생성 및 API 활성화
Google Cloud Console에 접속한다. 상단 드롭다운에서 새 프로젝트 만들기를 선택하고 적당한 이름을 지정한다(예: logdew-indexing). 프로젝트가 생성되면 좌측 메뉴에서 API 및 서비스 → 라이브러리로 이동한다. 검색창에 Indexing API를 입력하면 Web Search Indexing API가 나온다. 클릭 후 사용 버튼을 누른다.
5-2. 서비스 계정 생성
API 및 서비스 → 사용자 인증 정보 → 사용자 인증 정보 만들기 → 서비스 계정을 선택한다. 이름을 입력하고(예: indexing-bot) “만들고 계속하기”를 클릭한다. 권한 설정 단계는 선택사항이므로 건너뛰어도 된다. “완료”를 누르면 서비스 계정이 생성된다.
생성된 서비스 계정의 이메일 주소를 메모해둔다. 형식은 indexing-bot@logdew-indexing.iam.gserviceaccount.com 같은 형태다. 이 이메일이 Search Console에서 소유자로 등록될 계정이다.
5-3. JSON 키 다운로드
생성된 서비스 계정을 클릭하고 키 탭으로 이동한다. 키 추가 → 새 키 만들기 → JSON 형식을 선택하면 키 파일이 자동으로 다운로드된다. 이 파일은 한 번만 다운로드할 수 있으므로 안전한 위치에 보관해야 한다. 절대 GitHub 같은 공개 저장소에 올려서는 안 된다.
6. Search Console에 서비스 계정 소유자 등록
서비스 계정이 Indexing API를 통해 URL을 제출할 수 있으려면, 해당 서비스 계정이 Search Console에서 해당 사이트의 소유자(Owner)로 등록되어야 한다.3
Search Console 소유권 확인 페이지로 이동한다(또는 Search Console 내 해당 속성에서 설정 → 사용자 및 권한). “사용자 추가” 버튼을 누르고, 5-2에서 메모해둔 서비스 계정 이메일 주소를 입력한다. 권한은 소유자로 설정해야 한다. “위임된 소유자”가 아니라 검증된 소유자 권한이 필요하다.
이 단계를 “속성 소유자” 탭이 아니라 일반 “사용자” 탭에서 편집자 권한으로만 추가하면 API 요청 시 403 오류가 발생한다. 반드시 소유자 권한으로 추가해야 한다.
7. Python 환경 설정
필요한 라이브러리를 설치한다.
pip install google-auth requests
google-auth는 서비스 계정 JSON 파일을 읽어 자동으로 액세스 토큰을 발급해주는 Google 공식 라이브러리다.4 매번 수동으로 OAuth 토큰을 갱신할 필요 없이, 만료 시 자동 재발급된다.
8. 단일 URL 색인 요청 스크립트
기본 구조를 이해하기 위해 단일 URL을 색인 요청하는 스크립트부터 작성했다.
import json
import requests
from google.oauth2 import service_account
from google.auth.transport.requests import Request
# 서비스 계정 키 파일 경로
SERVICE_ACCOUNT_FILE = "service_account.json"
# Indexing API 엔드포인트
ENDPOINT = "https://indexing.googleapis.com/v3/urlNotifications:publish"
# 인증 범위
SCOPES = ["https://www.googleapis.com/auth/indexing"]
def get_credentials():
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE,
scopes=SCOPES
)
credentials.refresh(Request())
return credentials
def request_indexing(url: str, notification_type: str = "URL_UPDATED"):
credentials = get_credentials()
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {credentials.token}"
}
body = json.dumps({
"url": url,
"type": notification_type # URL_UPDATED 또는 URL_DELETED
})
response = requests.post(ENDPOINT, headers=headers, data=body)
return response.json()
if __name__ == "__main__":
url = "https://logdew.com/posts/my-first-post"
result = request_indexing(url)
print(result)
성공하면 다음과 같은 응답이 돌아온다.
{
"urlNotificationMetadata": {
"url": "https://logdew.com/posts/my-first-post",
"latestUpdate": {
"url": "https://logdew.com/posts/my-first-post",
"type": "URL_UPDATED",
"notifyTime": "2026-02-25T01:23:45.678Z"
}
}
}
이 응답은 “요청이 접수됐다”는 의미이지 즉시 색인이 생성됐다는 뜻이 아니다. 보통 수 분에서 수 시간 내로 Google Search Console의 URL 검사 결과에 변화가 나타난다.
9. sitemap-0.xml 파싱으로 전체 URL 일괄 색인
포스트가 20개였기 때문에 하나씩 URL을 넣는 방식은 비효율적이었다. Astro가 자동 생성한 sitemap-0.xml을 파싱해서 전체 URL을 추출하고, 일괄로 색인 요청하는 스크립트를 작성했다.
import json
import time
import requests
import xml.etree.ElementTree as ET
from google.oauth2 import service_account
from google.auth.transport.requests import Request
SERVICE_ACCOUNT_FILE = "service_account.json"
ENDPOINT = "https://indexing.googleapis.com/v3/urlNotifications:publish"
SCOPES = ["https://www.googleapis.com/auth/indexing"]
# 색인 요청할 사이트의 sitemap URL
SITEMAP_URL = "https://logdew.com/sitemap-0.xml"
def get_credentials():
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE,
scopes=SCOPES
)
credentials.refresh(Request())
return credentials
def get_urls_from_sitemap(sitemap_url: str) -> list[str]:
response = requests.get(sitemap_url)
response.raise_for_status()
root = ET.fromstring(response.content)
# sitemap XML의 네임스페이스
ns = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}
urls = [loc.text for loc in root.findall("sm:url/sm:loc", ns)]
return urls
def request_indexing(url: str, credentials) -> dict:
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {credentials.token}"
}
body = json.dumps({"url": url, "type": "URL_UPDATED"})
response = requests.post(ENDPOINT, headers=headers, data=body)
return response.json()
def main():
print(f"sitemap 파싱 중: {SITEMAP_URL}")
urls = get_urls_from_sitemap(SITEMAP_URL)
print(f"총 {len(urls)}개 URL 발견")
credentials = get_credentials()
for i, url in enumerate(urls, 1):
# 토큰 만료 대비: 매 10개마다 갱신
if i % 10 == 0:
credentials.refresh(Request())
result = request_indexing(url, credentials)
if "urlNotificationMetadata" in result:
print(f"[{i}/{len(urls)}] 성공: {url}")
else:
print(f"[{i}/{len(urls)}] 오류: {url}")
print(f" 응답: {result}")
# API 레이트 제한 대응 (분당 380 요청 한도)
time.sleep(0.5)
print("\n완료. Search Console에서 색인 상태를 확인하면 된다.")
if __name__ == "__main__":
main()
Astro의 sitemap XML은 http://www.sitemaps.org/schemas/sitemap/0.9 네임스페이스를 사용한다. 네임스페이스를 지정하지 않으면 root.findall("url/loc")가 빈 결과를 반환하므로 주의해야 한다.
실행하면 다음과 같은 출력이 나왔다.
sitemap 파싱 중: https://logdew.com/sitemap-0.xml
총 21개 URL 발견
[1/21] 성공: https://logdew.com/
[2/21] 성공: https://logdew.com/posts/my-first-post
[3/21] 성공: https://logdew.com/posts/another-post
...
[21/21] 성공: https://logdew.com/posts/last-post
완료. Search Console에서 색인 상태를 확인하면 된다.
21개(포스트 20개 + 인덱스 페이지)가 전부 “성공” 응답을 받았다. 스크립트 실행 시간은 약 15초였고, 이후 Search Console의 URL 검사 도구에서 하나씩 확인했을 때 대부분 당일 내로 크롤링이 완료되어 있었다. 며칠을 기다려야 하던 색인 작업이 몇 시간으로 줄어든 것이었다.
10. API 사용 제한과 주의사항
Indexing API의 기본 할당량은 다음과 같다5.
| 항목 | 기본값 |
|---|---|
| 하루 publish 요청 (URL_UPDATED + URL_DELETED) | 200건/일 |
| 분당 메타데이터 조회 요청 | 180건/분 |
| 분당 전체 요청 | 380건/분 |
하루 200건이면 신규 블로그 운영에는 충분하다. 포스트가 200개를 넘어가면 여러 날에 나눠 제출하거나 Google에 할당량 증가를 요청할 수 있다.
한 가지 중요한 점은, Indexing API가 공식적으로 지원하는 대상은 JobPosting 또는 BroadcastEvent 구조화 데이터가 있는 페이지다6. 일반 블로그 포스트는 공식 지원 대상이 아니다. 그럼에도 실제로는 크롤링 요청이 접수되고 색인이 생성되는 경우가 많다. 이는 Google이 명시적으로 허용한 사용법이 아니므로, 향후 정책 변경으로 동작이 달라질 수 있다.
또한 색인 요청이 접수됐다고 해서 검색 결과에 반드시 등장하는 것은 아니다. Google은 콘텐츠 품질, 도메인 신뢰도, 중복 여부 등을 종합 평가해 색인에 포함 여부를 결정한다. Indexing API는 크롤링 대기열의 우선순위를 높이는 수단일 뿐이다.
11. 배포 시 자동 색인 요청 연동 (선택)
Cloudflare Pages에 배포할 때마다 자동으로 색인 요청이 전송되도록 연동할 수도 있다.
방법 1: Cloudflare Pages Deploy Hook + GitHub Actions
GitHub Actions에서 배포 완료 이벤트를 감지하고 Python 스크립트를 실행하는 방식이다.
# .github/workflows/indexing.yml
name: Google Indexing After Deploy
on:
deployment_status:
jobs:
index:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install google-auth requests
- name: Run indexing script
env:
SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }}
run: |
echo "$SERVICE_ACCOUNT_JSON" > service_account.json
python indexing.py
GitHub Actions Secrets에 서비스 계정 JSON 파일 내용을 GOOGLE_SERVICE_ACCOUNT_JSON으로 등록해두면, 배포할 때마다 자동으로 전체 sitemap 기반 색인 요청이 실행된다.
방법 2: 새 포스트만 선택적으로 요청
배포할 때마다 전체 sitemap을 제출하면 하루 200건 할당량이 금방 소진될 수 있다. 특히 포스트가 많아지면 문제가 된다. 이를 해결하는 방법은 git의 변경 이력을 활용해 새로 추가된 .md 파일의 URL만 추출하는 것이다.
import subprocess
import json
def get_new_posts() -> list[str]:
"""마지막 커밋에서 새로 추가된 포스트 파일 목록 반환"""
result = subprocess.run(
["git", "diff", "--name-only", "--diff-filter=A", "HEAD~1", "HEAD"],
capture_output=True, text=True
)
new_files = result.stdout.strip().split("\n")
# src/content/posts/*.md 파일만 필터링
post_files = [f for f in new_files if f.startswith("src/content/posts/") and f.endswith(".md")]
# 파일 경로를 URL로 변환
base_url = "https://logdew.com"
urls = []
for f in post_files:
# src/content/posts/my-post.md → /posts/my-post
slug = f.replace("src/content/posts/", "").replace(".md", "")
urls.append(f"{base_url}/posts/{slug}")
return urls
이 방식이면 새 포스트가 1개 올라갈 때 1건만 요청하므로 할당량 낭비가 없다.
12. 결과 확인
색인 요청 후 결과를 확인하는 방법은 두 가지다.
Google Search Console URL 검사 도구: Search Console 좌측 메뉴에서 URL 검사를 클릭하고 URL을 입력하면 “Google이 이 URL을 확인했습니다” 상태와 마지막 크롤링 날짜를 볼 수 있다.
구글 검색으로 직접 확인: 구글 검색창에 site:logdew.com을 입력하면 색인된 페이지 목록이 나온다. 색인이 생성되기 전에는 결과가 없거나 일부만 나온다. Indexing API 요청 후 대부분은 하루 이내로 결과가 변화했다.
Search Console의 색인 생성 현황 보고서에서 “크롤링됨 – 현재 색인이 생성되지 않음” 상태가 “색인이 생성됨”으로 바뀌는 것을 확인하면 최종 완료다.
마치며
구글에 블로그가 보이지 않는 것은 기술적 결함이 아니라, 구글이 아직 내 사이트를 발견하지 못했기 때문인 경우가 대부분이다. sitemap 제출은 방향을 알려주는 신호이고, Indexing API는 크롤링 대기열의 우선순위를 높이는 수단이다.
설정 자체는 생각보다 복잡하지 않았다. Google Cloud Console에서 프로젝트를 만들고 서비스 계정을 생성해 키를 받는 데 10분, Search Console에 소유자로 등록하는 데 5분, Python 스크립트로 전체 sitemap을 제출하는 데 15초. 이후 Search Console에서 색인 상태가 바뀌는 것을 확인하는 재미가 있었다.
중요한 것은 Indexing API는 크롤링 요청이지 색인 보장이 아니라는 점이다. 콘텐츠 자체의 품질이 뒷받침되어야 한다. API는 구글의 문을 빠르게 두드리는 수단이고, 문이 열릴지는 여전히 콘텐츠에 달려 있다.
Footnotes
-
Google Search Central. “Indexing API Quickstart.” Last updated 2025-12-10. ↩
-
Astro Docs. “@astrojs/sitemap.” 공식 문서에서
sitemap-index.xml과 개별sitemap-0.xml생성 방식을 설명한다. ↩ -
Google Search Central. “Prerequisites for the Indexing API.” 서비스 계정을 사이트 소유자로 등록하는 절차를 안내한다. ↩
-
Google. “google-auth Python Library.” Google 공식 Python 인증 라이브러리 문서. ↩
-
Google Search Central. “Requesting Approval and Quota.” 기본 할당량 200건/일, 180건/분(메타데이터), 380건/분(전체) 명시. ↩
-
Google Search Central. “How to Use the Indexing API.” 공식 지원 대상은
JobPosting및BroadcastEvent구조화 데이터 포함 페이지. ↩
댓글