REPL 화면 — 메인 인터랙션 루프

screens/REPL.tsx — 5,000줄의 React 컴포넌트가 Claude Code 세션 전체를 오케스트레이션합니다.

01

개요

REPL.tsx는 Claude Code의 메인 인터랙션 루프를 담당하는 단일 React 컴포넌트입니다. 5,000줄이라는 방대한 규모는 동시성 관리, 권한 큐, 원격 세션, 스웜 워커, 듀얼 렌더 모드, 세션 재개, 키보드 내비게이션 등 실질적인 복잡성을 반영합니다.

소스 파일: screens/REPL.tsx (~5,000줄)

6계층 내부 구조

  1. ~526-700줄: Props, 환경 가드, 마운트 시점 로깅
  2. ~700-1200줄: 상태 선언 (useState, useRef)
  3. ~1200-2700줄: 핵심 콜백 (setMessages, onCancel, getToolUseContext, onQueryEvent, onQuery, onSubmit)
  4. ~2700-4100줄: 이펙트와 부가 시스템 (세션 재개, 큐 처리, 키보드 핸들러, 유휴 감지)
  5. ~4100-4490줄: 트랜스크립트 모드 (가상 스크롤 레이아웃)
  6. ~4490-5005줄: 메인 JSX 렌더 트리
02

턴 생명주기: 3함수 체인

onSubmitonQueryonQueryImpl의 3단계 체인으로 각 턴이 처리됩니다.

onSubmit

즉시 커맨드의 패스트 패스 처리, 유휴 복귀 게이트(75분 이상 시 대화상자 표시), 셸 모드 라우팅을 담당합니다. SessionStart 훅이 해결될 때까지 차단됩니다. 의존성 배열이 의도적으로 큽니다(약 25개 deps) — 하지만 메시지를 messagesRef.current를 통해 읽어 클로저 캡처를 방지합니다.

딥 다이브 — Ref 기반 메시지 읽기

힙 분석에서 ref 패턴 도입 전 턴당 약 9개의 REPL 스코프 누수가 발견되었습니다. messagesuseCallback deps에 추가하면 턴당 약 30회 onSubmit이 재생성되어 고정된 클로저 체인을 통한 메모리 누수가 발생합니다.

onQuery — QueryGuard 상태 머신

단순 boolean이 아닌 세대 카운터를 사용하여, 취소된 쿼리의 오래된 finally 블록이 새 쿼리 시작 후 상태를 손상시키는 것을 방지합니다.

onQueryImpl

Haiku 제목 추출(첫 메시지만), 스킬 범위 allowedTools, 병렬 비동기(시스템 프롬프트, 사용자 컨텍스트, 킬스위치 확인), onQueryEvent를 통한 스트리밍을 수행합니다.

03

로딩 상태: 3개 독립 소스

소스 메커니즘 시점
isQueryActiveuseSyncExternalStore로컬 onQuery 실행 중
isExternalLoadinguseState원격 세션/SSH/백그라운드 태스크
hasRunningTeammatesuseMemo over tasks스웜 워커 실행 중

경과 시간은 상태(state)가 아닌 ref를 사용하여 재렌더를 방지합니다. Ref 리셋은 useEffect가 아닌 첫 렌더에서 isQueryActive가 true가 되는 시점에 수행됩니다 — 경쟁 조건을 피하기 위함입니다.

04

다이얼로그 우선순위 큐

getFocusedInputDialog()는 결정론적 우선순위에서 정확히 하나의 대화상자를 반환합니다:

  1. message-selector
  2. (타이핑 중 억제, 1.5초 디바운스)
  3. sandbox-permission
  4. 기타 권한
  5. 온보딩
  6. 콜아웃
주의사항

ScrollKeybindingHandlerCancelRequestHandler 전에 렌더됩니다. 이유: 선택 영역이 있는 Ctrl+C = 복사(취소 아님). 스크롤 핸들러가 선택 존재 시 전파를 중단합니다.

05

메시지 배열 관리

Zustand 스타일 ref 패턴: messagesRef.current가 동기적으로 동기화됩니다. 3가지 일관성 메커니즘:

06

두 가지 렌더 경로

트랜스크립트 모드

VirtualMessageList를 통한 가상 스크롤링, 검색, 내비게이션. 긴 세션에서 가상 스크롤 없이 약 250MB 할당이 발생하므로 성능상 필수입니다.

프롬프트 모드

표준 인터랙티브 대화 모드.

07

세션 재개 & 자동 복원

세션 재개

메시지 역직렬화부터 contentReplacementState 재구성까지 15개 순차 단계. 오래된 에이전트 이름 상속을 방지하기 위해 복원 전 메타데이터를 클리어합니다.

인터럽트 시 자동 복원

