CloudWatch RUM + CloudFront Functions + KVS로 데이터 기반 Speculation Rules 구축하기
TL;DR: Speculation Rules API는 사용자가 다음에 방문할 페이지를 브라우저가 미리 로드하거나 렌더링하도록 지시합니다. 핵심 과제는 “어떤 페이지를” 예측하느냐입니다. 이 글에서는 자동화 파이프라인을 구축합니다: CloudWatch RUM이 실제 사용자 네비게이션 패턴을 수집하고, 일일 Lambda가 페이지별 상위 목적지를 계산하여 S3에 JSON 파일로 업로드하고 CloudFront KeyValueStore에 매핑을 기록합니다. CloudFront Function(viewer-response)이 Speculation-Rules 응답 헤더를 동적으로 삽입합니다. 데이터가 없는 페이지는 안전한 기본값(pointer-down prefetch)을 적용합니다. Terraform 코드 전체 포함.
Key Terms: Speculation Rules API = 브라우저가 다음 네비게이션을 prefetch/prerender하도록 JSON 규칙을 받는 API (Chrome 109+) | Prefetch = 페이지 리소스를 미리 다운로드 (네트워크 비용만 발생) | Prerender = 페이지를 숨겨진 탭에서 완전히 렌더링 (네비게이션 시 거의 즉시 전환) | CloudWatch RUM = 실제 방문자의 페이지뷰, 네비게이션 경로, 성능 메트릭을 수집하는 Real User Monitoring 서비스 | CloudFront KeyValueStore (KVS) = CloudFront Functions에서 읽을 수 있는 글로벌 저지연 키-값 스토어 | Eagerness = Speculation Rules의 트리거 타이밍 파라미터: immediate, eager, moderate (hover), conservative (pointer-down)
계기
최근 한 웹사이트의 HTTP 응답 헤더를 살펴보다가 처음 보는 헤더를 발견했습니다: Speculation-Rules. CDN 쪽 일을 하고 있어서 브라우저가 콘텐츠를 가져오는 방식에 영향을 주는 건 뭐든 궁금합니다.
찾아보니 Speculation Rules API는 Chrome이 기존의 <link rel="prerender">와 실패한 HTTP/2 Server Push를 대체하기 위해 만든 현대적 방식이었습니다. 이전 접근들과 다른 점은, 브라우저가 언제 얼마나 적극적으로 페이지를 미리 로드할지 스스로 판단한다는 것입니다. 개발자는 선언적 JSON 형식으로 의도만 전달합니다.
아이디어는 단순합니다: 사용자가 다음에 갈 가능성이 높은 페이지를 브라우저에 알려주면, 브라우저가 해당 페이지를 미리 fetch하거나 완전히 렌더링합니다. Prerender된 페이지로 이동하면 체감 로딩 시간은 사실상 0ms입니다.
“클릭하기 전에 로드하기"의 역사
웹 플랫폼은 이 문제를 여러 번 해결하려 했습니다:
| 연도 | 기술 | 접근 방식 | 결과 |
|---|---|---|---|
| 2008 | <link rel="prefetch"> | 다음 페이지 리소스 다운로드 | 현존하지만 단일 리소스 제한 |
| 2011 | <link rel="prerender"> | 전체 페이지를 숨겨진 탭에서 렌더링 | Chrome에서 제거 (2023) — Speculation Rules로 대체 |
| 2015 | HTTP/2 Server Push | 서버가 요청 없이 리소스를 밀어넣음 | Chrome 106에서 제거 (2022) — 브라우저 캐시 무시, CDN 호환 문제 |
| 2017 | 103 Early Hints | 응답 본문 전에 preload 힌트 전송 | 현존하지만 origin 처리 시간이 길 때만 유효 |
| 2023 | Speculation Rules API | prefetch/prerender를 위한 선언적 JSON 규칙 | 현재 표준 (Chrome 109+) |
핵심 교훈: 서버 주도 방식은 실패하고, 브라우저 주도 방식이 살아남았습니다. 서버/CDN은 힌트를 제공하고, 브라우저가 리소스 관리에 대한 최종 판단을 합니다.
Prefetch vs Prerender
| Prefetch | Prerender | |
|---|---|---|
| 하는 일 | HTML과 서브리소스 다운로드만 | 숨겨진 탭에서 페이지를 완전히 렌더링 |
| 리소스 비용 | 네트워크만 (수 KB~MB) | 네트워크 + CPU + 메모리 (전체 페이지) |
| 전환 시 체감 | 네트워크 시간 절약 (100-500ms) | 거의 즉시 (0ms 체감) |
| JS 실행 | 안 함 | 함 |
| 부작용 위험 | 없음 (다운로드만) | 있음 (analytics, API 호출 등) |
| 브라우저 제한 | 느슨함 | Chrome: 최대 2개 동시 prerender |
| 적합한 경우 | 후보 페이지 여러 개 (틀려도 손해 적음) | 확실한 단일 목적지 |
비즈니스 임팩트: 왜 이걸 해야 하는가
거의 즉시 페이지 전환은 단순한 UX 개선이 아니라 매출과 직결됩니다. 여러 사례 연구가 이를 증명합니다.
Speculation Rules 적용 후 검증된 결과
| 기업 | 구현 방식 | LCP 개선 | 비즈니스 결과 | 출처 |
|---|---|---|---|---|
| Ray-Ban | PLP→PDP prerender | -43% (4.69s → 2.66s 모바일) | 모바일 전환율 +101%, 데스크톱 +156% | web.dev, 2025.01 |
| Monrif (이탈리아 뉴스) | 기사 페이지 prerender | -17.9% LCP | 사용자 engagement +8.9% | web.dev, 2025 |
| Shopify (전체 플랫폼) | Moderate prefetch | -285ms TTFB, -228ms LCP | TTFB 중간값 0에 근접; 클릭 전에 HTML 도착 | Shopify Performance, 2026.05 |
Ray-Ban의 결과가 특히 강력합니다: 상품 상세 페이지(PDP)를 prerender한 결과, 모바일 전환율이 2배, 데스크톱은 2.5배 이상 증가했으며, 이탈율은 13% 감소했습니다. A/B 테스트로 확인된 수치입니다.
속도 → 매출 관계
산업 전반의 연구 결과:
- 100ms 개선 = 전환율 8.4% 증가 (소매), 10.1% (여행) — Akamai, 100억 세션 분석 (NitroPack)
- 1초 지연 = 전환율 7% 감소 — Portent study (Colorlib, 2026)
- 연 $10M 매출 사이트에서 500ms 개선 ≈ $500K 매출 회복 — Digital Applied, 2026
Speculation Rules의 prerender는 100ms가 아닌 수백 ms~수 초를 개선합니다. Shopify 측정 기준으로 중간값 TTFB가 0에 근접 — 사용자가 클릭하기 전에 페이지가 이미 로드되어 있습니다.
이 파이프라인이 가장 효과적인 경우
| 높은 효과 | 낮은 효과 |
|---|---|
| E-commerce: 목록 → 상세 전환 (Ray-Ban 패턴) | SPA (이미 클라이언트 라우팅) |
| 뉴스/미디어: 홈 → 기사 전환 (Monrif 패턴) | 이미 sub-second LCP인 사이트 |
| MPA (매 네비게이션이 서버 왕복) | Safari/Firefox 비율 높은 사이트 (헤더 무시) |
| 모바일 사용자 (네트워크 지연 큼 = prefetch 효과 큼) |
예측 문제
Speculation Rules의 효과는 예측 정확도에 달려 있습니다. Cloudflare는 Speed Brain (2024년 10월)으로 대시보드 토글 하나로 해결했습니다 — 자체 analytics 데이터 기반 자동 Speculation Rules 삽입.
AWS에도 동일한 데이터가 있습니다: CloudWatch RUM이 페이지뷰와 네비게이션 경로를 수집하고, CloudFront Functions가 응답 헤더를 삽입할 수 있고, KeyValueStore가 글로벌 저지연 읽기를 제공합니다. 조각을 조립하면 됩니다.
아키텍처
┌─────────────────────────────────────────────────────────────────────────┐
│ 일일 배치 파이프라인 │
│ │
│ CloudWatch RUM ──▶ Lambda (daily, EventBridge) │
│ (page_view_event │ │
│ + parentPageId) ├──▶ S3: /_speculation/{path}.json │
│ └──▶ KVS: page_path → rules file path │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 요청 시점 제공 │
│ │
│ 사용자 ──▶ CloudFront ──▶ Origin (S3) │
│ │ │
│ ▼ (viewer-response) │
│ CloudFront Function │
│ │ │
│ ├── 응답이 text/html인가? │
│ │ ├── Yes: KVS 조회 (key = 정규화된 URI) │
│ │ │ ├── 히트: Speculation-Rules: "/{path}.json" │
│ │ │ └── 미스: Speculation-Rules: "/default.json" │
│ │ └── No: 통과 (헤더 미삽입) │
│ ▼ │
│ Speculation-Rules 헤더 포함 응답 ──▶ 브라우저 │
└─────────────────────────────────────────────────────────────────────────┘
Step 1: CloudWatch RUM으로 네비게이션 패턴 수집
CloudWatch RUM은 실제 사용자 세션에서 page_view_event를 캡처합니다. 이 이벤트에는 parentPageId (이전 페이지)가 포함되어 있어 “A 페이지 → B 페이지” 전환 패턴을 추출할 수 있습니다.
Terraform: RUM + Cognito Identity Pool
CloudWatch RUM은 브라우저 클라이언트 인증을 위해 Cognito Identity Pool이 필요합니다:
# rum.tf
locals {
rum_domain = var.rum_domain != "" ? var.rum_domain : var.domain_name
}
data "aws_caller_identity" "current" {}
resource "aws_cognito_identity_pool" "rum" {
identity_pool_name = "buildonaws-rum"
allow_unauthenticated_identities = true
}
resource "aws_iam_role" "rum_unauthenticated" {
name = "buildonaws-rum-unauthenticated"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = "cognito-identity.amazonaws.com" }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"cognito-identity.amazonaws.com:aud" = aws_cognito_identity_pool.rum.id
}
"ForAnyValue:StringLike" = {
"cognito-identity.amazonaws.com:amr" = "unauthenticated"
}
}
}]
})
}
# 최소 권한: PutRumEvents만 허용, 이 app monitor로 스코프 제한
resource "aws_iam_role_policy" "rum_put_events" {
name = "rum-put-events"
role = aws_iam_role.rum_unauthenticated.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "rum:PutRumEvents"
Resource = "arn:aws:rum:us-east-1:${data.aws_caller_identity.current.account_id}:appmonitor/${local.rum_domain}"
}]
})
}
resource "aws_cognito_identity_pool_roles_attachment" "rum" {
identity_pool_id = aws_cognito_identity_pool.rum.id
roles = { unauthenticated = aws_iam_role.rum_unauthenticated.arn }
}
resource "aws_rum_app_monitor" "site" {
name = local.rum_domain
domain = local.rum_domain
app_monitor_configuration {
allow_cookies = true
enable_xray = false
session_sample_rate = var.rum_sample_rate
telemetries = ["performance"]
guest_role_arn = aws_iam_role.rum_unauthenticated.arn
identity_pool_id = aws_cognito_identity_pool.rum.id
}
}
RUM Web Client 스니펫
terraform apply 후 rum_app_monitor_id와 rum_identity_pool_id output을 받습니다. 사이트 <head>에 추가:
<!-- CloudWatch RUM (production only) -->
<script>
(function(n,i,v,r,s,c,x,z){
x=window.AwsRumClient={q:[],n:n,i:i,v:v,r:r,c:c};
window[n]=function(c,p){x.q.push({c:c,p:p})};
z=document.createElement('script');z.async=true;z.src=s;
document.head.insertBefore(z,document.head.getElementsByTagName('script')[0]);
})(
'cwr',
'YOUR_APP_MONITOR_ID',
'1.0.0',
'us-east-1',
'https://client.rum.us-east-1.amazonaws.com/1.x/cwr.js',
{
sessionSampleRate: 1,
identityPoolId: 'us-east-1:YOUR_IDENTITY_POOL_ID',
endpoint: 'https://dataplane.rum.us-east-1.amazonaws.com',
telemetries: ['performance'],
allowCookies: true,
enableXRay: false
}
);
</script>
Hugo 팁: 테마의 head partial을 오버라이드하여
{{ if hugo.IsProduction }}조건으로 프로덕션에서만 삽입합니다. ID는hugo.toml의[params.rum]섹션에 정의하여 템플릿에서 참조합니다.
Step 2: 일일 Lambda로 페이지별 Speculation Rules 생성
Lambda 함수가 매일(EventBridge 트리거) 실행되어, 지난 24시간의 RUM page_view_event에서 parentPageId → pageId 전환을 집계하고, 페이지별 규칙 JSON을 S3에 업로드하고, KVS에 매핑을 기록합니다.
로직
| 조건 | 결과 |
|---|---|
페이지 이탈 총 횟수 < MIN_TRANSITIONS (기본 10) | 규칙 생성 안 함 (기본값 사용) |
| 1위 목적지 점유율 >= 50% | prerender (eagerness: moderate) |
| 나머지 상위 목적지 (최대 5개) | prefetch (eagerness: moderate) |
Lambda 함수
"""CloudWatch RUM page_view 데이터에서 페이지별 speculation rules를 생성한다.
awscrt 필수: CloudFront KeyValueStore data plane API(UpdateKeys)는
SigV4A 서명을 사용하며, botocore는 CRT 확장이 있어야 지원한다.
"""
import json
import logging
import os
import time
from collections import Counter, defaultdict
from urllib.parse import urlsplit
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
RUM_APP_MONITOR_NAME = os.environ['RUM_APP_MONITOR_NAME']
KVS_ARN = os.environ['KVS_ARN']
S3_BUCKET = os.environ['S3_BUCKET']
S3_PREFIX = os.environ.get('S3_PREFIX', '_speculation')
MIN_TRANSITIONS = int(os.environ.get('MIN_TRANSITIONS', '10'))
CONFIDENCE_THRESHOLD_PRERENDER = float(
os.environ.get('CONFIDENCE_THRESHOLD_PRERENDER', '0.5'))
MAX_PREFETCH = int(os.environ.get('MAX_PREFETCH', '5'))
EAGERNESS_PRERENDER = os.environ.get('EAGERNESS_PRERENDER', 'moderate')
EAGERNESS_PREFETCH = os.environ.get('EAGERNESS_PREFETCH', 'moderate')
EXCLUDE_PATHS = tuple(
p for p in os.environ.get('EXCLUDE_PATHS', '').split(',') if p)
PAGE_VIEW_EVENT = 'com.amazon.rum.page_view_event'
KVS_BATCH_SIZE = 100
rum = boto3.client('rum')
s3 = boto3.client('s3')
kvs = boto3.client('cloudfront-keyvaluestore')
def normalize_path(value):
"""URL에서 path만 추출, trailing slash 제거, 빈 값은 '/'."""
if not value:
return None
path = urlsplit(value).path or '/'
if len(path) > 1:
path = path.rstrip('/') or '/'
return path
def collect_transitions():
"""지난 24시간 RUM page_view_event에서 parent→child 전환 추출."""
after = int((time.time() - 86400) * 1000)
transitions = defaultdict(Counter)
next_token = None
while True:
kwargs = {
'Name': RUM_APP_MONITOR_NAME,
'TimeRange': {'After': after},
'MaxResults': 100,
}
if next_token:
kwargs['NextToken'] = next_token
resp = rum.get_app_monitor_data(**kwargs)
for raw in resp.get('Events', []):
event = json.loads(raw)
if event.get('event_type') != PAGE_VIEW_EVENT:
continue
details = event.get('event_details', {})
if isinstance(details, str):
details = json.loads(details)
source = normalize_path(details.get('parentPageId'))
target = normalize_path(details.get('pageId'))
if source and target and source != target:
transitions[source][target] += 1
next_token = resp.get('NextToken')
if not next_token:
break
return transitions
def build_rules(destinations):
"""1위 >= threshold → prerender, 나머지 → prefetch."""
total = sum(destinations.values())
if total < MIN_TRANSITIONS:
return None
ranked = destinations.most_common()
prerender, prefetch = [], []
top_path, top_count = ranked[0]
if (top_count / total >= CONFIDENCE_THRESHOLD_PRERENDER
and not any(top_path.startswith(p) for p in EXCLUDE_PATHS)):
prerender = [top_path]
ranked = ranked[1:]
prefetch = [path for path, _ in ranked[:MAX_PREFETCH]]
rules = {}
if prefetch:
rules['prefetch'] = [{'urls': prefetch, 'eagerness': EAGERNESS_PREFETCH}]
if prerender:
rules['prerender'] = [{'urls': prerender, 'eagerness': EAGERNESS_PRERENDER}]
return rules or None
def rules_file_path(page_path):
if page_path == '/':
return f'/{S3_PREFIX}/index.json'
return f'/{S3_PREFIX}{page_path}.json'
def upload_rules(file_path, rules):
s3.put_object(
Bucket=S3_BUCKET, Key=file_path.lstrip('/'),
Body=json.dumps(rules).encode(),
ContentType='application/speculationrules+json',
CacheControl='public, max-age=86400',
)
def update_kvs(entries):
"""ETag 기반 optimistic locking으로 배치 업데이트."""
etag = kvs.describe_key_value_store(KvsARN=KVS_ARN)['ETag']
for i in range(0, len(entries), KVS_BATCH_SIZE):
batch = entries[i:i + KVS_BATCH_SIZE]
resp = kvs.update_keys(
KvsARN=KVS_ARN, IfMatch=etag,
Puts=[{'Key': k, 'Value': v} for k, v in batch],
)
etag = resp['ETag']
def handler(event, context):
transitions = collect_transitions()
kvs_entries = [('__default__', f'/{S3_PREFIX}/default.json')]
pages_updated = 0
for page_path, destinations in transitions.items():
rules = build_rules(destinations)
if rules is None:
continue
file_path = rules_file_path(page_path)
upload_rules(file_path, rules)
kvs_entries.append((page_path, file_path))
pages_updated += 1
update_kvs(kvs_entries)
logger.info(json.dumps({'pages_updated': pages_updated}))
return {'pages_updated': pages_updated}
중요: CloudFront KeyValueStore data plane API(
UpdateKeys)는 SigV4A 서명을 사용합니다. Lambda 배포 패키지에awscrt가 필요합니다:pip install awscrt -t build/
Terraform: Lambda + EventBridge + KVS
# speculation-rules.tf
resource "aws_cloudfront_key_value_store" "speculation" {
name = var.speculation_kvs_name
}
resource "aws_lambda_function" "speculation_generator" {
function_name = "speculation-rules-generator"
role = aws_iam_role.speculation_lambda.arn
runtime = "python3.12"
handler = "index.handler"
filename = data.archive_file.speculation_lambda.output_path
source_code_hash = data.archive_file.speculation_lambda.output_base64sha256
timeout = 300
memory_size = 256
environment {
variables = {
RUM_APP_MONITOR_NAME = aws_rum_app_monitor.site.name
KVS_ARN = aws_cloudfront_key_value_store.speculation.arn
S3_BUCKET = aws_s3_bucket.site.bucket
S3_PREFIX = var.speculation_s3_prefix
MIN_TRANSITIONS = var.speculation_min_transitions
CONFIDENCE_THRESHOLD_PRERENDER = var.speculation_confidence_threshold
MAX_PREFETCH = var.speculation_max_prefetch
EXCLUDE_PATHS = ""
}
}
}
# EventBridge: 매일 UTC 03:00
resource "aws_cloudwatch_event_rule" "daily_speculation" {
name = "daily-speculation-rules-update"
schedule_expression = var.speculation_schedule
}
resource "aws_cloudwatch_event_target" "speculation_lambda" {
rule = aws_cloudwatch_event_rule.daily_speculation.name
arn = aws_lambda_function.speculation_generator.arn
}
resource "aws_lambda_permission" "eventbridge" {
statement_id = "AllowEventBridgeInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.speculation_generator.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.daily_speculation.arn
}
# 기본 규칙 JSON (KVS에 엔트리가 없는 페이지에 제공)
resource "aws_s3_object" "default_speculation_rules" {
bucket = aws_s3_bucket.site.id
key = "${var.speculation_s3_prefix}/default.json"
content = jsonencode({
prefetch = [{
where = { href_matches = "/*" }
eagerness = "conservative"
}]
})
content_type = "application/speculationrules+json"
cache_control = "public, max-age=86400"
}
Step 3: Edge에서 Speculation Rules 제공
CloudFront Function(viewer-response)이 모든 HTML 응답에서 KVS를 읽어 적절한 Speculation-Rules 헤더를 삽입합니다.
CloudFront Function (viewer-response)
import cf from 'cloudfront';
const kvs = cf.kvs();
const DEFAULT_RULES_PATH = '/_speculation/default.json';
function normalizeUri(uri) {
// viewer-request의 '/index.html' rewrite를 되돌려 KVS key와 매칭
if (uri.endsWith('/index.html')) {
uri = uri.slice(0, uri.length - 11);
}
if (uri.length > 1 && uri.endsWith('/')) {
uri = uri.slice(0, uri.length - 1);
}
return uri === '' ? '/' : uri;
}
async function handler(event) {
const request = event.request;
const response = event.response;
// HTML 응답에만 삽입
const contentType = response.headers['content-type']
? response.headers['content-type'].value : '';
if (contentType.indexOf('text/html') === -1) {
return response;
}
// Markdown 응답(verified bot)에는 삽입하지 않음
const wantsMarkdown = request.headers['x-wants-markdown'];
if (wantsMarkdown && wantsMarkdown.value === 'true') {
return response;
}
let rulesPath = DEFAULT_RULES_PATH;
try {
rulesPath = await kvs.get(normalizeUri(request.uri));
} catch (e) {
// Key 미스 또는 KVS 오류 → 기본 규칙
}
// Speculation-Rules 헤더는 quoted URL 문자열이어야 함
response.headers['speculation-rules'] = { value: '"' + rulesPath + '"' };
return response;
}
주요 설계 결정:
- HTML 전용: CSS, JS, 이미지에는 speculation 헤더를 삽입하지 않음 (브라우저가 무시함).
- Markdown 제외: Verified bot이 받는 Markdown 응답에는 speculation rules 불필요.
- URI 정규화: viewer-request 함수가
/posts/hello/를/posts/hello/index.html로 rewrite하므로, viewer-response에서 이를 되돌려 KVS key와 매칭. - Quoted URL: Speculation-Rules 헤더 값은 HTTP structured field 스펙에 따라 quoted string이어야 함.
CloudFront Distribution 연결
cloudfront.tf의 default cache behavior에 viewer-response function association 추가:
default_cache_behavior {
# ... 기존 viewer-request association ...
function_association {
event_type = "viewer-response"
function_arn = aws_cloudfront_function.speculation_rules.arn
}
}
기본 규칙 전략
RUM 데이터가 없는 페이지는 보수적인 기본값을 받습니다:
{
"prefetch": [{
"where": { "href_matches": "/*" },
"eagerness": "conservative"
}]
}
eagerness: "conservative"는 pointer-down(mousedown/touchstart)에서만 prefetch를 트리거합니다. 이는 클릭 직전이므로:
- 낭비되는 fetch가 거의 없음 (사용자가 이미 클릭에 커밋)
- 여전히 50-100ms의 네트워크 지연을 절약
- 모든 페이지에 안전하며, 잘못된 prerender 위험 없음
RUM 데이터가 축적되면서 페이지는 점진적으로 데이터 기반 규칙으로 전환됩니다.
비용 분석
| 구성 요소 | 비용 추정 (일일 10만 페이지뷰 기준) |
|---|---|
| CloudWatch RUM | ~$10/월 (10만 세션 × $0.0001) |
| Lambda (일일 작업) | ~$0.01/월 |
| EventBridge | 프리 티어 |
| CloudFront KVS | ~$0.60/월 |
| S3 (speculation JSONs) | ~$0.01/월 |
| CloudFront Functions | CloudFront 과금에 포함 |
| Cognito Identity Pool | 프리 티어 (50K MAU) |
| 합계 | ~$10.62/월 |
이미 Core Web Vitals 모니터링용으로 RUM을 운용 중이라면, 추가 비용은 ~$0.62/월.
제한 사항
브라우저 지원
Speculation Rules API는 2026년 6월 기준 Chromium 전용 (Chrome 109+, Edge 109+). Firefox와 Safari는 헤더를 무시합니다 — 에러 없고 성능 저하 없음. Chromium은 전 세계 브라우저의 약 65-70%.
Prerender 안전성
Prerender된 페이지는 JavaScript를 실행합니다. 부작용을 일으키는 페이지는 Lambda의 EXCLUDE_PATHS 환경 변수로 제외합니다. 애플리케이션 코드에서도 방어:
if (document.prerendering) {
document.addEventListener('prerenderingchange', () => {
trackPageView(); // 사용자가 실제로 이 페이지에 도착할 때만 실행
});
} else {
trackPageView();
}
Cold Start와 awscrt 의존성
배포 후 첫째 날에는 KVS에 __default__만 있어 모든 페이지가 기본 규칙을 받습니다. Lambda 빌드 시 awscrt (SigV4A 서명용)를 반드시 포함해야 합니다:
cd infra/lambda/speculation-rules-generator
pip install -r requirements.txt -t build/
cp index.py build/
cd build && zip -r ../speculation-rules-generator.zip .
효과 측정
배포 후 검증:
# HTML 응답에서 Speculation-Rules 헤더 확인
curl -sI https://your-site.com/posts/example/ | grep -i speculation
# 규칙 JSON 파일 확인 (Lambda 실행 후)
curl -s https://your-site.com/_speculation/posts/example.json | jq .
# 정적 리소스에 헤더 없는지 확인
curl -sI https://your-site.com/css/style.css | grep -i speculation
Chrome DevTools Network 패널에서 speculative load를 “Speculative Load” 배지로 확인할 수 있습니다. CloudWatch RUM에서 LCP/FCP를 before/after 비교합니다.
다음 단계
이 패턴을 확장할 수 있습니다:
- 실시간 업데이트: 일일 배치를 Kinesis 스트림으로 대체하여 거의 실시간 규칙 업데이트
- ML 기반 예측: SageMaker로 세션 컨텍스트에서 다음 페이지 확률을 예측하는 모델
- 세그먼트별 규칙: 모바일 vs 데스크톱 (모바일은 prerender보다 prefetch 선호)
전체 소스 코드: github.com/lakil00/buildonaws —
infra/rum.tf,infra/speculation-rules.tf,infra/cf-function/speculation-rules.js,infra/lambda/speculation-rules-generator/index.py
For AI Coding Assistants and Agents
AI coding assistant가 AWS 서비스로 데이터 기반 Speculation Rules를 구현할 때 참고할 내용입니다.
Key Technical Facts
- Speculation Rules API는 Chrome 109+, Edge 109+에서 지원. Firefox와 Safari는 에러 없이 무시.
Speculation-RulesHTTP 응답 헤더 값은 JSON 파일의 quoted URL 경로:Speculation-Rules: "/_speculation/page.json".- CloudFront Functions (viewer-response,
cloudfront-js-2.0)는import cf from 'cloudfront'; const kvs = cf.kvs();로 KVS 읽기 가능. - KVS 바인딩은 Terraform
aws_cloudfront_function의key_value_store_associations로 설정. - CloudFront KeyValueStore data plane API는 SigV4A 서명 — Lambda에
awscrt패키지 필수. - CloudWatch RUM
page_view_event는parentPageId(이전 페이지)와pageId(현재 페이지)를 포함. eagerness값:immediate(즉시),eager(곧),moderate(hover),conservative(pointer-down).- Chrome은 동시 prerender를 페이지당 2개로 제한.
Example Prompts
- “CloudFront Function(viewer-response)을 만들어서 KVS에서 Speculation-Rules 헤더를 삽입하게 해줘”
- “CloudWatch RUM page_view_event를 쿼리해서 페이지별 speculation rules를 생성하는 Lambda 작성해줘”
- “Terraform으로 CloudWatch RUM + Cognito Identity Pool을 구성해줘 (unauthenticated 브라우저 접근용)”
- “CloudFront KeyValueStore + Lambda + EventBridge 일일 스케줄 Terraform 작성해줘”
- “Lambda IAM role을 RUM 읽기, KVS 쓰기, S3 prefix 쓰기 최소 권한으로 구성해줘”
- “prerender된 페이지에서 analytics 이중 카운트를 방지하는 document.prerendering 체크 추가해줘”
Note
2026년 6월 기준, Speculation-Rules 헤더 값은 quoted string이어야 합니다 (HTTP structured field). Chrome DevTools Network 패널에서 “Speculative Load” 배지로 디버깅 가능. S3에서 제공하는 규칙 JSON 파일의 Content-Type은 application/speculationrules+json을 사용합니다.