시리즈 이전 편: [Part 1] 프로젝트 구조 분석 -- 51만 줄의 지도 그리기
터미널에 claude라고 치면 프롬프트가 뜨기까지 체감상 0.5초 정도 걸립니다. 그 짧은 시간 안에 설정 로딩, OAuth 인증, 텔레메트리 초기화, API 서버 TCP 프리커넥트, React/Ink 렌더러 마운트까지 전부 끝납니다. 오늘은 그 과정을 소스코드 레벨에서 따라가 보겠습니다.
목차
-
- cli.tsx -- 진짜 진입점, fast-path의 세계
-
- init.ts -- 설정, 네트워크, 텔레메트리 초기화
-
- main.tsx -- 모듈 로딩의 시간싸움
-
- Commander.js 파싱 + React/Ink 렌더러
-
- 병렬 프리페치 패턴 -- 전부 동시에
-
- 그래서 뭐가 달라지나 -- CLI 부팅 최적화 기법 정리
1. cli.tsx -- 진짜 진입점, fast-path의 세계
Claude Code의 최초 진입점은 src/entrypoints/cli.tsx입니다. 이 파일의 핵심 설계 원칙은 딱 하나, "필요 없으면 아무것도 로딩하지 마라" 입니다.
// src/entrypoints/cli.tsx (line 33~42)
async function main(): Promise<void> {
const args = process.argv.slice(2);
// Fast-path for --version/-v: zero module loading needed
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
console.log(`${MACRO.VERSION} (Claude Code)`);
return;
}
// ...
}
--version은 모듈 임포트가 정확히 0개입니다. MACRO.VERSION은 빌드 타임에 인라인되기 때문에 런타임 비용이 없습니다. --help도 비슷한 구조고, --dump-system-prompt, --daemon-worker, claude ps, claude daemon 등 서브커맨드별로 전용 fast-path가 분기되어 있습니다.
모든 fast-path를 통과한 "일반 실행"만 마지막에 main.tsx를 동적 임포트합니다.
// src/entrypoints/cli.tsx (line 292~297)
profileCheckpoint('cli_before_main_import');
const { main: cliMain } = await import('../main.js');
profileCheckpoint('cli_after_main_import');
await cliMain();
이 구조 덕분에 claude --version은 수 밀리초, 전체 부팅은 수백 밀리초에 끝납니다. fast-path가 아닌 경로만 무거운 모듈을 로딩하는 겁니다.
2. init.ts -- 설정, 네트워크, 텔레메트리 초기화
src/entrypoints/init.ts는 memoize로 감싸진 단일 init() 함수입니다. 두 번 호출해도 한 번만 실행됩니다. 여기서 하는 일을 순서대로 정리하면 이렇습니다.
| 순서 | 작업 | 비고 |
|---|---|---|
| 1 | enableConfigs() |
설정 시스템 활성화 |
| 2 | applySafeConfigEnvironmentVariables() |
신뢰 확인 전 안전한 환경변수만 적용 |
| 3 | applyExtraCACertsFromConfig() |
TLS 인증서 -- 첫 핸드셰이크 전에 반드시 실행 |
| 4 | setupGracefulShutdown() |
종료 시 텔레메트리 플러시 보장 |
| 5 | populateOAuthAccountInfoIfNeeded() |
OAuth 정보 캐시 (fire-and-forget) |
| 6 | configureGlobalMTLS() + configureGlobalAgents() |
프록시/mTLS 설정 |
| 7 | preconnectAnthropicApi() |
API 서버에 TCP+TLS 미리 연결 |
특히 7번이 재미있습니다. 아직 사용자가 프롬프트를 입력하지도 않았는데, Anthropic API 서버에 미리 TCP 연결을 열어둡니다.
// src/entrypoints/init.ts (line 153~159)
// Preconnect to the Anthropic API -- overlap TCP+TLS handshake
// (~100-200ms) with the ~100ms of action-handler work before the API
// request. After CA certs + proxy agents are configured so the warmed
// connection uses the right transport.
preconnectAnthropicApi()
100~200ms짜리 TLS 핸드셰이크를 다른 초기화 작업과 겹치게 만드는 겁니다. 첫 번째 API 호출 시 이미 연결이 준비되어 있으니 체감 응답이 빨라집니다.
3. main.tsx -- 모듈 로딩의 시간싸움
main.tsx는 파일 상단부터 전장입니다. 200줄 가까이가 전부 import 문인데, 그 사이에 사이드이펙트 임포트 3개가 끼어 있습니다.
// src/main.tsx (line 9~20)
profileCheckpoint('main_tsx_entry');
import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead(); // MDM 설정 서브프로세스 즉시 시작
import { ensureKeychainPrefetchCompleted, startKeychainPrefetch }
from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch(); // 키체인 읽기 즉시 시작
주석이 설계 의도를 정확히 설명하고 있습니다. startMdmRawRead()는 MDM 설정을 읽는 plutil/reg query 서브프로세스를, startKeychainPrefetch()는 macOS 키체인 읽기를 즉시 시작합니다. 이 두 작업은 나머지 135ms 분량의 임포트가 진행되는 동안 백그라운드에서 병렬 실행됩니다.
그리고 Commander.js의 preAction 훅에서 이 결과를 수거합니다.
// src/main.tsx (line 907~916)
program.hook('preAction', async thisCommand => {
// 서브프로세스 결과 대기 -- 임포트 중 이미 완료됨
await Promise.all([
ensureMdmSettingsLoaded(),
ensureKeychainPrefetchCompleted()
]);
await init();
// ...
});
await하는 시점에는 이미 끝나 있을 가능성이 높습니다. 135ms 동안 병렬로 돌렸으니까요.
4. Commander.js 파싱 + React/Ink 렌더러
main() 함수 안에서 Commander.js 프로그램이 생성되고, 여기에 수십 개의 옵션과 서브커맨드가 등록됩니다.
// src/main.tsx (line 902)
const program = new CommanderCommand()
.configureHelp(createSortedHelpConfig())
.enablePositionalOptions();
program.name('claude')
.description('Claude Code - starts an interactive session by default')
.argument('[prompt]', 'Your prompt', String)
.helpOption('-h, --help', 'Display help for command')
.option('-p, --print', 'Print response and exit')
// ... 수십 개 옵션
대화형 세션이 시작되면 React/Ink 렌더러가 마운트됩니다. Ink는 React 컴포넌트를 터미널 UI로 렌더링하는 라이브러리입니다.
// src/main.tsx (line 2227~2229)
const { createRoot } = await import('./ink.js');
root = await createRoot(ctx.renderOptions);
ink.js도 동적 임포트입니다. -p(print) 모드처럼 비대화형 세션에서는 Ink를 아예 로딩하지 않습니다. 주석에도 "Ink constructor의 patchConsole이 headless 모드에서 console 출력을 삼킨다"고 적혀 있습니다.
5. 병렬 프리페치 패턴 -- 전부 동시에
신뢰 다이얼로그(Trust Dialog)를 통과하면 백그라운드 프리페치가 시작됩니다. 여기서도 void + Promise를 활용한 fire-and-forget 패턴이 반복됩니다.
// src/main.tsx (line 2350~2364)
checkQuotaStatus().catch(error => logError(error));
void fetchBootstrapData();
void prefetchPassesEligibility();
void prefetchFastModeStatus();
Quota 확인, 부트스트랩 데이터, 패스 자격, Fast Mode 상태를 전부 동시에 요청합니다. 이 결과들은 REPL이 렌더링되는 동안 백그라운드에서 도착하고, 첫 번째 턴에서 사용자가 프롬프트를 입력할 때쯤이면 캐시가 채워져 있습니다.
MCP 서버 연결도 마찬가지입니다. prefetchAllMcpResources()로 미리 연결을 시작하되, REPL 렌더링을 절대 블로킹하지 않습니다. 느린 MCP 서버는 2번째 턴부터 사용 가능해집니다.
6. 그래서 뭐가 달라지나 -- CLI 부팅 최적화 기법 정리
Claude Code의 부팅 과정에서 배울 수 있는 패턴을 정리하면 이렇습니다.
Fast-path 분기: --version 같은 단순 명령은 모듈 로딩 자체를 건너뜁니다. 동적 import()로 필요한 경로만 로딩하는 게 핵심입니다.
사이드이펙트 임포트 사이에 서브프로세스 시작: 모듈 평가(evaluation)가 진행되는 135ms를 그냥 낭비하지 않고, 그 시간에 MDM/키체인 서브프로세스를 병렬로 돌립니다.
TCP 프리커넥트: API 호출 전에 TLS 핸드셰이크를 미리 시작해서 100~200ms를 절약합니다.
텔레메트리 지연 로딩: OpenTelemetry + protobuf 모듈(~400KB)을 init 시점이 아니라 텔레메트리가 실제로 필요할 때 동적 임포트합니다.
memoize로 중복 초기화 방지: init()이 여러 경로에서 호출될 수 있지만 실행은 한 번만 됩니다.
이런 기법들이 합쳐져서 "터미널에 치고 엔터 누르면 바로 뜨는" 경험이 만들어집니다.
다음 편 예고: [Claude Code 해부학 Part 3] 클로드 코드(Claude Code) 시스템 프롬프트 분석 -- 915줄은 어떻게 조립되는가
[Claude Code 해부학] 시리즈
- Part 0: 소스코드 51만 줄이 유출됐다
- Part 1: 프로젝트 구조 분석
- Part 3: 시스템 프롬프트 분석
'AI 코딩 에이전트' 카테고리의 다른 글
| Claude Code 프롬프트 캐싱 분석: API 비용을 90% 줄이는 구조 (해부학 Part 4) (0) | 2026.04.01 |
|---|---|
| Claude Code 시스템 프롬프트 분석: 915줄은 어떻게 조립되는가 (해부학 Part 3) (0) | 2026.04.01 |
| Claude Code 프로젝트 구조 분석: 1,900개 파일의 지도 그리기 (해부학 Part 1) (2) | 2026.04.01 |
| Claude Code BashTool 분석: 도구 프롬프트가 500줄인 이유 (해부학 Part 7) (0) | 2026.04.01 |
| Claude Code 소스코드 51만 줄 전체 분석: 유출된 코드를 전부 뜯어본다 (해부학 Part 0) (0) | 2026.04.01 |
