
【#12】 OpenClaw 분석하기 — 통제된 거대함, 뒷받침하는 요소와 그 철학
요약
OpenClaw 프로젝트의 거대한 코드베이스를 유지하기 위한 빌드, 테스트, 품질 관리 시스템을 분석합니다. tsdown을 활용한 통합 번들링 전략과 Vitest를 이용한 다중 레인 테스트 구조, 그리고 동적 임포트 오류를 방지하는 CI 메커니즘을 다룹니다.
핵심 포인트
- tsdown을 사용하여 코어와 플러그인 SDK의 dts 생성을 하나의 그래프로 관리
- 의존성 성격에 따른 명시적인 번들링/비번들링 전략 적용
- 동적 임포트 경계 체크를 통해 지연 로딩의 효율성 보장
- Vitest 래퍼를 통한 프로세스 클린업 및 타임아웃(Watchdog) 관리
본 기사의 코드 참조는 OpenClaw main의 cee2aca409 (version 2026.6.10) 시점입니다. 행 번호는 업데이트에 따라 어긋날 수 있습니다.
연재 「OpenClaw를 분석하기」
연재 최종회입니다. 지금까지 11회에 걸쳐 OpenClaw의 각 서브시스템 (subsystem)을 읽어왔습니다. 마지막으로는 그 거대한 코드베이스를 파탄 내지 않고 유지하고 있는 뒷받침 요소——빌드(build)·테스트(test)·품질 게이트(quality gate)——를 살펴본 뒤, 전체를 관통하는 설계 철학을 총괄합니다.
OpenClaw의 빌드는 tsdown (rolldown 기반의 번들러 (bundler))입니다. 설정은 tsdown.config.ts (800행 초과)에 집약됩니다. 코어(core)·워크스페이스 패키지(workspace package)·번들 플러그인(bundle plugin)·plugin-sdk의 dts 생성을 **하나의 그래프 (graph)**로 다루는 것이 목적이며, Rollup 설정이 흩어지는 것을 방지하고 있습니다.
의존성 (dependency) 처리가 명시적입니다.
- 절대로 번들링하지 않음:
@slack/bolt,vitest,jimp,sharp,typescript등 (네이티브 의존성이나 거대 라이브러리). - 반드시 번들링함:
@openclaw/fs-safe,@openclaw/normalization-core,zod등 내부 및 소규모 의존성.
plugin-sdk의 dts는 서브 패스 (sub-path)마다 .d.ts를 생성합니다 (buildPluginSdkPackageExports(), 정의는 src/plugin-sdk/entrypoints.ts:103. tsdown.config.ts 측은 이 publicPluginSdkEntrypoints를 가져오기만 함). #04에서 보았던 약 322개의 서브 패스 엑스포트 (sub-path export)가 여기서 자동 생성됩니다.
export function buildPluginSdkPackageExports() {
return Object.fromEntries(
publicPluginSdkEntrypoints.map((entry) => [
...
그리고 #04에서 예고했던 동적 임포트 (dynamic import) 경계 체크. *.runtime.ts라는 지연 경계 (lazy boundary)에 대해, 동일한 모듈을 static과 dynamic 양쪽 모두에서 import 하고 있지는 않은지 검사하며, 문제가 있다면 [INEFFECTIVE_DYNAMIC_IMPORT]를 출력합니다 (lint:tmp:dynamic-import-warts). 지연 로딩 (lazy loading)의 효과를 무효화하는 실수를 CI에서 잡아내는 메커니즘입니다. 런타임 (runtime)은 Node 22.19+ (24 권장)이며, Bun도 지원합니다.
테스트는 vitest입니다. 코로케이트 (colocated)된 *.test.ts, E2E는 *.e2e.test.ts입니다. 특징은 다수의 레인 (lane, 프로젝트)으로 분할되어 있다는 점입니다 (unit / infra / gateway-core / contracts-plugin / extension-* …).
직접 vitest를 실행하지 않고, scripts/run-vitest.mjs 래퍼 (wrapper)를 통하는 것이 규약입니다 (AGENTS.md). 이유는 다음과 같습니다.
- 프로세스 그룹의 클린업 (SIGTERM으로 자식 프로세스까지 정리).
- 출력 없는 와치독 (watchdog): 기본 120초, E2E는 300초, infra는 더 길게 설정하는 등 config마다 타임아웃을 설정. 멈춰버린(hang) 테스트를 자동으로 종료.
- 순수
vitest(watch 모드)를 방지하고,vitest run을 강제 (watch는 스스로 종료되지 않음).
그리고 OpenClaw 독자적인 Crabbox / Testbox를 통한 원격 검증. scripts/crabbox-wrapper.mjs가 이를 해결합니다. Mac 로컬에서 전부 돌리면 무겁거나, Codex worktree에서는 pnpm이 install 프롬프트를 띄우는 등의 사정 때문에, 광범위한 검증은 격리 컨테이너 (isolated container)로 넘깁니다. pnpm check:changed가 Crabbox에 위임되며, pnpm changed:lanes --json이 "git diff로 어떤 레인이 수정되었는지(touched)"를 보고합니다.
레인 분류 (changed-lanes.mjs
)는 정규 표현식으로 경로를 분류합니다.
const DOCS_PATH_RE = /^(?:docs\/|README\.md$|AGENTS\.md$|.*\.mdx?$)/u;
const EXTENSION_PATH_RE = /^extensions\/[^/]+(?:\/|$)/u;
const CORE_PATH_RE = /^(?:src\/|ui\/|packages\/)/u;
...
문서만 변경했다면 docs 레인만, package.json을 건드렸다면 모든 레인, 하는 식으로 필요한 테스트만 실행합니다. 거대 리포지토리에서 CI 시간을 억제하는 핵심입니다.
OpenClaw는 일반적인 JS 툴체인 (Toolchain)을 더 빠른 대체재로 교체하고 있습니다.
| 용도 | 채택 | 미채택 |
|---|---|---|
| 포맷팅 (Formatting) | oxfmt | Prettier |
| 린트 (Lint) | oxlint | ESLint |
| 타입 체크 (Type Check) | tsgo (pnpm tsgo:core 등) | tsc --noEmit |
모두 Rust 기반으로 매우 빠릅니다. tsgo는 워크스페이스 패키지(Workspace Package)를 가로지르는 타입 체크를 병렬화합니다. 규약에는 tsc --noEmit / typecheck / check:types를 추가하지 않는다고 명시되어 있습니다.
아키텍처 계열 검사도 풍부합니다.
check:import-cycles/check:madge-import-cycles— 순환 의존성(Circular Dependency) 탐지 (강결합 성분(Strongly Connected Components)을 확인).check:loc— 파일당 최대 500행.node --import tsx scripts/check-ts-max-loc.ts --max 500.check:architecture— 위 항목들에 더해 deprecated API / jsdoc / Kysely 검사를 통합 실행.lint:plugins:no-monolithic-plugin-sdk-entry-imports— 루트 배럴(Root Barrel)이 아닌 서브 경로(Sub-path) 임포트를 강제 (#04의 지연 로딩 규율).
check:loc가 보여주듯, 코드량 그 자체가 품질 지표로서 CI에 포함되어 있습니다.
연재를 통해 동일한 원칙이 여러 번 등장했습니다. 마지막으로 그것들을 하나로 묶어보겠습니다.
AGENTS.md의 Code 절은 명쾌합니다.
Code size matters. Prefer small clear code; maintainability includes not growing LOC without payoff.
(코드 크기는 중요하다. 작고 명확한 코드를 선호하라. 유지보수성에는 이득 없이 LOC(Lines of Code)를 늘리지 않는 것도 포함된다.)Refactors should reduce non-test LOC unless they remove a larger architectural cost. Treat positive prod LOC as a smell. Before closeout, run
git diff --numstat; if non-test LOC grew, trim or explicitly justify why fewer paths now exist.
(리팩터링은 더 큰 아키텍처 비용을 제거하는 경우가 아니라면 테스트 외 LOC를 줄여야 한다. 양(+)의 프로덕션 LOC를 냄새(Smell)로 취급하라. 작업을 마치기 전git diff --numstat을 실행하라. 만약 테스트 외 LOC가 늘어났다면, 내용을 다듬거나 왜 경로가 더 적어졌는지 명시적으로 정당화하라.)Prefer deleting branches, modes, adapters, and tests over preserving them.
(브랜치, 모드, 어댑터, 테스트를 보존하기보다 삭제하는 것을 선호하라.)
"LOC가 늘어나는 것은 냄새(Smell)다", "분기·모드·어댑터·테스트는 보존보다 삭제를 택하라". check:loc라는 CI 게이트와 맞물려, 비대해지는 것에 저항하는 메커니즘이 문화와 기계 양면에서 작동하고 있습니다.
#01·#06·#10에서 반복해서 보았듯이, 런타임(Runtime)은 현재의 정준형(Canonical Form)만 읽습니다. 오래된 설정이나 상태의 호환성은 openclaw doctor --fix의 마이그레이션(Migration)에 일원화시키고, 상시 런타임에 폴백(Fallback) 분기를 두지 않습니다. 그렇기에 '읽어야 할 경로'가 항상 하나로 유지되어, 122만 행이라도 각 서브시스템의 동작을 추적할 수 있습니다.
#04·#05·#06·#11에서 보았듯이, 코어(Core)에 특정 ID·기본값·정책을 하드코딩하지 않고, **역량 계약(Capability Contract)과 매니페스트(Manifest)**를 통해 플러그인이 자신의 능력을 선언합니다. 코어는 범용 루프를 돌릴 뿐입니다. 130개 이상의 플러그인을 품고 있어도 코어가 비대해지지 않는 이유는 바로 이 일방향 의존성 덕분입니다. 소유 경계는 디렉터리별 AGENTS.md
에 명문화되어 있으며, "어떤 코드가 무엇을 소유하는지"를 항상 추적할 수 있습니다.
#03의 에러 코드, #06의 페일오버 (failover) 이유, #07의 종료 상태, #08의 메시지 트리(message tree)——이들 모두 "자유 문자열 센티넬 (free-string sentinel)"이 아니라 **닫힌 판별된 공용체 (closed discriminated union) / 닫힌 코드 (closed code)**로 표현되어 있었습니다. AGENTS.md의 "표현 불가능한 상태를 만들 수 없게 하기 (make impossible states unrepresentable)"라는 원칙이 곳곳에서 구체화되어 있습니다.
#02의 패스트 패스 (fast-path), #04의 *.runtime 경계, #08의 프롬프트 캐시 결정론적 순서. "필요할 때까지 읽지 않는다", "캐시를 깨뜨리지 않는 순서"라는 운영 요구사항이 코드 구조 그 자체에 녹아들어 있었습니다.
OpenClaw는 "멀티 채널 × 멀티 프로바이더 × 플러그인형"이라는, 본래라면 무너지기 쉬운 세 축의 교차점에 서 있는 프로젝트입니다. 그럼에도 불구하고 이를 해석할 수 있었던 이유는,
**단 하나의 정준 경로 (canonical path)**만을 가지며,
명확한 소유 경계로 책임을 나누고,
**기능 계약 (capability contract)**으로 코어를 얇게 유지하며,
**CI 게이트 (CI gate)**를 통해 그 규율을 기계적으로 지키고
있었기 때문입니다. 거대함은 무질서의 결과가 아니라, 규율이 쌓인 결과였습니다——이것이 연재 전체의 결론입니다. OpenClaw의 코드를 열 때는, 먼저 해당 디렉터리의 AGENTS.md를 읽고 정준 경로를 하나 찾아내십시오. 그 이후부터는 이 연재에서 살펴보았던 것과 동일한 패턴의 재연으로서 읽어 내려갈 수 있을 것입니다.
12회 동안 함께해 주셔서 감사합니다.
참고: tsdown.config.ts, src/plugin-sdk/entrypoints.ts, scripts/run-vitest.mjs, scripts/crabbox-wrapper.mjs, scripts/check-ts-max-loc.ts, scripts/changed-lanes.mjs, 루트 AGENTS.md (Commands / Validation / Tests / Code 절)
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기