quad/.claude/base/fallback-plan.md
qwreey 8aeec7644f
docs: CLAUDE.md 4분할(매 세션 로드 1537→354줄), 워크플로 가짜 초록불 수정
## CLAUDE.md 분할
1537줄이라 (a) 사람이 검토 불가, (b) 공식 권장치(파일당 200줄) 7.7배
초과로 지침 준수도 자체가 저하, (c) 긴 파일 편집 시 에이전트 실수 증가.

  CLAUDE.md (39줄, 진입점)
  ├─ @.claude/conventions.md      언어/모델 관례 + 작업 방식
  ├─ @.claude/project-context.md  프로젝트 설명 + 문서 구조
  └─ @.claude/todos.md            지금 할 일
     .claude/session-summary.md   ← import 안 함(의도적, 온디맨드)

세션 히스토리 1231줄(전체의 80%)은 그 문서 스스로 "항상 읽을 필요 없음,
base/가 소스"라고 명시해온 색인이라 @import에서 뺐음. 내용 유실 없음
(1537→1647줄, 추가 헤더만큼 증가).

주의: @import는 컨텍스트를 줄이지 않음(전부 로드됨). 분할이 사는 건
사람 검토성 + 편집 정확도 + 파일 단위 자동생성 가능성.
CLAUDE.md 계열의 블록 HTML 주석은 주입 전 제거되므로 지시는 본문에 쓸 것.

## 워크플로 가짜 초록불 수정
첫 실측에서 감사 에이전트 6개 전원 실패했는데 converged:true가 나왔음
(전멸하면 fresh가 비어 "깨끗한 라운드"와 구분 불가). 감사 도구 최악의
실패 모드라 (1) 전멸이면 throw, (2) 반영 에이전트 실패 시 그 발견을
seen에서 빼 다음 라운드가 재시도하도록 수정.

## 부수
- 분할로 깨진 상호참조 20여 곳 정정(병렬 에이전트 3개).
  ref-plan.md:541의 사전 존재 오류(→ pre-implementation-audit 1-5)도 정정.
- doc-check.py: 새 파일 4개를 OURS에 등록(안 하면 깨진 참조가 WARN으로만
  잡힘), is_history()로 session-summary.md를 archive/와 같이 면제.
- doc-include-plan.md: 목적지가 통째로 생성되는 파일이 되면서 양방향
  마커 설계의 절반(목적지 마커)이 불필요해져 단방향 생성으로 단순화.

doc-check.py ERROR 0 유지.

미해결: session-summary.md 자동생성 미착수(91개 세션 파일 마커 삽입 필요),
orphan 인용 3건(modifier-plan.md:536, v1-compat-plan.md:50,
pre-implementation-audit.md:434 — 분할 이전부터 존재).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HvEKn9f67kkx2nGLG8PsP
2026-08-16 01:35:02 +09:00

6.9 KiB

Fallback/Traceback — 컴포넌트 에러 격리 유틸

상태: base — 확정(2026-08-14 세션). research/에서 신설(사용자 제안) → luau 스파이크로 xpcall/debug.traceback 배선 실측(같은 날 두 번째 세션) → Fallback/Traceback 분리·정확한 제네릭 시그니처·err: any 확정(같은 날 세 번째 세션, 사용자 확정)까지 한 흐름 — base/로 승격. 구현 우선순위는 여전히 맨 뒤(아래 "우선순위" 절), 승격은 설계가 다 정해졌다는 뜻이지 지금 만든다는 뜻이 아님.

동기

컴포넌트마다 개별적으로 pcall을 직접 감싸는 건 실용적이지 않음 — 매 호출 자리마다 local ok, result = pcall(MyComp, props); if not ok then ... end를 손으로 반복해 쓰는 건 번거롭고 빠뜨리기도 쉬움. 대신 컴포넌트 함수 하나를 받아서 "에러 나면 자동으로 플레이스홀더를 그려주는 버전"으로 바꿔주는 아주 단순한 유틸이면 충분함 — 클린업 동작(언마운트/리소스 해제)이 목적이 아니라, 실제 에러가 났을 때 디버깅이나 프로덕션 유저 리포트를 편하게 만드는 게 유일한 목적.

왜 새 프리미티브가 아닌가

research/additional-primitives-plan.md가 이미 "Error Boundary는 빈 자리 아님 — pcall(MyComp, props)만으로 React Error Boundary와 같은 격리 효과를 프레임워크 지원 없이 얻는다"고 확정해둔 결론을 뒤집는 게 아니라, 그 결론 위에 얹는 순수 슈가(그 문서를 다시 열 필요 없음) — Operator 콤비네이터(research/operator-sugar-plan.md)가 :Compute/ :Apply 위에 얹힌 것과 같은 관계. Fallback/Traceback 둘 다 original을 호출하고 결과를 그대로 돌려주는 순수 함수일 뿐, 디스패치/Store/Handler 계층에 아무것도 새로 안 만듦.

API — 왜 둘로 나뉘는가

Fallback<OkComp, ErrComp, Args...>(
  base: (Args...) -> OkComp,
  onError: (err: any) -> ErrComp
) -> (Args...) -> (OkComp | ErrComp)

