레이더 씬까지 5단계
랜딩 페이지 비주얼을 다섯 번 갈아엎었다.
처음엔 Canvas 2D 네트워크 애니메이션이었다. 노드 7개, 엣지 40개 미만이라 WebGL은 과하다고 판단했다. 의존성 최소화 원칙을 지키려는 선택이었다.
하지만 첫 결과물은 “평범하다”는 한마디로 기각됐다. Designer 서브에이전트가 레퍼런스 사이트의 핵심 인터랙션을 “금융 제품에 기믹”이라며 자의 삭제한 게 원인이었다. 지시사항을 자기가 판단해서 걸러버린 것이다.
재위임할 때는 협상 불가 항목을 하나하나 핀으로 고정했다. 풀뷰포트 글로우 스피어, 픽셀 전환, 패럴렉스, 다크↔라이트 대비. 7섹션 시네마틱 스펙이 나왔고 카드 그리드는 0개로 설계했다. 같은 카드를 섹션마다 반복하면 제네릭 AI 생성물 느낌이 나기 때문이다.
그렇게 R3F 기반 글로우 스피어까지 갔는데, 보라색 파티클이 시각적으로 마음에 들지 않았다. 항공 도시 사진을 보여주며 “이게 레이더처럼 움직이면 좋겠다”고 방향을 틀었다.
최종 방향은 절차적 3D 도시 씬이다. 이미지 텍스처가 아니라 R3F 인스턴스로 로우폴리 빌딩 블록을 세운다. 어두운 항공 도시 베이스 위에 동심원 레이더 링, 회전 스캔 라인, 펄스 파동을 얹는다. R3F v9, React 19, postprocessing v3 Bloom 조합으로 확정했다.
설계 → 계획 → 구현을 분리 위임했다. 한 번에 코드로 덮어쓰지 않고 Designer가 시각을 설계하고 Planner가 파일 단위 계획을 세우고 Worker가 구현한다. 각 단계 산출물을 따로 검증하는 구조다.
코드베이스를 직접 읽으면서 발견한 것도 있었다. Pretendard woff2는 weight 800, 900을 제공하지 않는다. 변수 축을 드롭한 woff2만 self-host해서 400부터 700까지만 가능하다. 800 이상을 요청하면 faux-bold가 된다. 그래서 폰트 무게가 아니라 사이즈와 tracking 조임으로 임팩트를 만들어야 한다.
tokens.css가 진실
DESIGN.md를 읽고 디자인 결정을 하면 안 된다.
배포 중인 tokens.css가 실제 소스 오브 트루스다.
prose 문서는 이미 드리프트가 여러 군데 나 있었다.
색 토큰 값, 다크 모드 배경, glassmorphism, mesh 그라디언트 전부 실제 CSS와 맞지 않았다.
디자인 토큰 v2는 3축을 동시에 재설계했다. 컬러, 타이포그래피, radius 각각을 별도 서브에이전트에 맡겼고 전부 데이터 기반으로 결정했다. grep 카운트와 대비비 계산 결과로 설계했다.
컬러 — AA가 팔레트보다 우선
기존 팔레트는 24 raw 색상이었다. 이걸 13 시맨틱 토큰으로 줄였다.
핵심은 AA 대비 기준을 “닻”으로 쓴 것이다. AA를 통과하는 색상만 그대로 쓰고 실패한 색상은 같은 hue에서 lightness만 튜닝했다. red, green, orange는 지각 휘도가 높아 기본 lightness에서 AA가 불가능했다. 그래서 lightness를 낮춰서 맞췄다.
glass 클래스 84회 사용을 3문맥으로 분류해서 제거했다.
콘텐츠 카드는 bg-card border border-border/50, 팝오버는 불투명 배경, 스티키 헤더만 backdrop-blur를 허용했다.
별도 glass 토큰 없이 기존 컬러에 opacity modifier만 붙이면 된다.
4색 무지개 mesh 그라디언트도 단일 primary 색상으로 통일했다. 다중색상 그라디언트는 전형적인 “AI 티” 신호다.
타이포그래피 — line-height가 진짜 문제
이번 재설계에서 가장 큰 통찰이다.
한국어 접근성 격차는 폰트 크기가 아니라 line-height에서 온다.
Tailwind 기본값을 확인해보면 text-sm의 LH는 1.429, text-xs는 1.333이다.
둘 다 WCAG 한국어 바디 텍스트 기준인 1.5에 미달한다.
그래서 폰트 크기는 14px을 유지하고 line-height를 1.5로 고정했다. KRDS 권장값인 17px로 키우면 대시보드 밀도가 떨어진다. 토스, 카카오페이뱅크 같은 한국 핀테크 서비스도 13~14px 바디를 쓴다. 크기가 아니라 행간이 한국어 가독성의 안전바다.
theme.extend.fontSize에서 xs, sm, lg의 LH를 덮어쓰면 기존 400곳 이상의 text-sm 클래스가 코드 변경 없이 즉시 1.5 LH로 교정된다.
radius — 최소 churn 경로
radius는 7종에서 4종으로 줄였다. grep 카운트로 가장 많이 쓰는 표면은 건드리지 않는 경로를 찾았다. 카드 radius 10px은 58회 사용이라 값을 유지했고 Button과 Input을 동일 6px로 통일했다.
Tailwind v3 함정도 하나 발견했다.
theme.extend는 기본 radius 키를 unset할 수 없다.
alias를 명시하지 않으면 rounded-md, rounded-xl이 Tailwind 기본값으로 조용히 폴백된다.
preset 전체 블록을 복사해야 한다.
loop-guard 순환 루프
12.9MB 세션 로그를 분석했다.
bash 호출 392회 중 307회가 서브에이전트 session.jsonl 파일 상태 확인이었다.
wc -l, tail -1, cat을 반복한 것이다.
subagent_wait 도구를 쓰지 않고 파일을 직접 폴링한 안티패턴이었다.
이 폴링이 loop-guard를 건드렸다. 동일 호출 10회 누적 시 abort를 발생시키는데, abort 직후 auto-fallback이 매번 에이전트를 재시작시켰다. abort는 실제로 12번 일어났지만 사용자 눈에는 정상으로 보였다.
문제는 abort 시점에 recentKeys 버퍼를 비우는 것이었다.
재시작 직후 카운트가 0부터 다시 시작되니, 10회 폴링이 다시 누적되고 다시 abort되는 순환이 된다.
stopReason=error 14개를 전수조사했다.
13개는 "This operation was aborted" 메시지, 즉 loop-guard 서명이었다.
나머지 1개만 실제 500 에러였다.
“stopReason=error가 곧 loop-guard”라는 가정은 틀리다.
error 필드를 직접 들여다봐야 구분할 수 있다.
수정은 두 가지다.
넛지 delivery를 nextUserRequest 지연에서 thisStep 즉시 주입으로 바꿨고 recentKeys 초기화를 제거했다.
초기화를 안 하면 재시작 직후에도 임계값 도달 상태가 유지돼서 같은 호출이 막힌다.
근본 해결은 subagent_wait를 쓰는 것이다.
한 번 호출하면 완료 시 자동으로 복귀하는 도구다.
수백 건의 낭비 bash 호출, 컨텍스트 오염, 혼란스러운 경고 burst가 한꺼번에 해소된다.
skill 주입 replace 함정
서브에이전트에 런타임 skill 파라미터를 넘기면 프론트메터 skills:가 완전히 교체된다.
병합이 아니라 replace다.
소스를 확인해보니 ??(nullish coalescing)로 돼 있었다.
params.skills ?? agentConfig.skills ?? [].
런타임 값이 정의돼 있으면 프론트메터 값을 무시한다.
해법은 skill 파라미터를 안 쓰는 것이다.
대신 태스크 프롬프트에 스킬 파일의 절대경로를 적어주고 read로 읽으라고 지시한다.
skill 파라미터가 하는 일도 결국 <available_skills> 블록을 시스템 프롬프트에 넣고 read로 읽으라고 지시하는 것이므로, 메커니즘이 동일하다.
배치 위치만 시스템 프롬프트가 아니라 태스크 프롬프트라는 점이 다르다.
GPT-5.6 contextWindow 272K의 진짜 원인
GPT-5.6 모델의 직접 API 컨텍스트 윈도우는 1,050,000 토큰이다. OpenAI 공식 문서에서 확인한 진짜 용량이다.
그런데 Codex 백엔드 라우트로 연결하면 같은 슬러그를 272K로 캡한다. OpenAI 서버 단에서 272K 초과 요청을 400 에러로 거부한다. pi 설정은 1M로 정상이다. 해결은 모델을 바꾸는 게 아니라 라우트를 전환하는 것이다. 직접 API나 OpenRouter로 연결하면 1.05M이 열린다.
“설정은 N인데 더 일찍 압축” 증상이 다른 모델에서도 나오면, 임계값이 아니라 백엔드 거부 경로를 의심해야 한다.