다이얼로그 & UI 컴포넌트 시스템

권한 요청, 설정 화면, 인터랙티브 대화상자 — 터미널 내 React + Ink 렌더링.

01

개요

Claude Code의 터미널 UI는 단순한 텍스트 출력이 아닙니다. React와 Ink를 활용한 완전한 컴포넌트 시스템으로, 권한 요청 대화상자, 설정 위자드, 선택 메뉴 등 복잡한 인터랙티브 UI를 제공합니다. 이 시스템은 4개의 레이어로 구성되어 관심사를 명확히 분리합니다.

다루는 소스 파일: components/Dialog.tsxcomponents/Pane.tsxcomponents/PermissionDialog.tsxhelpers/dialog.tscomponents/CustomSelect.tsx

4개 레이어 아키텍처:

02

Launcher 패턴: showDialog / showSetupDialog

대화상자를 표시하는 두 가지 주요 Launcher가 있습니다. showDialog()는 범용 대화상자를 위한 것이고, showSetupDialog()는 초기 설정 및 온보딩 플로우를 위한 것입니다.

// showDialog — 범용 대화상자 런처
async function showDialog<T>(options: DialogOptions<T>): Promise<T> {
  const { resolve, promise } = createDeferred<T>()

  mountOverlay(
    <Dialog
      title={options.title}
      onDismiss={() => resolve(options.defaultValue)}
    >
      {options.render(resolve)}
    </Dialog>
  )

  return promise  // 사용자가 선택할 때까지 대기
}

showDialog()Promise를 반환하여 호출 코드가 사용자의 선택을 비동기적으로 기다릴 수 있습니다. 내부적으로 createDeferred() 패턴을 사용하여 resolve 함수를 자식 컴포넌트에 전달합니다.

// showSetupDialog — 온보딩/설정 전용 런처
async function showSetupDialog(config: SetupConfig): Promise<SetupResult> {
  return showDialog({
    title: config.title,
    render(resolve) {
      return <SetupWizard
        steps={config.steps}
        onComplete={resolve}
      />
    }
  })
}
03

Dialog / Pane / PermissionDialog 구분

Dialog 컴포넌트

Dialog는 모달 오버레이를 제공하는 기본 컨테이너입니다. 제목 표시줄, 테두리, 키보드 이벤트 캡처(Escape로 닫기)를 포함합니다. 모든 인터랙티브 대화상자의 기반이 됩니다.

Pane 컴포넌트

Pane은 비모달 정보 패널입니다. Dialog와 달리 키보드 이벤트를 독점하지 않으며, 다른 UI 요소와 나란히 표시될 수 있습니다. 도움말 패널, 상태 표시, 미리보기 등에 사용됩니다.

PermissionDialog 컴포넌트

PermissionDialog는 보안이 중요한 권한 요청을 위한 특수 Dialog입니다. 요청하는 도구, 대상 파일/디렉토리, 수행할 작업을 명확히 표시하고, "항상 허용" / "이번만 허용" / "거부" 선택지를 제공합니다.

// PermissionDialog — 권한 요청 전용 대화상자
function PermissionDialog({ request, onDecision }: PermissionProps) {
  return (
    <Dialog title={"권한 요청"}>
      <ToolDescription tool={request.tool} />
      <TargetDisplay target={request.target} />
      <ActionPreview action={request.action} />
      <CustomSelect
        items={[
          { label: '항상 허용', value: 'always' },
          { label: '이번만 허용', value: 'once' },
          { label: '거부',      value: 'deny' },
        ]}
        onSelect={onDecision}
      />
    </Dialog>
  )
}
딥 다이브 — 권한 요청 라우팅

도구가 권한을 요청하면 요청은 먼저 권한 캐시를 확인합니다. 캐시에 "항상 허용" 항목이 있으면 대화상자 없이 즉시 허용됩니다. 캐시에 없으면 PermissionDialog가 마운트되고, 사용자의 결정이 캐시에 저장됩니다. "거부" 결정은 캐시되지 않아 다음 요청 시 다시 물어봅니다.

04

CustomSelect와 위자드 패턴

CustomSelect

CustomSelect는 터미널 환경에서 화살표 키로 항목을 선택할 수 있는 커스텀 선택 컴포넌트입니다. Ink의 기본 Select를 확장하여 검색 필터링, 그룹화, 설명 표시 등 추가 기능을 제공합니다.

// CustomSelect — 향상된 터미널 선택 컴포넌트
function CustomSelect<T>({ items, onSelect, filter }: SelectProps<T>) {
  const [highlightedIndex, setHighlightedIndex] = useState(0)
  const [query, setQuery] = useState('')

  const filtered = filter
    ? items.filter(item => item.label.includes(query))
    : items

  useInput((input, key) => {
    if (key.upArrow)   setHighlightedIndex(i => Math.max(0, i - 1))
    if (key.downArrow) setHighlightedIndex(i => Math.min(filtered.length - 1, i + 1))
    if (key.return)    onSelect(filtered[highlightedIndex].value)
  })

  return <Box flexDirection="column">{/* 항목 렌더링 */}</Box>
}