Traceback<OkComp, ErrComp, Args...>(
  base: (Args...) -> OkComp,
  onError: (err: any, trace: string) -> ErrComp
) -> (Args...) -> (OkComp | ErrComp)
  • Fallbackpcall 기반, 가벼움, onErrorerr만 넘어감(trace 없음).
  • Tracebackxpcall+debug.traceback 기반, onErrorerr와 함께 trace: string항상(옵셔널 아님) 넘어감.
  • 왜 플래그 하나로 안 합쳤는가: quad는 이미 이런 갈림을 별도 타입/함수로 가르는 쪽을 택해왔음(Ref/PreRef가 같은 예) — 항상 xpcall+ debug.traceback 비용을 물지 않아도 되는 가벼운 경로를 자연스럽게 분리해 둘 수 있고, onError의 시그니처 자체가 달라서(trace 유무) 타입으로도 둘을 구분하는 게 더 정확함.
  • OkComp/ErrComp를 하나로 합친 Comp가 아니라 독립 제네릭으로 둔 이유: 원래 컴포넌트와 에러 플레이스홀더가 다른 컴포넌트 타입일 수 있고, 래핑된 함수의 실제 반환 타입은 정확히 OkComp | ErrComp 유니온이기 때문(사용자 확정).
  • onError 자신이 추가 컨텍스트를 캡처하려고 커링된 클로저인 건 완전히 자유 — Fallback/Traceback은 여기 관여하지 않음(아래 예시).
local SafeWidget = Fallback(Widget, function(err)
    return ErrorPlaceholder { Message = err }
end)

-- 추가 컨텍스트가 필요하면 onError 쪽에서 그냥 커링
local function makeErrorHandler(context)
    return function(err, trace)
        return ErrorPlaceholder { Message = err, Context = context, Trace = trace }
    end
end
local SafeWidget2 = Traceback(Widget, makeErrorHandler(someContext))

-- 호출부는 원래 컴포넌트 대신 그대로 씀
Frame { SafeWidget{ ... }, SafeWidget2{ ... } }

err: any임을 반드시 문서화 — 흔한 함정

Lua/Luau의 error()는 문자열이 아닌 임의의 값(테이블 등)을 던질 수 있음 — Fallback/Traceback 둘 다 errany로 그대로 전달하고 어떤 가공도 안 함. error(msg)를 레벨 지정 없이(Luau 기본 level=1) 호출하면 err가 문자열이더라도 quad가 아무것도 안 붙였는데 Luau가 자동으로 "파일:줄: " 위치 접두를 붙여서 옴 — error(msg, 0)으로 호출해야 접두 없는 순수 메시지가 옴. 다들 errstring으로 가정하고 코드를 짜는 게 제일 흔한 실수라 문서화에서 최우선으로 경고할 것(가공은 Fallback/Traceback이 대신해주지 않음 — 가공까지 대신해주면 그게 또 다른 매직이라는 원칙, onError 구현 몫으로 완전히 열어둠).

메커니즘 스케치

function Fallback(base, onError)
    return function(...)
        local ok, resultOrErr = pcall(base, ...)
        if ok then
            return resultOrErr
        end
        return onError(resultOrErr)
    end
end

function Traceback(base, onError)
    return function(...)
        local trace: string? = nil
        local ok, resultOrErr = xpcall(base, function(err)
            trace = debug.traceback(nil, 2)
            return err
        end, ...)
        if ok then
            return resultOrErr
        end
        return onError(resultOrErr, trace :: string)
    end
end

Tracebackdebug.traceback(nil, 2) 배선(클로저 업밸류가 xpcall 리턴 이후에도 정상적으로 보이는지, 중첩 호출에서도 실패 지점까지 스택을 정확히 담는지)과 err: any(테이블 에러도 손실 없이 통과하는지)는 luau 스파이크로 실측 확인됨 — audit/fallback-xpcall-verification.md 참고(스크립트: audit/fallback-xpcall-spike.luau). research/debug-tooling-plan.md가 이미 확인해둔 선례(Vide/Fusion 둘 다 xpcall+debug.traceback으로 에러 나는 순간에만 스택을 찍는 패턴)를 그대로 재사용 — 새 트레이싱 메커니즘을 발명하지 않음.

패키지 배치

quad-basebase를 그냥 호출하고 결과를 그대로 돌려주는 순수 함수라 Store/Dispatch 어디에도 안 걸림, 엔진 지식이 전혀 필요 없음. Operator 콤비네이터와 같은 결(사용자 확정).

이름

Fallback/Traceback 확정 — 낱개 함수 둘뿐이라 Tag/Attribute류 네임스페이스가 필요했던 것과 달리 충돌 표면이 작다고 판단, 용어 정리 대기열에 안 올리고 바로 점유(사용자 확정).

우선순위

형제 백로그 항목들(quad-mock/quad-debug/문서 사이트/Operator)과 동급, 맨 뒤 — "quad 개발 상당 부분 끝난 뒤"로 사용자가 못박은 후순위 (.claude/todos.md 4번). 이 문서가 base/로 승격된 건 설계가 다 확정됐다는 뜻이지, 구현 착수 순서가 앞당겨졌다는 뜻이 아님 — Operator처럼 순수 슈가라 없어도 quad 기능상 완전함(pcall을 직접 쓰면 되므로).