/click-path-audit — 동작 흐름 감사(Audit)
정적 코드 읽기로는 놓치는 버그를 찾습니다: 상태 상호 작용 부작용, 순차 호출 간 경합 조건, 서로를 조용히 취소하는 핸들러.
이 스킬이 해결하는 문제
전통적 디버깅은 다음을 확인합니다:
- 함수가 존재하는가? (배선 누락)
- 충돌하는가? (런타임 오류)
- 올바른 타입을 반환하는가? (데이터 흐름)
하지만 다음은 확인하지 않습니다:
- 최종 UI 상태가 버튼 라벨이 약속하는 것과 일치하는가?
- 함수 B가 함수 A가 방금 한 일을 조용히 취소하는가?
- 공유 상태(Zustand/Redux/context)에 의도된 동작을 취소하는 부작용이 있는가?
실제 예시: "새 이메일" 버튼이 setComposeMode(true)를 호출한 후 selectThread(null)을 호출했습니다. 각 함수는 개별적으로 정상 작동했습니다. 하지만 selectThread에 composeMode: false를 리셋하는 부작용이 있었습니다. 버튼은 아무것도 하지 않았습니다. 체계적 디버깅으로 54개의 버그를 찾았지만, 이 버그는 놓쳤습니다.
작동 방식
대상 영역의 모든 대화형 터치포인트에 대해:
1. 핸들러 식별 (onClick, onSubmit, onChange 등)
2. 핸들러의 모든 함수 호출을 순서대로 추적
3. 각 함수 호출에 대해:
a. 어떤 상태를 읽는가?
b. 어떤 상태를 쓰는가?
c. 공유 상태에 부작용이 있는가?
d. 상태를 리셋하거나 지우는가?
4. 확인: 이후 호출이 이전 호출의 상태 변경을 취소하는가?
5. 확인: 최종 상태가 사용자가 버튼 라벨에서 기대하는 것인가?
6. 확인: 경합 조건(잘못된 순서로 해결되는 비동기 호출)이 있는가?
실행 단계
1단계: 상태 저장소 매핑
터치포인트를 감사하기 전에, 모든 상태 저장소 액션의 부작용 맵을 구축합니다:
범위 내 각 Zustand 저장소 / React 컨텍스트에 대해:
각 액션/세터에 대해:
- 어떤 필드를 설정하는가?
- 부작용으로 다른 필드를 리셋하는가?
- 문서화: actionName → {sets: [...], resets: [...]}
이것이 중요한 참조입니다. "새 이메일" 버그는 selectThread가 composeMode를 리셋한다는 것을 알지 못하면 보이지 않았습니다.
출력 형식:
STORE: emailStore
setComposeMode(bool) → sets: {composeMode}
selectThread(thread|null) → sets: {selectedThread, selectedThreadId, messages, drafts, selectedDraft, summary} RESETS: {composeMode: false, composeData: null, redraftOpen: false}
setDraftGenerating(bool) → sets: {draftGenerating}
...
위험한 리셋(자기 소유가 아닌 상태를 지우는 액션):
selectThread → composeMode 리셋 (setComposeMode 소유)
reset → 모든 것 리셋
2단계: 각 터치포인트 감사
대상 영역의 각 버튼/토글/폼 제출에 대해:
TOUCHPOINT: [버튼 라벨] in [컴포넌트:줄번호]
HANDLER: onClick → {
call 1: functionA() → sets {X: true}
call 2: functionB() → sets {Y: null} RESETS {X: false} ← 충돌
}
EXPECTED: 사용자가 [버튼 라벨이 약속하는 설명]을 봄
ACTUAL: functionB가 리셋했기 때문에 X는 false
VERDICT: BUG — [설명]
다음 각 버그 패턴을 확인하세요:
패턴 1: 순차 취소
handler() {
setState_A(true) // sets X = true
setState_B(null) // 부작용: X를 false로 리셋
}
// 결과: X는 false. 첫 번째 호출은 무의미했습니다.
패턴 2: 비동기 경합
handler() {
fetchA().then(() => setState({ loading: false }))
fetchB().then(() => setState({ loading: true }))
}
// 결과: 최종 로딩 상태는 어느 것이 먼저 해결되는지에 따라 달라짐
패턴 3: 오래된 클로저
const [count, setCount] = useState(0)
const handler = useCallback(() => {
setCount(count + 1) // 오래된 count 캡처
setCount(count + 1) // 동일한 오래된 count — 2가 아닌 1만 증가
}, [count])
패턴 4: 상태 전환 누락
// 버튼이 "저장"이라고 표시되지만 핸들러는 검증만 하고 실제로 저장하지 않음
// 버튼이 "삭제"라고 표시되지만 핸들러는 플래그 설정만 하고 API를 호출하지 않음
// 버튼이 "보내기"라고 표시되지만 API 엔드포인트가 제거/손상됨
패턴 5: 조건부 사망 경로
handler() {
if (someState) { // someState는 이 시점에서 항상 false
doTheActualThing() // 도달 불가
}
}
패턴 6: useEffect 간섭
// 버튼이 stateX = true 설정
// useEffect가 stateX를 감시하고 false로 리셋
// 사용자는 아무 일도 일어나지 않음을 봄
3단계: 보고
발견된 각 버그에 대해:
CLICK-PATH-NNN: [심각도: CRITICAL/HIGH/MEDIUM/LOW]
Touchpoint: [버튼 라벨] in [파일:줄번호]
Pattern: [Sequential Undo / Async Race / Stale Closure / Missing Transition / Dead Path / useEffect Interference]
Handler: [함수 이름 또는 인라인]
Trace:
1. [호출] → sets {필드: 값}
2. [호출] → RESETS {필드: 값} ← 충돌
Expected: [사용자가 기대하는 것]
Actual: [실제로 일어나는 것]
Fix: [구체적인 수정 사항]
범위 제어
이 감사는 비용이 많이 듭니다. 적절히 범위를 지정하세요:
- 전체 앱 감사: 출시 시 또는 주요 리팩터링 후에 사용. 페이지별로 병렬 에이전트를 실행하세요.
- 단일 페이지 감사: 새 페이지를 구축한 후 또는 사용자가 버튼 오작동을 보고한 후에 사용.
- 저장소 중심 감사: Zustand 저장소를 수정한 후에 사용 — 변경된 액션의 모든 소비자를 감사.
전체 앱에 권장되는 에이전트 분할:
Agent 1: 모든 상태 저장소 매핑 (1단계) — 다른 모든 에이전트의 공유 컨텍스트
Agent 2: 대시보드 (Tasks, Notes, Journal, Ideas)
Agent 3: 채팅 (DanteChatColumn, JustChatPage)
Agent 4: 이메일 (ThreadList, DraftArea, EmailsPage)
Agent 5: 프로젝트 (ProjectsPage, ProjectOverviewTab, NewProjectWizard)
Agent 6: CRM (모든 서브 탭)
Agent 7: 프로필, 설정, Vault, 알림
Agent 8: 관리 제품군 (모든 페이지)
Agent 1이 반드시 먼저 완료되어야 합니다. 그 출력은 다른 모든 에이전트의 입력입니다.
사용 시기
- 체계적 디버깅 후 "버그 없음"이지만 사용자가 UI 오작동을 보고하는 경우
- Zustand 저장소 액션을 수정한 후 (모든 호출자 확인)
- 공유 상태를 건드리는 리팩터링 후
- 출시 전, 중요한 사용자 흐름에서
- 버튼이 "아무것도 하지 않을" 때 — 이것이 그 도구입니다
사용하지 말아야 할 시기
- API 수준 버그(잘못된 응답 형태, 엔드포인트 누락) — 체계적 디버깅 사용
- 스타일/레이아웃 문제 — 시각적 검사
- 성능 문제 — 프로파일링 도구
다른 스킬과의 통합
/superpowers:systematic-debugging이후에 실행 (다른 54가지 버그 유형을 찾음)/superpowers:verification-before-completion이전에 실행 (수정 사항이 작동하는지 확인)/superpowers:test-driven-development에 피드백 제공 — 여기서 발견된 모든 버그는 테스트를 받아야 함
예시: 이 스킬을 만든 버그
ThreadList.tsx "새 이메일" 버튼:
onClick={() => {
useEmailStore.getState().setComposeMode(true) // ✓ composeMode = true 설정
useEmailStore.getState().selectThread(null) // ✗ composeMode = false로 리셋
}}
저장소 정의:
selectThread: (thread) => set({
selectedThread: thread,
selectedThreadId: thread?.id ?? null,
messages: [],
drafts: [],
selectedDraft: null,
summary: null,
composeMode: false, // ← 이 조용한 리셋이 버튼을 죽였습니다
composeData: null,
redraftOpen: false,
})
체계적 디버깅이 놓친 이유:
- 버튼에 onClick 핸들러가 있습니다 (죽지 않음)
- 두 함수 모두 존재합니다 (배선 누락 없음)
- 어느 함수도 충돌하지 않습니다 (런타임 오류 없음)
- 데이터 타입이 올바릅니다 (타입 불일치 없음)
클릭 경로 감사가 잡은 이유:
- 1단계에서
selectThread가composeMode를 리셋한다는 것을 매핑 - 2단계에서 핸들러 추적: 첫 번째 호출이 true 설정, 두 번째 호출이 false 리셋
- 판정: 순차 취소 — 최종 상태가 버튼 의도와 모순됨