
【#2】 OpenClaw 분석하기 — openclaw 입력부터 실행까지
요약
OpenClaw CLI의 실행 경로와 초기화 과정을 심층 분석합니다. 메인 모듈 판별을 통한 이중 실행 방지 기제와 V8 컴파일 캐시를 활용한 프로세스 재시작 로직 등 거대 애플리케이션의 안정적인 실행 설계 방식을 다룹니다.
핵심 포인트
- isMainModule 가드를 통한 Gateway 이중 실행 및 포트 충돌 방지
- V8 컴파일 캐시 무효화 및 프로세스 재시작(respawn) 로직 분석
- 프로세스 식별 및 환경 변수 정규화를 통한 초기화 순서 설계
- 비자명한 분기점에 대한 인라인 주석 작성 규약의 중요성
본 기사의 코드 참조는 OpenClaw main의 cee2aca409 (version 2026.6.10) 시점입니다. 행 번호는 업데이트에 따라 어긋날 수 있습니다.
연재 「OpenClaw 분석하기」
터미널에 openclaw라고 입력한 후 Gateway가 응답할 때까지 어떤 일이 일어나는가. 이번에는 src/entry.ts (총 295행)를 기점으로, CLI 프로세스의 **실행 경로(起動の一本道)**를 분석합니다. 거대 애플리케이션의 실행 코드는 "어떻게 무거운 처리를 지연시키고, 지뢰를 밟지 않을 것인가"에 대한 고안이 가득 담긴 곳입니다.
package.json의 bin은 다음과 같습니다.
"bin": { "openclaw": "openclaw.mjs" }
루트의 openclaw.mjs가 얇은 래퍼(wrapper)이며, 실체는 dist/.../entry.js (소스는 src/entry.ts)입니다. 이 2단계 구조에는 이유가 있습니다.
entry.ts:57의 isMainModule 가드(guard)에는 이 파일의 가장 중요한 주석이 달려 있습니다.
// Guard: only run entry-point logic when this file is the main module.
// The bundler may import entry.js as a shared dependency when dist/index.js
// is the actual entry point; without this guard the top-level code below
...
dist/index.js (라이브러리용 엔트리)가 entry.js를 의존성으로 읽어들이는 경우가 있으며, 이때 탑 레벨(top-level)의 실행 처리가 다시 한 번 실행되면, **Gateway가 이중 실행되어 락(lock)/포트 충돌로 인해 크래시(crash)**가 발생합니다. ENTRY_WRAPPER_PAIRS (entry.ts:28)를 통해 openclaw.mjs / openclaw.js라는 래퍼 이름을 허용하면서도, 정말로 자신이 프로세스의 주인공일 때만 부작용(side effect)을 실행하도록 하는 방어 기제입니다.
이 주석은 AGENTS.md의 "lifecycle ordering / ownership boundary와 같은 비자명한 분기에는 인라인 주석(inline comment)을 필수적으로 작성한다"라는 규약의 좋은 사례이기도 합니다.
메인 모듈임이 확정되면, 먼저 실행 경로를 해결하고, 필요하다면 **자기 자신을 재시작(respawn)**합니다.
const installRoot = resolveEntryInstallRoot(entryFile);
const waitingForCompileCacheRespawn = respawnWithoutOpenClawCompileCacheIfNeeded({
currentFile: entryFile, installRoot,
...
Node의 V8 컴파일 캐시(compile cache)를 사용하면 실행이 빨라지지만, 상황에 따라서는 무효화하고 재시작하는 것이 더 안전합니다. respawnWithoutOpenClawCompileCacheIfNeeded가 그 판단을 담당하며, 재시작을 선택한 경우 부모 프로세스는 여기서 멈춥니다 (waitingForCompileCacheRespawn === true).
그 이후의 초기화도 순서가 의미를 갖습니다.
process.title = "openclaw" — ps 명령어로 식별하기 쉽게 함 -
ensureOpenClawExecMarkerOnProcess() — 자신이 openclaw 실행임을 나타내는 마커 (자식 프로세스 판정용) -
installProcessWarningFilter() — Node의 경고 노이즈를 억제 -
normalizeEnv() — 환경 변수의 정규화
process.argv = normalizeWindowsArgv(process.argv);
if (!ensureCliRespawnReady()) {
const parsedContainer = parseCliContainerArgs(process.argv); // --container
...
ensureCliRespawnReady() (entry.ts:90
)는 buildCliRespawnPlan()을 호출하고, 필요하다면 runCliRespawnPlan(plan)으로 재실행하여 true를 반환합니다. true인 경우는 "부모 프로세스는 더 이상 CLI를 계속해서는 안 된다"는 의미입니다. Node 버전이나 플래그 문제로 인해, 적절한 실행 형태로 **자신을 다시 시작(re-spawn)**하는 메커니즘입니다.
--container (컨테이너 타겟 지정)와 --profile / --dev (프로파일 전환)는 초기 단계에서 파싱되며, 두 옵션을 동시에 지정하는 것은 여기서 차단됩니다.
if (containerTargetName && parsed.profile) {
console.error("[openclaw] --container cannot be combined with --profile/--dev");
process.exit(2);
...
프로파일 지정이 있으면 applyCliProfileEnv로 환경을 전환하고, Commander와 애드혹(ad-hoc)한 argv 체크가 모두 일관성을 유지하도록 process.argv를 다시 씁니다.
시작 코드를 읽다 보면, tryHandle...FastPath라는 함수가 여러 개 있다는 것을 알 수 있습니다.
if (!tryHandleRootVersionFastPath(process.argv)) {
await runMainOrRootHelp(process.argv);
}
tryHandleRootVersionFastPath (entry.ts:22 import) — openclaw --version을 무거운 모듈을 전혀 로드하지 않고 즉시 응답합니다.
tryHandleRootHelpFastPath (entry.ts:137) — openclaw --help의 도움말 표시. 이 부분이 매우 뛰어난데, 먼저 loadRootHelpRenderOptionsForConfigSensitivePlugins를 통해 설정에 민감한 플러그인이 있는지 확인하고, 없다면 사전 계산된 도움말 문자열 (outputPrecomputedRootHelpText)을 그대로 출력합니다. 플러그인 상태에 따라 도움말 내용이 달라질 수 있으므로, 변하지 않는다는 것을 알 때만 사전 계산을 건너뛰는(shortcut) 2단계 구조를 취하고 있습니다.
tryHandlePrecomputedCommandHelpFastPath (entry.ts:208) — browser / secrets / nodes 각 서브 커맨드의 --help도 마찬가지로 사전 계산합니다.
왜 이렇게까지 하는 걸까요? --version이나 --help를 위해 플러그인 로더나 설정 로딩까지 실행하는 것은 낭비이며, 체감 속도도 느려지기 때문입니다. "무거운 초기화는 정말로 필요할 때까지 지연시킨다"는 설계가 시작 코드 곳곳에서 보입니다. OPENCLAW_GATEWAY_STARTUP_TRACE를 설정하면 entry.bootstrap / entry.argv 등의 측정 트레이스가 나오는 장치도 마련되어 있습니다 (createGatewayStartupTrace, src/cli/startup-trace.ts).
openclaw.mjs
└─ entry.ts (main module 판정)
├─ compile cache respawn 판정 ── 필요 시 재시작 후 부모 종료
...
openclaw gateway로 실행한 Gateway를 상주 서비스로 만드는 것이 src/daemon/입니다. README.md의 권장 설정인 openclaw onboard --install-daemon이 이곳을 사용합니다.
디렉토리를 보면 OS별 서비스 매니저를 추상화하고 있음을 알 수 있습니다.
launchd.ts / launchd-plist.ts — macOS (launchd user service)
systemd.ts / systemd-unit.ts / systemd-linger.ts — Linux (systemd user service)
schtasks.ts / schtasks-exec.ts — Windows (작업 스케줄러)
service.ts
/service-env.ts
/service-layout.ts
— 3개 OS 공통 서비스 추상화
즉, "OS별 서비스 등록의 차이"를 daemon/이 흡수하여, 상위 계층에서는 openclaw gateway restart/status --deep (AGENTS.md의 Platform/Ops 섹션)이라는 통일된 인터페이스로 다룰 수 있도록 설계되었습니다. 이와 대조적으로 bootstrap/은 얇게 구성되어 있으며, node-extra-ca-certs (추가 CA 인증서)와 node-startup-env (기동 시 환경)의 조정만을 담당합니다.
이번에 파악하고자 하는 핵심은 다음 3가지입니다.
- 이중 실행을 방지하는 main-module 가드 — 번들링 시의 공유 의존성에 강함.
- respawn을 통한 자기 재실행 — 컴파일 캐시/Node 실행 형태를 최적화하기 위해, 엔트리(entry)는 자신을 다시 실행하는 것을 마다하지 않음.
- fast-path를 통한 지연 초기화 —
--version/--help는 무거운 경로를 거치지 않고 즉시 응답. "필요할 때까지 읽지 않는다"는 원칙이 철저히 지켜짐.
이것들은 모두 "기동이 빠르고, 잘 망가지지 않는 CLI"를 만들기 위한 정석이며, 애플리케이션이 거대해질수록 그 효과가 커집니다.
#03에서는 기동 후 실행되는 Gateway와 그 통신 규약인 Gateway 프로토콜 (packages/gateway-protocol)을 분석합니다. 채널(Channel), 툴(Tool), 이벤트(Event)를 묶는 "제어 평면(Control Plane)"이 클라이언트(CLI / Web UI / 모바일 노드)와 어떻게 대화하는지 살펴봅니다. 또한 프로토콜의 버전 관리 규칙 (AGENTS.md: "additive first")에 대해서도 깊이 있게 다룹니다.
참고: src/entry.ts (특히 :28 / :49 / :57 / :90 / :130 / :137 / :208), src/cli/startup-trace.ts, src/daemon/, src/bootstrap/, README.md (daemon 셋업)
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기