이 글은 AI 에이전트(Claude)와 함께 진행한 한 세션의 기록이다. 아래 커밋 해시와 수치는 전부 이 저장소에서 확인할 수 있다. 사람이 쓴 회고가 아니라, 같이 작업한 결과를 사람이 정리한 것이다.
이 사이트에는 검증이 여섯 개 있다. check:contrast가 색 대비를 재고,
check:content가 콘텐츠 불변식을 보고, check:layers가 캐스케이드 순서를 확인하고,
check:motion이 스크롤 애니메이션의 게이트를 검사하고, check:budget이 라우트별
전송량을 재고, check:release가 발행 자격을 묻는다.
한 세션에서 다국어, SEO 구조, 주제 분류, 인터랙션 효과 14종을 넣었다. 여섯 개가 전부 초록인 상태로 세 가지가 깨져 있었다.
1. 공유 카드 이미지가 전 페이지에서 사라졌다
라우트를 app/[lang]/ 아래로 옮긴 뒤(7c554fa), 감사를 하다가 실측했다.
og:image home ✓ chapter ✗ entry ✗ build ✗ art-direction ✗
원인은 두 겹이었다. app/opengraph-image.tsx가 루트에 남아 페이지들과 떨어졌고,
그것보다 중요한 건 이쪽이다 — OG 이미지는 파일 컨벤션이고 Next는 그것을 해당
세그먼트의 openGraph 객체에 주입하는데, 자기 generateMetadata에서 openGraph를
반환하는 페이지는 그 객체를 이미지까지 통째로 덮어쓴다.
홈만 살아 있던 이유는 홈만 metadata를 layout에서 받기 때문이다. 나머지는 전부 자기 제목과 설명을 정의하므로 자기 openGraph를 갖고, 그래서 카드를 잃었다.
에러도 경고도 없었다. 공유되는 모든 링크에 이미지가 없었다. 고친 건 010baa4다.
어느 게이트도 이걸 잡을 수 없었다. check:release는 페이지마다 <title>,
meta description, canonical을 검사한다. og:image는 목록에 없었다. 없는 항목은
검사되지 않는다.
2. 홈 헤드라인이 안 보였다
sweep이라는 효과를 넣었다. accent 색 띠가 display 활자를 통과하는 것으로,
background-clip: text로 만들었다. 배포하고 나서 화면 상단 타이틀이 안 나온다는
말을 들었다.
background-clip: text는 요소 자신의 배경을 자기 글자 모양으로 통과시켜 그리고,
그게 보이려면 color: transparent가 필요하다. 그런데 같은 세션에서 줄마다 다른
박자로 도착하게 하려고 DisplayLines가 각 줄을 span으로 감싸도록 바꿨다.
span은 투명한 색은 물려받고, 배경은 물려받지 못한다 — background는 상속 속성이 아니다. 클립할 배경이 없으니 아무것도 안 보였다.
제거 전에 측정한 값:
h1 color rgba(0,0,0,0) backgroundClip text backgroundImage 있음
spans color rgba(0,0,0,0) backgroundImage none
.accentBlock만 살아남았다. 자기 배경과 색을 갖고 있어서. 그래서 헤드라인 세 줄
중 한 줄만 남은, 부분적으로 보이는 형태였다 — 완전히 사라졌으면 더 빨리
알아챘을 것이다. 제거는 2361a68.
여기서 중요한 건 게이트가 틀리지 않았다는 것이다.
check:motion은 내내 통과했다. 이 게이트가 묻는 질문은 "스크롤 구동 규칙이 하는
일에 맞게 게이트되어 있는가" 다. sweep은 올바르게 게이트돼 있었다.
결과가 보이는지는 묻지 않는다.
check:contrast는 styles/tokens.css의 팔레트를 파싱해 실제 대비를 재고 기록된
수치와 어긋나면 실패한다. 계산된 텍스트 색은 보지 않는다. 투명한 헤드라인은
검사 범위 밖이다.
둘 다 자기가 약속한 일을 정확히 했다.
3. 그래프의 막대를 누르면 404였다
TRACE의 WebGL 씬은 막대 하나가 기록 하나고, 누르면 그 글로 이동한다.
lib/field.ts가 주소를 /{chapter}/{slug}로 만들고 있었다.
라우트가 /[lang] 아래로 옮겨간 뒤 그 주소는 존재하지 않는다.
/think/crm-sidebar-vanished 404
/ko/think/crm-sidebar-vanished 200
씬은 정상 렌더됐고 데이터도 맞았다. 깨진 것은 방문자가 그 안에서 하는 단
하나의 동작이었다. 페이지에 틀린 게 없었고 그 다음에 일어나는 일만 틀렸다.
그래서 i18n 작업과 게이트가 전부 지나쳤다. 고친 건 6ae09ad이고, 그때
toField의 locale을 기본값 있는 옵션이 아니라 필수 파라미터로 바꿨다.
다음 호출자가 빠뜨리고 그럴듯한 오답을 얻을 수 없게.
게이트 자신이 낡은 답을 하고 있었다
라우트 이동은 게이트 세 개를 조용히 깨뜨렸고, 그중 하나는 적극적으로 틀린 답을 하고 있었다.
| 게이트 | 무엇이 깨졌나 |
|---|---|
check:layers · check:motion | app/index.html을 읽는데 그 파일이 없어짐 |
check:release (feed) | 루트의 feed.xml 하나만 찾음 — 로케일별로 둘인데 |
check:release (이미지) | 앱 루트만 읽고 no opengraph-image in the build라고 보고 — 모든 페이지에 있는데 |
마지막 줄이 이 글의 요지에 가장 가깝다. 게이트가 초록이 아니라 경고를 내고 있었고, 그 경고가 사실이 아니었다. 잘못된 경고는 없는 경고보다 나쁘다. 다음번에 같은 자리에서 진짜 경고가 나와도 무시하게 되기 때문이다.
셋 다 손으로 적힌 경로를 발견 방식으로 바꿔서 고쳤다 — check:budget이 라우트를
빌드 산출물에서 발견하는 것과 같은 이유로. 손으로 적은 경로는 결국 낡는다.
반대 방향
같은 세션에서 게이트가 나를 세 번 잡았다.
check:motion이 거부했다. 타이핑 효과를 만들면서 .lineIn의 스크롤 타임라인을
무효화하려고 animation-timeline: auto를 썼다. 게이트가 막았다 — 콘텐츠를 가리는
규칙에서 animation-timeline이 @supports 밖에 있다고.
거부가 맞았다. 규칙 바깥에서는 그 예외가 안 보인다. 그리고 그 선언은 애초에
불필요했다 — animation 단축 속성이 이미 animation-timeline을 리셋한다.
스펙을 읽고 넘어가지 않고 브라우저에서 측정해 확인했다. 제거하고 게이트 통과,
동작 동일.
React Compiler가 두 번 잡았다. /build 페이지의 행 배분과 DisplayLines의
누적 오프셋에서, 렌더 중 캡처된 변수를 재할당했다. 두 번 다 순수 함수로 다시 썼다.
그리고 내가 게이트를 무시했다
759e01b는 린트 에러를 안은 채로 올라갔다. pnpm verify가 이미 실패했는데
커밋했다. 이유는 단순하다 — 그 작업을 실행한 명령이 verify 출력을 몇 개 패턴으로
grep한 다음 종료 코드와 무관하게 git commit을 실행했다. 일치하지 않는 에러
줄이 그냥 스크롤로 지나갔다.
d4cf6fe에서 고쳤고, 그 뒤로는 verify의 exit code를 확인했다.
게이트가 실패했는데 통과한 것처럼 진행하는 것이, 게이트가 없는 것보다 나쁘다. 없으면 손으로 확인하는데, 있으면 확인했다고 믿는다.
남는 것
세 버그가 다르게 발견됐다.
- 카드 이미지 — 감사하려고 페이지 타입별 매트릭스를 만들어 재다가
- 헤드라인 — 사이트를 쓰는 사람이 보고
- 막대 클릭 — 사이트를 쓰는 사람이 눌러보고
셋 중 어느 것도 게이트가 잡지 않았고, 셋 다 게이트가 잡을 수 있는 종류가
아니었다. 게이트는 자기가 묻는 질문에만 답한다. check:motion은 애니메이션이
게이트됐는지 묻고 보이는지 묻지 않는다. check:release는 목록에 있는 태그를
검사하고 목록에 없는 태그는 검사하지 않는다.
그래서 이 저장소에 이미 있는 THINK 항목 실패하지 않는 검증은 검증이 아니다의 원칙에 한 줄을 덧붙이게 됐다. 그 글은 가드를 만들 때마다 일부러 깨뜨려 본다고 적어 뒀다. 이번에 배운 것은 그 다음이다 —
가드가 통과했다는 것은 가드가 묻는 질문의 답일 뿐이고, 그 질문이 무엇인지는 가드 밖에서 관리해야 한다.
이번 세션에 추가한 게이트 검사들은 그 기준으로 골랐다. hreflang alternates가
없으면 blocker(추가한 즉시 /build와 /art-direction이 걸렸다). 스냅샷에 이메일
주소가 있으면 blocker. 주제에 글이 0편이면 blocker. localhost URL은 blocker이고,
noindex가 붙어 있다는 증명이 있을 때만 예외 — 페이지 이름을 믿지 않는다.
넷 다 부정 시험으로 실제로 걸리는지 확인했다. 통과만 확인한 가드는 여전히 아무것도 보장하지 않는다.