권한 요청, 설정 화면, 인터랙티브 대화상자 — 터미널 내 React + Ink 렌더링.
Claude Code의 터미널 UI는 단순한 텍스트 출력이 아닙니다. React와 Ink를 활용한 완전한 컴포넌트 시스템으로, 권한 요청 대화상자, 설정 위자드, 선택 메뉴 등 복잡한 인터랙티브 UI를 제공합니다. 이 시스템은 4개의 레이어로 구성되어 관심사를 명확히 분리합니다.
components/Dialog.tsx → components/Pane.tsx → components/PermissionDialog.tsx → helpers/dialog.ts → components/CustomSelect.tsx
4개 레이어 아키텍처:
showDialog(), showSetupDialog() 등 대화상자를 시작하는 진입점Dialog, Pane, CustomSelect 등 재사용 가능한 기본 컴포넌트PermissionDialog, SettingsWizard, OnboardingFlow 등 특정 기능을 위한 복합 컴포넌트대화상자를 표시하는 두 가지 주요 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}
/>
}
})
}
Dialog는 모달 오버레이를 제공하는 기본 컨테이너입니다. 제목 표시줄, 테두리, 키보드 이벤트 캡처(Escape로 닫기)를 포함합니다. 모든 인터랙티브 대화상자의 기반이 됩니다.
Pane은 비모달 정보 패널입니다. Dialog와 달리 키보드 이벤트를 독점하지 않으며, 다른 UI 요소와 나란히 표시될 수 있습니다. 도움말 패널, 상태 표시, 미리보기 등에 사용됩니다.
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가 마운트되고, 사용자의 결정이 캐시에 저장됩니다. "거부" 결정은 캐시되지 않아 다음 요청 시 다시 물어봅니다.
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 키 설정, 권한 모드 선택, 기본 설정 구성을 안내합니다. 온보딩 완료 상태는 전역 설정에 저장되어 다음 실행 시 건너뜁니다.
showDialog()는 createDeferred() 패턴으로 사용자 선택을 Promise로 반환합니다Dialog는 모달 오버레이, Pane은 비모달 패널, PermissionDialog는 보안 권한 요청용 특수 Dialog입니다CustomSelect는 검색 필터링과 그룹화를 지원하는 향상된 선택 컴포넌트입니다Q1. showDialog()가 Promise를 반환할 수 있는 이유는 무엇인가요?
showDialog()는 createDeferred()로 Promise와 resolve 함수를 생성합니다. resolve 함수가 자식 컴포넌트(예: 버튼, 선택 메뉴)에 전달되어 사용자가 선택하면 Promise가 해결됩니다.Q2. Dialog와 Pane의 가장 큰 차이는 무엇인가요?
Dialog는 모달 오버레이로 Escape로 닫기와 키보드 이벤트 캡처를 포함합니다. Pane은 비모달 패널로 다른 UI 요소와 나란히 표시되며 키보드 이벤트를 독점하지 않습니다.Q3. PermissionDialog에서 "거부" 결정이 캐시되지 않는 이유는?
Q4. 4레이어 아키텍처에서 PermissionDialog는 어떤 레이어에 속하나요?
PermissionDialog는 특정 기능(권한 요청)을 위한 복합 컴포넌트이므로 Feature Components 레이어에 속합니다. Design System 레이어의 Dialog와 CustomSelect를 조합하여 구성됩니다.Q5. 위자드 패턴에서 각 단계가 독립적인 React 컴포넌트인 이유는?