cafe24-validate
Cafe24 스킨 배포 후, 이번에 작업한 path는 에러/렌더링을 점검하고 작업하지 않은 path는 라이브와 스크린샷을 비교해 사이드이펙트(회귀)를 잡는 검증 스킬. 특히 Cafe24 옵티마이저의 @js/@css 번들 순서 역전으로 JS가 조용히 실패해 스타일이 안 먹는 케이스 — 콘솔 에러도 안 나고 DOM 구조도 그대로여서 놓치기 쉬운 사고 — 를 P
SKILL.mdskills/cafe24-validate/SKILL.md
GitHubcafe24-validate — 배포 후 사이드이펙트(회귀) 검증
이 스킬이 잡으려는 사고
작업한 페이지 말고 손 안 댄 다른 페이지가 조용히 깨지는 것을 잡는다. 대표 사고 패턴:
공용모듈의 스크립트 import를 교체했는데, Cafe24 옵티마이저가
@js/@css를 문서 순서와 무관하게 번들링(references/optimizer.md/ 프로젝트 CLAUDE.md 참고)해서 실행 순서가 역전됐다.var CONFIG = window.X || {}같은 즉시 캡처가 빈 값으로 굳어 JS가 throw 없이 조용히 실패하고, 특정 영역의 스타일이 안 먹었다.
이 사고의 핵심 성질 세 가지가 검증 방법을 결정한다:
- 콘솔 에러가 안 난다 (throw 아님, 빈 값 캡처일 뿐) → 콘솔 모니터링으로 못 잡음.
- DOM(HTML) 구조는 그대로다 (클래스·엘리먼트 동일, "적용된 스타일"만 없음) → DOM diff로 못 잡음.
- 그 배포에선 결정론적으로 깨진 상태다 (flaky 랜덤 아님) → 단일 로드 스크린샷 1장으로 충분히 재현됨.
결론: 렌더 결과를 라이브(변경 전 마지막 정상 상태)와 스크린샷으로 비교하는 것만이 이 클래스를 잡는다. 이 스킬의 회귀 파트는 그걸 한다.
0. 인자 검증 — 테스트 스킨 URL 없으면 즉시 실패 (가장 중요)
이 스킬은 세 가지를 인자로 받는다:
test_base_url(필수) — 이번에 배포된 테스트 스킨 base URL.live_base_url(선택 — 없으면 유도) — 회귀 비교의 baseline이 되는 라이브 base URL.worked_paths(선택 — 없으면 질문) — 이번에 의도적으로 건드린 URL path 목록.
규칙:
test_base_url이 없으면 아무 점검도 시작하지 말고 즉시 실패로 종료한다. "테스트 스킨 URL을 인자로 주세요 (예:/cafe24-validate https://몰주소/skin-skin42/ worked=/product/detail.html?product_no=123)"라고 안내하고 끝낸다.- 기억·이전 대화·추측으로 URL을 만들어내지 않는다. Cafe24 테스트 스킨 번호(skin-skin42, skin-mobile30 등)는 스킨 복사/교체 때마다 바뀐다. 잘못된 스킨을 점검하면 "정상인데 깨졌다 / 깨졌는데 정상이다"라는 완전히 틀린 보고가 나간다. URL을 강제로 받는 이유가 정확히 이 사고를 막기 위해서다.
live_base_url유도:test_base_url에서 스킨 경로 세그먼트(/skin-skinNN/,/skin-mobileNN/등)를 제거해 유도한다. 예:https://mall.com/skin-skin42/→https://mall.com/. 유도한 URL을 사용자에게 한 줄로 확인받는다("라이브 baseline을https://mall.com/로 잡습니다, 맞나요?"). 확신이 안 서면 물어본다.worked_paths가 없으면 AskUserQuestion으로 물어본다: "이번 배포에서 의도적으로 바꾼 페이지 path가 어디인가요? (나머지는 라이브와 비교해 사이드이펙트를 봅니다)". 비워두면 6개 타입 전부를 unworked로 간주해 전부 회귀 비교한다.
시작 전 두 URL 모두 유효한지 확인한다:
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" "<url>"
200이 아니거나 예상 밖 도메인으로 리다이렉트되면 재확인받는다.
전제 — 테스트 스킨은 라이브의 미러여야 한다
회귀 비교는 테스트 스킨이 "라이브 + 이번 변경"에 가까운 미러라는 전제 위에서만 유효하다. 같은 몰이라 상품·가격 데이터가 동일하고 손 안 댄 페이지의 스킨 코드도 라이브와 같으므로, unworked 페이지가 라이브와 다르게 그려지면 그게 곧 사이드이펙트다.
테스트 스킨에 이번 변경 외에 안 나간 다른 미배포 변경이 쌓여 있으면 이 전제가 깨져 false positive가 터진다. 그런 상황으로 의심되면 사용자에게 알리고, 회귀 결과를 "라이브 대비 차이"로만 보고하되 원인 단정은 하지 않는다.
1. 준비
- 브라우저는
chrome-devtools-sharedMCP를 쓴다 (navigate_page/evaluate_script/take_screenshot/resize_page/list_console_messages/list_network_requests). 이 서버는 persistent 프로필에 attach하므로 로그인이 세션을 넘어 유지된다 — 주문·마이페이지 검증에 필수. (상위dev/works/CLAUDE.md규칙.) 안 되면playwright-shared를 폴백으로. - 다른 세션이 쓰는 탭을 건드리거나 브라우저를 종료하지 말 것. 내가 연 탭에서만 작업한다.
- 뷰포트는 PC와 모바일 둘 다 돈다. PC는 기본, 모바일은
resize_page로 390×844. 각 페이지 타입을 두 뷰포트에서 각각 검증한다. - 스크린샷은 scratchpad 디렉토리에 저장한다 (리포지토리 오염 방지). test/live, PC/모바일이 구분되게 파일명을 짓는다 (예:
main_pc_test.png,main_pc_live.png).
1.5. 로그인 처리 — 공개 페이지는 그냥 돌고, 로그인 페이지만 세션 확인
이 스킬은 로그인 페이지(4 주문, 6 마이페이지)를 일부러 검증하므로, 예전처럼 "로그인 요구되면 전체 중단"하지 않는다.
- 공개 페이지(1 메인, 2 카테고리, 3 상품상세, 5 로그인폼): 로그인 없이 그대로 검증.
- 로그인 필요 페이지(4 주문진입, 6 마이페이지): 해당 URL로 navigate했을 때 로그인 페이지(
/member/login,/intro/)로 리다이렉트되면 → shared 브라우저가 아직 로그인 안 된 것. 우회·자동 로그인하지 말고, 브라우저를 열어둔 채 사용자에게 안내한다: "주문/마이페이지 검증에는 로그인이 필요합니다. shared 크롬 창에서 직접 로그인해 주세요. 로그인은 persistent 프로필에 유지됩니다. 로그인 후 '계속'이라고 알려주시면 이어서 돕니다." 공개 페이지 검증은 그와 무관하게 먼저 끝내 보고하고, 로그인 필요 페이지만 대기 상태로 둔다.
2. 점검 대상 — 고정 6개 페이지 타입
상품 여러 개를 도는 게 아니라 타입별 대표 1개씩 돈다 (공용 자원 변경의 영향 범위는 페이지 타입 전체이므로). 각 타입의 대표 URL은 test·live 양쪽에서 같은 리소스(같은 cate_no / product_no)를 가리키도록 맞춘다.
| # | 타입 | 대표 URL 선정 | 로그인 |
|---|---|---|---|
| 1 | 메인 | test_base_url 루트 |
불필요 |
| 2 | 카테고리 리스트 | GNB 최상위 카테고리 | 불필요 |
| 3 | 상품상세 | GNB 최상위 카테고리의 첫 번째 상품 | 불필요 |
| 4 | 주문 진입 | 3번 상품을 장바구니→주문서까지 진입 (기능 확인) | 필요 |
| 5 | 로그인 | /member/login.html |
불필요(폼) |
| 6 | 마이페이지 | /myshop/ (또는 몰의 마이페이지 경로) |
필요 |
GNB 최상위 카테고리·첫 상품 수집 — 메인에서 evaluate로 뽑는다 (셀렉터는 스킨마다 다르니 폴백 여러 개):
() => {
// GNB 최상위 카테고리 링크
var cat = document.querySelector('#category a[href*="cate_no="], .xans-layout-category a[href*="cate_no="], nav a[href*="/category/"]');
return cat ? cat.getAttribute('href') : null;
}
그 카테고리 리스트로 navigate 후 첫 상품:
() => {
var p = document.querySelector('a[href*="product_no="]');
return p ? p.getAttribute('href') : null;
}
수집한 링크가 절대경로(라이브 도메인)면 base URL 기준 상대경로로 바꿔 접속한다 — test 점검하러 와서 live로 새면 안 된다. test에서 정한 cate_no·product_no를 live 대표 URL에도 그대로 써서 같은 페이지를 비교한다.
3. 페이지별 점검 방식
각 페이지 타입 × 각 뷰포트(PC/모바일)마다. worked인지 unworked인지에 따라 방식이 갈린다 — 대표 URL의 path(쿼리 포함)가 worked_paths와 정확 일치하면 worked, 아니면 unworked.
공통 (worked/unworked 모두)
(a) 리소스 로드 체크 — 가장 싼 1차 그물. CSS/JS가 실제로 붙었는지. 존재하지 않는 파일의 @js는 에러 없이 번들에서 조용히 빠지므로(기능 통째 실종 → 파일 404 의심), 네트워크에서 스킨 에셋 4xx / 번들 누락을 확인한다:
() => performance.getEntriesByType('resource')
.filter(r => /\.(js|css)/.test(r.name) && (r.responseStatus >= 400 || r.transferSize === 0 && r.decodedBodySize === 0))
.map(r => r.name)
list_network_requests로도 4xx를 교차 확인.
(b) 콘솔/런타임 에러 — list_console_messages(error). 트래킹/서드파티/기존 템플릿 에러는 첫 페이지를 기준선으로 잡고 새로 등장하는 에러에 집중. 프로젝트 knowledge에 기지(旣知) 콘솔 에러 목록이 있으면 대조해 제외한다 — 특정 페이지에만 뜨는 에러는 기준선 방식으로 못 거른다 (예: standard-cafe24 상품상세의 null (reading 'CAFE24')/undefined (reading 'stype') 2종은 의도된 모듈 임베드의 기지 노이즈, knowledge/cafe24-coupon-front-data.md E절). 단, 이 스킬의 주 타깃(조용한 스타일 미적용)은 콘솔 에러를 안 낸다 — 콘솔이 깨끗해도 안심하지 말고 (c)/(d)를 반드시 본다.
worked path — "변경이 의도대로 먹었나"
라이브와 다른 게 정상이므로 회귀 비교하지 않는다. 대신:
- (a)(b) 통과 확인.
- 양성 확인: 이번 변경이 실제로 반영됐는지 스크린샷을 찍어 Read로 눈으로 확인한다. 사용자가 "무엇을 바꿨는지" 알려줬으면 그 지점이 반영됐는지 구체적으로 본다.
unworked path — "사이드이펙트로 안 깨졌나" (회귀 비교, 이 스킬의 핵심)
test와 live를 스크린샷으로 비교한다:
- 애니메이션 정지 후 촬영 — 촬영 직전 CSS 주입으로 흔들림 제거:
() => { var s=document.createElement('style'); s.textContent='*,*::before,*::after{animation:none!important;transition:none!important}'; document.head.appendChild(s); } - test와 live를 **같은 뷰포트·같은 스크롤(전체 페이지)**로 각각
take_screenshot, scratchpad에 저장. - 비교 — 두 이미지를 Read로 열어 나란히 육안 대조. 같은 몰·같은 데이터이므로 정상이면 거의 동일해야 한다. 레이아웃/스타일이 다르게 그려진 영역(영역이 통째로 안 나옴, 정렬·색·크기가 무너짐, 스타일이 안 먹은 듯 밋밋)이 있으면 사이드이펙트 후보다. 롤링배너·시간·재고 같은 알려진 동적 조각의 차이는 무시한다.
- (선택) ImageMagick이 있으면
compare로 차이 하이라이트를 만들어 눈으로 볼 지점을 좁힌다:
하이라이트는 어디를 볼지 안내일 뿐, 최종 판정은 육안으로 한다 (서브픽셀 차이로 AE 값은 과하게 나올 수 있음).compare -metric AE -fuzz 5% test.png live.png diff.png 2>&1 || true
주문 진입(4) — 회귀보다 기능 확인
주문서는 세션·장바구니에 따라 내용이 달라 픽셀 회귀가 노이즈다. worked/unworked 무관하게 기능 스모크로 본다: 3번 상품 → (필수 옵션 있으면 기본 선택) → 장바구니/바로구매 → 주문서 페이지 진입 → 에러 없이 주문 폼이 렌더되는지 확인. 진입 실패·빈 화면·에러면 보고.
4. 사이트 전용 체크
점검 대상이 아래 사이트면 해당 레퍼런스를 읽고 추가로 수행한다:
| 사이트 | 레퍼런스 | 내용 |
|---|---|---|
| 클룹 (cloop) | references/cloop.md |
골라담기(커스텀옵션) 방어 로직 상태 — ready 플래그, 잠금/오버레이, product_submit 래핑, 검증 함수, PC/모바일 템플릿 배포 조합 |
새 자사몰에서 반복 점검이 생기면 같은 형식으로 references/<사이트>.md를 추가한다.
5. 배포 직후 번들 캐시 주의
번들에는 서버 캐시가 있어 배포 직후 이전 내용이 나올 수 있다 (프로젝트 CLAUDE.md 참고). URL의 filename=/t=가 바뀌었어도 내용이 옛것일 수 있다. 회귀 결과가 이상한데 원인이 안 잡히면, 아직 캐시가 안 갈렸을 가능성을 의심하고 시간을 두고 재확인한 뒤 판정한다.
6. 보고 형식
## 검증 결과 — test: <test_base_url> / live: <live_base_url>
| # | 페이지 타입 | worked? | PC | 모바일 |
|---|---|---|---|---|
| 1 | 메인 | unworked | ✅ 라이브와 동일 | ✅ 동일 |
| 2 | 카테고리(GNB최상위) | unworked | ⚠️ 하단 상품리스트 스타일 미적용 | ✅ |
| 3 | 상품상세 | worked | ✅ 변경 반영됨 | ✅ |
| 4 | 주문 진입 | - | ✅ 주문서 진입/렌더 정상 | ✅ |
| 5 | 로그인 | unworked | ✅ | ✅ |
| 6 | 마이페이지 | unworked | ⏭️ 로그인 필요, 대기 | ⏭️ |
- 리소스 로드: (4xx/번들 누락 없음 / 있으면 무엇)
- 콘솔 에러: (신규 없음 / 기존 N건 — 무엇)
- 회귀(스크린샷) 대조: 어떤 타입을 봤고 test vs live 차이 요약
- 사이드이펙트 발견 시: 증상 → test/live 스크린샷 근거 → 추정 원인(옵티마이저 순서 역전 의심 등) 순으로 서술
사이드이펙트를 발견하면 나머지를 계속 돌기 전에 먼저 사용자에게 보고한다 — 공용모듈 문제면 다른 타입도 같은 원인으로 깨졌을 것이라 원인부터 공유하는 게 낫다. 가능하면 어떤 파일/번들이 원인인지까지 좁힌다.
마무리
- scratchpad 외 리포지토리에 생긴 임시 파일이 없는지 확인·정리한다.
- 점검은 읽기 전용이다 — 코드 수정이나 커밋을 하지 않는다. 발견된 문제의 수정은 별도 작업으로 제안만 한다. (근본 방어는 옵티마이저 경계를 넘는 import 순서에 의존하지 않는 코드 수정이며 이 스킬 범위 밖이다.)