2026 OpenClaw 프론트 실전:
원격 Mac에서 SW 생명주기·캐시 키 델타를 파싱하고 PR 요약으로 멱등 회수
대상: sw.js·프리캐시 manifest·라우팅 테이블을 PR마다 다루는 팀. Linux 전용 CI만으로는 WebKit 전이 타이밍이 흐려져 waiting worker와 캐시 키 표류가 머지 후에야 드러납니다. 원격 Mac에서 OpenClaw가 navigator.serviceWorker와 caches.keys()를 동일 Runbook으로 읽고, .openclaw/reports/sw_cache_delta.json과 pr_sw_cache_summary.md를 남긴 뒤 게이트웨이에 멱등 POST까지 한 번에 묶습니다. 연관: Safari·Chromium SW 출시 체크리스트, Netlify Deploy Hook 스모크 체인, 빌드 메트릭→PR 요약, 영문 동일 슬러그.
01 왜 Service Worker를 Mac에서 파싱해야 하나
실제 WebKit은 Apple 실기에서만 리뷰 스택과 타이밍이 맞습니다. 원격 Mac은 상시 가동으로 OpenClaw가 install·waiting·activate 경계를 폴링하기에 적고, 산출물 경로를 PR마다 동일하게 고정할 수 있습니다.
각 패스마다 .openclaw/reports/ 아래에 작은 JSON을 남기면 CI·채팅·GitHub가 같은 사실 집합을 공유합니다. 스크린샷만으로는 재현 번호가 붙지 않습니다.
02 이 런북이 줄이는 통점
- 숨은 waiting 워커: 리뷰어 화면의 active만 보고 머지하면, 고객은 새로고침 전까지 이전 캐시에 머뭅니다.
- 의미 없는 키 churn: 배포마다
caches.keys()가 바뀌지만 어떤 접두사를 지워도 안전한지 기록이 없습니다. - 채팅만 디버깅: 대화 로그는 증발합니다.
GIT_SHA에 묶인 NDJSON과 아티팩트 경로가 필요합니다.
03 어디에 자동화를 쓸지 표로 고르기
| 접근 | 강점 | 약점 | 언제 쓸지 |
|---|---|---|---|
| 수동 Web Inspector | 브레이크포인트 깊이 | PR 간 재현 불가 | 탐색 전용 |
| Playwright 프로브 페이지 | 생명주기 읽기 스크립트화 | 대기 정책이 필요 | OpenClaw 기본값 |
| 고객 RUM 비콘 | 실사용 커버 | 해상도·프라이버시 한계 | 머지 후 모니터링 |
sw.js나 프리캐시 목록이 바뀔 때마다 프로브 페이지를 기본으로 두고, skipWaiting·clients.claim 정책은 체크리스트 글과 교차 검증하세요.
04 재현 가능한 런북(소유자 5단계)
- 러너 예약: SSH 후
cd ~/openclaw-runs/$PR_NUMBER, 미리보기 호스트를BASE_URL로, 커밋을GIT_SHA로 고정합니다. - 브라우저 1회 설치:
npx playwright install webkit으로 이후 프로브가 같은 WebKit 묶음을 재사용합니다. - 이중 워밍:
curl "$BASE_URL"→ 8초 sleep → 다시curl후 Playwright를 띄워install·activate이벤트가 안정적으로 잡히게 합니다. - 델타 기록: main의
baseline/sw_cache_baseline.json과 비교해.openclaw/reports/sw_cache_delta.json에 정렬된addedKeys·removedKeys를 씁니다. - 요약 전송:
pr_sw_cache_summary.md를 렌더하고Idempotency-Key: $GIT_SHA:sw-cache:$PR_NUMBER로 POST해 훅 플래킹 시 중복 코멘트를 막습니다.
05 붙여 넣을 스크립트·셸 프로브 자리
아래를 scripts/openclaw/sw-cache-probe.mjs로 저장하고 OpenClaw 작업 파일에서 호출합니다. 셸에서는 OPENCLAW_RUN_ID를 export한 뒤 stdout을 tee -a .openclaw/reports/sw_probe.ndjson에 붙여 실패해도 흔적을 남깁니다.
// scripts/openclaw/sw-cache-probe.mjs (플레이스홀더)
import { webkit } from 'playwright';
const base = process.env.BASE_URL;
const browser = await webkit.launch();
const page = await browser.newPage();
await page.goto(base, { waitUntil: 'networkidle' });
const snapshot = await page.evaluate(async () => {
const regs = await navigator.serviceWorker.getRegistrations();
const keys = await caches.keys();
return {
registrations: regs.map(r => ({
scope: r.scope,
activeState: r.active?.state,
waitingState: r.waiting?.state,
installingState: r.installing?.state
})),
cacheKeys: keys.sort()
};
});
console.log(JSON.stringify(snapshot));
await browser.close();
셸 프로브 자리(예시): for i in 1 2 3; do curl -fsS "$BASE_URL" && break || sleep $((i*2)); done — 읽기 전용 GET에 한해 502·503·TLS 리셋 시 지수 간격으로 최대 3회까지 반복합니다.
06 NDJSON 로그 필드·실패 재시도·운영 메모
시도마다 한 줄의 NDJSON을 씁니다. Netlify 훅 러너와 동일 키를 쓰면 대시보드가 균일해집니다.
- ▸
ts,level,openclaw_run_id,git_sha,pr_number,phase(warmup|probe|diff|callback). - ▸
attempt,duration_ms,browser(webkit|chromium),cache_key_count,waiting_worker_present(bool). - ▸
error_class(timeout|quota|assert|http),last_http_status, Playwright 산출물trace_path.
재시도 정책: 읽기 전용 프로브는 502·503·429 또는 TLS 리셋에서 시작 2초·지수 백오프·지터 20%·최대 5회. GitHub 코멘트 POST는 첫 요청이 서버에 도달했는지 불명확할 때만 같은 Idempotency-Key로 재시도하고, 4xx(429 제외)는 계약 버그로 간주해 중단합니다.
07 구조화 데이터(JSON-LD)와 내링크 계획
본문 <head>에는 BlogPosting, BreadcrumbList, HowTo, FAQPage를 @graph로 묶어 리치 결과 후보를 맞춥니다. 본문에는 Microdata BlogPosting의 headline·날짜 메타를 유지합니다.
- 인링크: 블로그 인덱스와 프리캐시 목록이 바뀌는 Deploy Hook 글에서 이 슬러그를 인용합니다.
- 로케일: 슬러그는 언어별 동일 파일명으로 두고, 번역 HTML이 있을 때만
hreflang를 게시합니다. - 앵커: 목차
id(예:#sw-faq)는 감사·딥링크가 깨지지 않게 고정합니다.
08 FAQ: 저장소 할당량과 opaque 응답
| 증상 | 원인 | 완화 |
|---|---|---|
QuotaExceededError at cache.put |
오리진 저장 예산 소진 또는 큰 opaque 다량. | quotaEstimateMb 로깅, 프리캐시 버전 캡, activate 전 RELEASE_ID 접두사 정리. |
| 감사에서 CDN 글꼴이 0바이트 | opaque no-cors 응답은 크기가 가려짐. | opaqueEntryCountHint를 별도 집계·동일 출처 번들과 바이트 비교 금지. |
| 요약 POST 반복 실패 | 토큰 스코프·2차 레이트 리밋. | 지터 백오프, OPENCLAW_GATEWAY_TOKEN 순환, PR 본문에는 아티팩트 URL만. |
예산이 깨지면 동일 출처 또는 CORS 허용 자산으로 opaque 비중을 줄이는 편이 안전합니다.