위자드 패턴

다단계 설정 플로우는 위자드 패턴으로 구현됩니다. 각 단계는 독립적인 React 컴포넌트이며, 위자드 컨테이너가 현재 단계를 관리하고 단계 간 데이터를 전달합니다.

온보딩

최초 실행 시 온보딩 위자드가 표시됩니다. API 키 설정, 권한 모드 선택, 기본 설정 구성을 안내합니다. 온보딩 완료 상태는 전역 설정에 저장되어 다음 실행 시 건너뜁니다.

05

핵심 요약

핵심 포인트

  • UI 시스템은 4개 레이어로 구성됩니다: Launchers → Helpers → Design System → Feature Components
  • showDialog()createDeferred() 패턴으로 사용자 선택을 Promise로 반환합니다
  • Dialog는 모달 오버레이, Pane은 비모달 패널, PermissionDialog는 보안 권한 요청용 특수 Dialog입니다
  • 권한 요청은 캐시를 먼저 확인하고, "항상 허용"은 캐시되지만 "거부"는 캐시되지 않습니다
  • CustomSelect는 검색 필터링과 그룹화를 지원하는 향상된 선택 컴포넌트입니다
  • 위자드 패턴은 다단계 설정을 독립 컴포넌트로 분리하여 관리합니다
06

지식 확인

퀴즈 — 5문제

Q1. showDialog()가 Promise를 반환할 수 있는 이유는 무엇인가요?

  • A) 터미널 I/O가 본래 비동기적이기 때문
  • B) createDeferred() 패턴으로 resolve 함수를 자식 컴포넌트에 전달하여 사용자 선택 시 Promise를 해결하기 때문
  • C) React의 Suspense 메커니즘을 활용하기 때문
  • D) Node.js의 readline 모듈을 래핑하기 때문
showDialog()createDeferred()로 Promise와 resolve 함수를 생성합니다. resolve 함수가 자식 컴포넌트(예: 버튼, 선택 메뉴)에 전달되어 사용자가 선택하면 Promise가 해결됩니다.

Q2. DialogPane의 가장 큰 차이는 무엇인가요?

  • A) Dialog는 모달로 키보드 이벤트를 독점하지만, Pane은 비모달로 다른 UI와 공존한다
  • B) Dialog는 텍스트만, Pane은 그래픽도 표시할 수 있다
  • C) Dialog는 사용자 입력을 받지만, Pane은 출력만 한다
  • D) Dialog는 상단에, Pane은 하단에 표시된다
Dialog는 모달 오버레이로 Escape로 닫기와 키보드 이벤트 캡처를 포함합니다. Pane은 비모달 패널로 다른 UI 요소와 나란히 표시되며 키보드 이벤트를 독점하지 않습니다.

Q3. PermissionDialog에서 "거부" 결정이 캐시되지 않는 이유는?

  • A) 기술적 제약으로 거부 상태를 저장할 수 없다
  • B) 거부를 캐시하면 메모리 사용량이 증가하기 때문
  • C) 다음 요청 시 다시 물어봐서 사용자가 결정을 바꿀 기회를 제공하기 위해
  • D) 보안 감사 로그를 위해 매번 기록이 필요하기 때문
"거부"를 캐시하면 사용자가 나중에 허용하고 싶을 때 캐시를 수동으로 지워야 합니다. 캐시하지 않음으로써 다음 권한 요청 시 대화상자가 다시 표시되어 사용자가 쉽게 결정을 바꿀 수 있습니다.

Q4. 4레이어 아키텍처에서 PermissionDialog는 어떤 레이어에 속하나요?

  • A) Launchers
  • B) Helpers
  • C) Design System
  • D) Feature Components
PermissionDialog는 특정 기능(권한 요청)을 위한 복합 컴포넌트이므로 Feature Components 레이어에 속합니다. Design System 레이어의 DialogCustomSelect를 조합하여 구성됩니다.

Q5. 위자드 패턴에서 각 단계가 독립적인 React 컴포넌트인 이유는?

  • A) 터미널이 한 번에 하나의 컴포넌트만 렌더링할 수 있기 때문
  • B) 각 단계를 독립적으로 테스트하고, 재사용하며, 순서를 변경할 수 있기 때문
  • C) React가 단일 컴포넌트의 상태 크기를 제한하기 때문
  • D) 메모리 사용량을 줄이기 위해 이전 단계를 언마운트하기 때문
독립적인 컴포넌트로 구성하면 각 단계를 개별적으로 테스트하고, 다른 위자드에서 재사용하며, 플로우의 순서를 쉽게 변경할 수 있습니다. 위자드 컨테이너가 단계 간 데이터 전달과 네비게이션을 관리합니다.
0 / 5