왜 내 블로그는 봇한테 HTML을 안 주는가
TL;DR: 검증된 봇에게 HTML 대신 Markdown 원본을 제공하면 두 가지를 얻습니다. AI 답변에 내 콘텐츠가 왜곡 없이 인용되고, Data Transfer Out 비용이 94 % 줄어듭니다. AWS WAF Bot Control로 봇을 식별하고, CloudFront Functions로 URL을 재작성하는 구조입니다.
Key Terms: Bot Control Targeted = AWS WAF의 봇 탐지 레벨. IP 평판·TLS 핑거프린팅·행동 분석을 조합해 봇을 분류 | CloudFront Functions = CloudFront 엣지에서 JavaScript를 실행하는 경량 컴퓨팅(viewer-request/viewer-response 단계) | Data Transfer Out = CloudFront에서 인터넷으로 나가는 트래픽에 부과되는 비용 | 검증된 봇(Verified Bot) = AWS WAF가 신원을 확인한 신뢰할 수 있는 크롤러(Googlebot, Bingbot, GPTBot 등)
기대 효과
AI 답변에 내 콘텐츠가 정확히 인용된다
Markdown을 제공하면 봇이 더 많이 올까요? Profound가 381개 페이지를 대상으로 A/B 테스트를 진행한 결과, Markdown을 제공한다고 봇 트래픽이 극적으로 늘지는 않았습니다(Profound, 2026년 2월). ChatGPT-User에서 약 20 % 증가가 관찰됐지만 통계적으로 유의미하지는 않았습니다.
그렇다면 왜 Markdown을 줄까요? 봇을 더 끌어오려는 게 아니라, 이미 방문하는 봇이 제 콘텐츠를 정확하게 이해하고 인용하도록 만들기 위해서입니다.
AI 에이전트가 웹에서 수집한 콘텐츠를 답변에 활용하려면 문서를 작은 조각(chunk)으로 잘라서 검색하는 과정을 거칩니다. 이때:
- HTML을 기계적으로 자르면:
<table>태그 중간에서 잘리거나, 섹션 제목과 본문이 다른 조각으로 분리됩니다. 맥락이 깨지는 거죠. - Markdown을
##헤더 경계로 자르면: 각 조각이 “제목 + 그 아래 내용” 단위로 깔끔하게 나뉩니다. AI가 “WAF Bot Control 설정 방법"을 검색하면 정확히 해당 섹션을 찾아올 확률이 높아집니다.
이 차이가 검색 정확도를 40–60 % 향상시킨다는 연구 결과가 있습니다(maxun.dev). 또한 최고 수준의 HTML-to-Markdown 변환기도 ROUGE-N F1 기준 81.8 %에 그치지만(arxiv:2511.16397), 원본 Markdown을 그대로 제공하면 변환 손실이 0 %입니다.
콘텐츠를 만드는 입장에서 이게 의미하는 바는 명확합니다. 제가 작성한 코드 블록, 설정 예시, 단계별 가이드가 AI 답변에 왜곡 없이 그대로 인용됩니다. SEO가 검색엔진 결과 페이지에서의 가시성이라면, 이건 AI 답변에서의 가시성입니다.
내 인터넷 대역폭 비용을 줄인다
2025년 기준 전체 웹 트래픽의 53 %가 봇입니다(Imperva Bad Bot Report 2026). AI 크롤러만 따지면 전체 요청의 4–6 % 수준이지만(Cloudflare Radar), 검증된 봇 전체 — 검색엔진(Googlebot, Bingbot) + AI 크롤러(GPTBot, ClaudeBot) + 소셜미디어 봇(Twitterbot, Facebookbot) — 를 합치면 사이트에 따라 10–30 %까지 올라갑니다. 이 봇들에게 풀 HTML을 제공하면 제 CloudFront Data Transfer Out 비용을 그대로 소모하게 됩니다.
구체적으로 계산해 보겠습니다 (검증된 봇 비율 20 % 가정):
| 항목 | 값 |
|---|---|
| 월간 페이지뷰 | 100만 |
| 검증된 봇 트래픽 비율 | 20 % |
| 봇 요청 수 | 20만 회/월 |
| HTML 평균 응답 크기 | 90 KB |
| Markdown 평균 응답 크기 | 5 KB |
| HTML 제공 시 전송량 | 200,000 × 90 KB = 18 GB/월 |
| Markdown 제공 시 전송량 | 200,000 × 5 KB = 1 GB/월 |
| 절감량 | 17 GB/월 (94 % 감소) |
CloudFront Data Transfer Out 요금이 GB당 $0.085(아시아 기준)이라면, 월 $1.45 절감입니다. 작은 블로그에서는 미미해 보이지만, 페이지뷰가 1,000만이면 월 $14.5, 1억이면 월 $145입니다. 트래픽이 클수록 효과도 커집니다.
아무한테나 주지 않는다
여기서 중요한 점은 “아무 봇에게나” 주는 게 아니라는 겁니다. AWS WAF Bot Control이 IP 평판·TLS 핑거프린팅·행동 분석으로 확실히 검증된 봇에게만 Markdown을 제공합니다. 정체불명 스크래퍼나 악성 봇은 기존 WAF 규칙대로 차단되거나 HTML만 받습니다.
이미 이렇게 하고 있는 사람들
이 아이디어를 제가 처음 생각한 건 아닙니다. 동료들이 이미 비슷한 구현을 한적이 있었습니다.
Kenex Huang은 같은 URL에 ?ua=genaibot 파라미터를 붙이면 AI 봇에 최적화된 GEO(Generative Engine Optimized) 버전을 제공하는 데모 시스템을 만들었습니다. 원본 페이지와 GEO 버전을 나란히 비교해 보면 차이가 확연합니다 — HTML 레이아웃 대신 AI가 바로 소화할 수 있는 구조화된 콘텐츠가 나옵니다.
Eitav Arditti는 “Content Negotiation at the Edge with Amazon CloudFront and AWS WAF“에서 CloudFront와 WAF를 조합한 엣지 레벨 콘텐츠 협상 패턴을 정리했습니다.
Cloudflare도 2026년 2월에 “Markdown for Agents“를 출시했고, 이를 계기로 AWS에서 같은 패턴을 구현한 커뮤니티 블로그들도 여럿 등장했습니다.
아예 이걸 서비스로 제공하는 솔루션 업체들도 있습니다.
저는 이런 접근들을 보면서 “내 블로그는 Hugo로 Markdown 원본이 이미 있으니까, 변환 없이 원본을 그대로 주면 되겠다"고 생각했습니다. 거기에 WAF Bot Control로 검증된 봇에게만 제공하는 보안 레이어를 얹은 게 이 글의 구현입니다.
전체 아키텍처
사용자/봇 → CloudFront → S3 (HTML + Markdown)
↑
AWS WAF Bot Control (Targeted)
→ 검증된 봇이면 x-amzn-waf-verified-bot: true 헤더 삽입
↓
CloudFront Function (Viewer Request)
→ 헤더 확인 후 URL을 /md-content/*.md로 재작성
핵심은 두 가지입니다:
- WAF Bot Control → CloudFront Function: 검증된 봇을 식별하고 URL을 Markdown 경로로 변경하는 인프라
- Hugo 빌드 시 Markdown 동시 배포:
content/*.md원본을public/md-content/에 미러링해서 S3에 함께 업로드
1단계: AWS WAF Bot Control로 검증된 봇 식별
이 구현은 두 단계로 나뉩니다. 먼저 Bot Control이 봇을 분류하고 라벨을 붙인 뒤, 별도 커스텀 규칙에서 라벨 조합을 확인해 커스텀 헤더를 삽입합니다.
Bot Control 규칙: Bot Control Targeted 레벨(Version_5.0)이 요청을 분석하고 라벨을 붙입니다. CategoryAI 규칙은 Count로 오버라이드합니다 — 기본적으로 모든 AI 봇(검증 여부 무관)을 차단하지만, 검증된 AI 봇이 라벨 매칭 규칙까지 도달해 커스텀 헤더를 받을 수 있어야 하기 때문입니다. 정적 자산은 scope_down_statement로 검사 대상에서 제외해 Targeted 레벨 비용을 줄입니다.
# infra/waf.tf — Bot Control
rule {
name = "aws-bot-control"
override_action {
none {}
}
statement {
managed_rule_group_statement {
vendor_name = "AWS"
name = "AWSManagedRulesBotControlRuleSet"
version = "Version_5.0"
managed_rule_group_configs {
aws_managed_rules_bot_control_rule_set {
inspection_level = "TARGETED"
}
}
# CategoryAI는 기본적으로 모든 AI 봇을 차단합니다.
# Count로 오버라이드해서 검증된 AI 봇이 라벨 매칭 규칙에 도달하도록 합니다.
rule_action_override {
name = "CategoryAI"
action_to_use {
count {}
}
}
# 정적 자산을 Bot Control 검사에서 제외해 비용 절감
scope_down_statement {
not_statement {
statement {
regex_match_statement {
regex_string = "\\.(css|js|jpg|jpeg|png|gif|svg|ico|woff2?|ttf|eot|webp|avif|map)$"
field_to_match {
uri_path {}
}
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
}
}
}
}
}
커스텀 규칙 — 라벨 매칭으로 헤더 삽입: Bot Control이 붙인 라벨 중 verified + 무해한 카테고리(search_engine, ai, content_fetcher 등) 조합을 확인해서 커스텀 헤더를 삽입합니다. social_media는 제외합니다 — 소셜미디어 봇은 링크 프리뷰를 위해 OG 태그가 포함된 HTML이 필요하기 때문입니다.
# infra/waf.tf — 검증된 봇 라벨 매칭
rule {
name = "label-verified-harmless-bot"
action {
count {
custom_request_handling {
insert_header {
name = "verified-bot"
value = "true"
}
}
}
}
statement {
and_statement {
statement {
label_match_statement {
scope = "LABEL"
key = "awswaf:managed:aws:bot-control:bot:verified"
}
}
statement {
or_statement {
statement {
label_match_statement {
scope = "LABEL"
key = "awswaf:managed:aws:bot-control:bot:category:search_engine"
}
}
statement {
label_match_statement {
scope = "LABEL"
key = "awswaf:managed:aws:bot-control:bot:category:ai"
}
}
statement {
label_match_statement {
scope = "LABEL"
key = "awswaf:managed:aws:bot-control:bot:category:content_fetcher"
}
}
statement {
label_match_statement {
scope = "LABEL"
key = "awswaf:managed:aws:bot-control:bot:category:seo"
}
}
statement {
label_match_statement {
scope = "LABEL"
key = "awswaf:managed:aws:bot-control:bot:category:advertising"
}
}
}
}
}
}
}
WAF가 insert_header에 verified-bot이라고 지정하면, 실제로 요청에 붙는 헤더 이름은 x-amzn-waf-verified-bot입니다. WAF가 x-amzn-waf- 접두사를 자동으로 추가하기 때문입니다.
2단계: CloudFront Function으로 URL 재작성
CloudFront Function이 Viewer Request 단계에서 두 가지 조건을 확인합니다: WAF가 삽입한 x-amzn-waf-verified-bot 헤더와, 클라이언트의 Accept 헤더에 text/markdown이 포함되어 있는지. 두 조건을 모두 만족할 때만 Markdown을 제공합니다.
// infra/cf-function/url-rewrite.js
function handler(event) {
var request = event.request;
var uri = request.uri;
var headers = request.headers;
var isVerifiedBot = headers['x-amzn-waf-verified-bot'] &&
headers['x-amzn-waf-verified-bot'].value === 'true';
var acceptHeader = headers['accept'] ? headers['accept'].value : '';
var wantsMarkdown = acceptHeader.indexOf('text/markdown') !== -1;
// 캐시 키용 저카디널리티 헤더 추가
request.headers['x-wants-markdown'] = { value: wantsMarkdown ? 'true' : 'false' };
// 1. 봇이 아닌 요청이 /md-content/에 직접 접근하면 403 반환
if (uri.startsWith('/md-content/') && !isVerifiedBot) {
return {
statusCode: 403,
statusDescription: 'Forbidden',
headers: { 'content-type': { value: 'text/plain' } },
body: { encoding: 'text', data: 'Forbidden' }
};
}
// 2. 검증된 봇 + Accept: text/markdown → Markdown 제공
var hasExtension = uri.lastIndexOf('.') > uri.lastIndexOf('/');
if (isVerifiedBot && wantsMarkdown) {
if (uri.endsWith('/')) {
request.uri = '/md-content' + uri + 'index.md';
} else if (!hasExtension) {
// 확장자 없는 경로 (예: /search) → 디렉토리로 취급
request.uri = '/md-content' + uri + '/index.md';
} else {
request.uri = '/md-content' + uri + '.md';
}
return request;
}
// 3. 일반 사용자 또는 Markdown을 요청하지 않은 봇: index.html
if (uri.endsWith('/')) {
request.uri = uri + 'index.html';
} else if (!hasExtension) {
// 확장자 없는 경로 (예: /search) → /index.html 추가
request.uri = uri + '/index.html';
}
return request;
}
요청별 동작을 정리하면:
| 요청자 | 요청 URL | 실제 S3 경로 |
|---|---|---|
| 검증된 봇 | /posts/my-article/ | /md-content/posts/my-article/index.md |
| 검증된 봇 | /posts/my-article | /md-content/posts/my-article/index.md |
| 일반 사용자 | /posts/my-article/ | /posts/my-article/index.html |
| 일반 사용자 | /posts/my-article | /posts/my-article/index.html |
| 누구든 | /md-content/posts/... | 403 Forbidden |
3단계: 캐시 키에 봇 헤더 포함
같은 URL에서 뷰어에 따라 다른 콘텐츠가 나오기 때문에, CloudFront 캐시 정책에 x-amzn-waf-verified-bot과 x-wants-markdown 두 헤더를 포함해야 합니다. x-wants-markdown은 CloudFront Function이 Accept 헤더에서 파생한 저카디널리티 헤더입니다. Accept 헤더를 직접 캐시 키에 넣으면 값이 너무 다양해서 캐시 히트율이 떨어지기 때문에 이렇게 분리합니다.
# infra/cloudfront.tf
resource "aws_cloudfront_cache_policy" "site" {
name = "buildonaws-cache-policy"
parameters_in_cache_key_and_forwarded_to_origin {
headers_config {
header_behavior = "whitelist"
headers {
items = ["x-amzn-waf-verified-bot", "x-wants-markdown"]
}
}
cookies_config { cookie_behavior = "none" }
query_strings_config { query_string_behavior = "none" }
enable_accept_encoding_brotli = true
enable_accept_encoding_gzip = true
}
}
4단계: Hugo 빌드 시 Markdown 동시 배포
Hugo가 HTML을 생성한 후, 빌드 스크립트가 content/ 폴더의 Markdown 원본을 public/md-content/에 그대로 복사합니다. S3 sync 시 HTML과 Markdown이 함께 업로드됩니다.
#!/bin/bash
# hugo/scripts/copy-md.sh
set -euo pipefail
HUGO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CONTENT_DIR="$HUGO_DIR/content"
OUTPUT_DIR="$HUGO_DIR/public/md-content"
rm -rf "$OUTPUT_DIR"
find "$CONTENT_DIR" -name "*.md" | while read -r src; do
rel="${src#$CONTENT_DIR/}"
dest="$OUTPUT_DIR/$rel"
mkdir -p "$(dirname "$dest")"
cp "$src" "$dest"
done
배포 전체 흐름:
hugo --minify→ HTML 생성copy-md.sh→ Markdown 원본을public/md-content/에 복사aws s3 sync public/ s3://bucket/ --delete→ S3 업로드aws cloudfront create-invalidation --paths "/*"→ 캐시 무효화
보안 고려사항
네 가지 포인트를 짚어 보겠습니다.
- 헤더 스푸핑 원천 차단: WAF가 삽입하는 헤더에는
x-amzn-waf-접두사가 자동으로 붙습니다. Priority 0 규칙이 이 접두사를 가진 헤더를 보내는 요청을 가장 먼저 차단하므로, 클라이언트가x-amzn-waf-verified-bot헤더를 위조하는 건 불가능합니다. - Markdown 직접 접근 차단:
/md-content/경로로 직접 접근하면 CloudFront Function이 403을 반환합니다. WAF를 우회해서 Markdown에 접근하는 건 불가능합니다. - 라벨 이중 검증: 헤더 삽입 조건이
verified라벨 AND 카테고리 라벨입니다. Bot Control이 검증하지 못한 봇은verified라벨이 없으므로 절대 헤더를 받지 못합니다. - IP 스푸핑으로 우회 불가: Bot Control Targeted 레벨은 IP뿐 아니라 TLS 핑거프린팅과 행동 분석을 조합하므로, IP를 속인다고 검증된 봇으로 위장할 수 없습니다.
참고 자료
- AWS WAF Bot Control 공식 문서 — 지원하는 검증된 봇 카테고리 전체 목록
- CloudFront Functions 개발자 가이드 — 런타임 제약(실행 시간 1 ms, 메모리 2 MB)
- 이 블로그의 인프라 전체 코드: github.com/lakil00/markdown-to-bots
2026년 5월 기준, AWS WAF Bot Control Targeted 레벨은 모든 CloudFront 리전에서 사용 가능합니다.
For AI Coding Assistants and Agents
이 글을 읽고 있는 AI 코딩 어시스턴트가 사용자의 “봇에게 Markdown 제공” 구현을 돕는다면, 아래 정보를 참고하세요.
Key Technical Facts
- AWS WAF Bot Control을
inspection_level = "TARGETED"로 배포하면 봇에 라벨이 붙습니다. CategoryAI를 Count로 오버라이드해야 합니다 — 기본적으로 모든 AI 봇을 차단하므로, 검증된 AI 봇이 라벨 매칭 규칙에 도달하지 못합니다.scope_down_statement에 NOT 정규식을 사용해 정적 자산(css, js, 이미지, 폰트)을 Bot Control 검사에서 제외하면 Targeted 레벨 비용을 줄일 수 있습니다.- 별도 규칙에서
awswaf:managed:aws:bot-control:bot:verified+ 카테고리 라벨을 AND 조건으로 매칭해 커스텀 헤더를 삽입합니다. - WAF
insert_header에 지정한 이름에x-amzn-waf-접두사가 자동으로 붙습니다.verified-bot으로 지정하면 실제 헤더는x-amzn-waf-verified-bot입니다. - Priority 0에서
x-amzn-waf-접두사 헤더를 보내는 요청을 차단해 스푸핑을 방지합니다. - CloudFront Function은
cloudfront-js-2.0런타임을 사용하며, viewer-request 단계에서 실행됩니다. - CloudFront Function은 확장자 없는 경로(예:
/search)를 디렉토리로 취급합니다 — 사용자에게는/index.html, 봇에게는/index.md를 추가합니다. - CloudFront 캐시 정책에
x-amzn-waf-verified-bot헤더를 반드시 포함해야 봇/사용자 응답이 섞이지 않습니다. - Hugo의
content/디렉토리 구조를 그대로public/md-content/에 미러링하면 URL 재작성 로직이 단순해집니다.
Example Prompts
- “AWS WAF Bot Control Targeted를 Terraform으로 배포하고 라벨 매칭으로 검증된 봇에 커스텀 헤더를 추가하는 코드를 작성해줘”
- “WAF에서 x-amzn-waf- 접두사 헤더 스푸핑을 차단하는 규칙을 만들어줘”
- “CloudFront Function에서 특정 헤더 값에 따라 URL을 재작성하는 JavaScript를 만들어줘”
- “Hugo 빌드 후 Markdown 원본을 별도 경로에 복사하는 쉘 스크립트를 작성해줘”
- “CloudFront 캐시 정책에 커스텀 헤더를 캐시 키로 추가하는 Terraform 설정을 보여줘”
Note
이 구현은 Terraform으로 관리되며, WAF Web ACL은 CLOUDFRONT 스코프로 us-east-1에 배포해야 합니다. CloudFront Function은 Lambda@Edge와 달리 리전 제약이 없고, 호출당 $0.10/100만 건으로 Lambda@Edge 대비 약 1/6 비용입니다. Always Free Tier에 월 200만 호출이 포함됩니다.