screens/REPL.tsx — 5,000줄의 React 컴포넌트가 Claude Code 세션 전체를 오케스트레이션합니다.
REPL.tsx는 Claude Code의 메인 인터랙션 루프를 담당하는 단일 React 컴포넌트입니다. 5,000줄이라는 방대한 규모는 동시성 관리, 권한 큐, 원격 세션, 스웜 워커, 듀얼 렌더 모드, 세션 재개, 키보드 내비게이션 등 실질적인 복잡성을 반영합니다.
screens/REPL.tsx (~5,000줄)
onSubmit → onQuery → onQueryImpl의 3단계 체인으로 각 턴이 처리됩니다.
즉시 커맨드의 패스트 패스 처리, 유휴 복귀 게이트(75분 이상 시 대화상자 표시), 셸 모드 라우팅을 담당합니다. SessionStart 훅이 해결될 때까지 차단됩니다. 의존성 배열이 의도적으로 큽니다(약 25개 deps) — 하지만 메시지를 messagesRef.current를 통해 읽어 클로저 캡처를 방지합니다.
힙 분석에서 ref 패턴 도입 전 턴당 약 9개의 REPL 스코프 누수가 발견되었습니다. messages를 useCallback deps에 추가하면 턴당 약 30회 onSubmit이 재생성되어 고정된 클로저 체인을 통한 메모리 누수가 발생합니다.
단순 boolean이 아닌 세대 카운터를 사용하여, 취소된 쿼리의 오래된 finally 블록이 새 쿼리 시작 후 상태를 손상시키는 것을 방지합니다.
Haiku 제목 추출(첫 메시지만), 스킬 범위 allowedTools, 병렬 비동기(시스템 프롬프트, 사용자 컨텍스트, 킬스위치 확인), onQueryEvent를 통한 스트리밍을 수행합니다.
| 소스 | 메커니즘 | 시점 |
|---|---|---|
| isQueryActive | useSyncExternalStore | 로컬 onQuery 실행 중 |
| isExternalLoading | useState | 원격 세션/SSH/백그라운드 태스크 |
| hasRunningTeammates | useMemo over tasks | 스웜 워커 실행 중 |
경과 시간은 상태(state)가 아닌 ref를 사용하여 재렌더를 방지합니다. Ref 리셋은 useEffect가 아닌 첫 렌더에서 isQueryActive가 true가 되는 시점에 수행됩니다 — 경쟁 조건을 피하기 위함입니다.
getFocusedInputDialog()는 결정론적 우선순위에서 정확히 하나의 대화상자를 반환합니다:
ScrollKeybindingHandler는 CancelRequestHandler 전에 렌더됩니다. 이유: 선택 영역이 있는 Ctrl+C = 복사(취소 아님). 스크롤 핸들러가 선택 존재 시 전파를 중단합니다.
Zustand 스타일 ref 패턴: messagesRef.current가 동기적으로 동기화됩니다. 3가지 일관성 메커니즘:
useDeferredValue로 무거운 재렌더를 지연VirtualMessageList를 통한 가상 스크롤링, 검색, 내비게이션. 긴 세션에서 가상 스크롤 없이 약 250MB 할당이 발생하므로 성능상 필수입니다.
표준 인터랙티브 대화 모드.
메시지 역직렬화부터 contentReplacementState 재구성까지 15개 순차 단계. 오래된 에이전트 이름 상속을 방지하기 위해 복원 전 메타데이터를 클리어합니다.
Escape로 취소 시 의미 있는 출력 없이 취소되면 대화가 되감기고 입력이 복원됩니다. 5개 가드가 적용됩니다. queryGuard.end() 바깥에서 실행됩니다 — onCancel이 forceEnd()를 호출하기 때문입니다.
AlternateScreen
└─ KeybindingSetup
└─ AnimatedTerminalTitle // 격리된 960ms 틱
└─ GlobalKeybindingHandlers
└─ ScrollKeybindingHandler
└─ CancelRequestHandler
└─ MCPConnectionManager
└─ FullscreenLayout
├─ scrollable: 메시지 목록
├─ bottom: 프롬프트 입력
├─ overlay: toolJSX
└─ modal: 대화상자
null을 반환하는 별도의 리프 컴포넌트로, 타이틀 애니메이션 사이드 이펙트만 실행합니다. 이렇게 격리함으로써 960ms 틱이 REPL 전체를 재렌더하지 않습니다.
Q1. useCallback deps 대신 messagesRef.current로 메시지를 읽는 이유는?
Q2. QueryGuard의 세대 카운터가 하는 역할은?
Q3. ScrollKeybindingHandler가 CancelRequestHandler 전에 렌더되어야 하는 이유는?
Q4. 자동 복원이 queryGuard.end() 바깥에서 실행되는 이유는?
Q5. 로컬 onQuery가 실행 중이 아닌데 isLoading이 true를 반환하는 경우는?