Escape로 취소 시 의미 있는 출력 없이 취소되면 대화가 되감기고 입력이 복원됩니다. 5개 가드가 적용됩니다. queryGuard.end() 바깥에서 실행됩니다 — onCancelforceEnd()를 호출하기 때문입니다.

08

메인 렌더 트리

AlternateScreen
  └─ KeybindingSetup
      └─ AnimatedTerminalTitle // 격리된 960ms 틱
      └─ GlobalKeybindingHandlers
      └─ ScrollKeybindingHandler
      └─ CancelRequestHandler
      └─ MCPConnectionManager
      └─ FullscreenLayout
          ├─ scrollable: 메시지 목록
          ├─ bottom: 프롬프트 입력
          ├─ overlay: toolJSX
          └─ modal: 대화상자
딥 다이브 — AnimatedTerminalTitle 격리

null을 반환하는 별도의 리프 컴포넌트로, 타이틀 애니메이션 사이드 이펙트만 실행합니다. 이렇게 격리함으로써 960ms 틱이 REPL 전체를 재렌더하지 않습니다.

09

핵심 요약

핵심 포인트

  • 5,000줄은 실질적 복잡성을 반영: 동시성, 권한 큐, 원격 세션, 스웜, 듀얼 렌더 모드, 세션 재개, 키보드 내비게이션
  • QueryGuard는 동기적 취소와 비동기 React 배칭 간의 비동기화를 방지합니다
  • Ref 기반 상태 읽기는 연쇄적 클로저 캡처와 메모리 누수를 방지합니다
  • 다이얼로그 시스템은 순수 결정론적 우선순위 로직을 사용합니다
  • 자동 복원은 설계상 세대 가드 바깥에서 실행됩니다
  • 풀스크린과 스크롤백 모드는 동일한 출력을 생성합니다
10

지식 확인

퀴즈 — 5문제

Q1. useCallback deps 대신 messagesRef.current로 메시지를 읽는 이유는?

  • A) deps에 messages를 추가하면 턴당 약 30회 onSubmit이 재생성되어 고정된 클로저 체인으로 메모리 누수 발생
  • B) TypeScript가 요구하므로
  • C) React 18의 제한사항
  • D) 성능 최적화만을 위해
messages를 deps에 추가하면 onSubmit이 매 턴마다 약 30회 재생성되고, 각 재생성이 이전 클로저를 고정하여 메모리 누수를 일으킵니다. Ref 패턴은 항상 최신 값을 읽으면서 클로저를 안정적으로 유지합니다.

Q2. QueryGuard의 세대 카운터가 하는 역할은?

  • A) 쿼리 수를 추적
  • B) 취소된 쿼리의 오래된 finally 블록이 새 쿼리 시작 후 상태를 손상시키는 것을 방지
  • C) 동시 쿼리를 제한
  • D) 분석 이벤트를 카운트
단순 boolean 대신 세대 카운터를 사용하면, 이전 쿼리의 finally 블록이 실행될 때 현재 세대와 비교하여 불일치 시 무시할 수 있습니다.

Q3. ScrollKeybindingHandler가 CancelRequestHandler 전에 렌더되어야 하는 이유는?

  • A) 알파벳 순서
  • B) 성능 최적화
  • C) 텍스트 선택 시 Ctrl+C는 복사여야 하고 취소가 아님 — 선택 존재 시 스크롤 핸들러가 전파 중단
  • D) React 요구사항
Ctrl+C는 두 가지 역할(복사와 취소)을 합니다. 텍스트가 선택되어 있으면 복사로 동작해야 하므로, 스크롤 핸들러가 먼저 체크하고 선택이 있으면 이벤트 전파를 중단합니다.

Q4. 자동 복원이 queryGuard.end() 바깥에서 실행되는 이유는?

  • A) 세대 가드 내부에서는 메시지에 접근할 수 없어서
  • B) onCancel이 forceEnd()를 호출하여 세대 카운터를 증가시키므로, end(thisGeneration)이 false를 반환 — 그래도 자동 복원은 실행되어야 함
  • C) React 생명주기 제약
  • D) 성능상 이유
onCancel이 forceEnd()를 호출하면 세대 카운터가 증가하여 정상적인 end(thisGeneration)은 false를 반환합니다. 하지만 자동 복원 로직은 여전히 실행되어야 하므로 가드 바깥에 배치됩니다.

Q5. 로컬 onQuery가 실행 중이 아닌데 isLoading이 true를 반환하는 경우는?

  • A) 백그라운드 메모리 추출 중
  • B) 컴파일 중
  • C) 원격 세션, SSH 연결, 또는 포그라운드된 백그라운드 태스크가 활성 상태(isExternalLoading)
  • D) MCP 서버 연결 중
isLoading은 3가지 독립 소스를 OR 결합합니다. isQueryActive 외에도 원격 세션, SSH, 포그라운드된 백그라운드 태스크가 isExternalLoading을 true로 설정할 수 있습니다.
0 / 5