히스토리
- 임베디드 시뮬레이션 테스트 전략 추가 (테스트 피라미드, Output enum 추상화)
- 요소별 심층 분석 방법론 추가 (BlockingQueue 사례, 학습 템플릿)
- Ghostty 동시성 아키텍처 비교 추가 (libxev, BlockingQueue, Mutex 패턴)
- Ghostty 분석 추가: 크로스플랫폼 앱의 Zig 선택 이유, 전문가 개발 방법론
- 줄줄이 떠들었다. 그리고 작성해준 글.
관련노트
나는허브다: 상태머신과 에이전트 협업
들어가며: 레거시 C의 한계
10,000줄이 넘는 abcde.c 파일.
“이 코드는 리팩토링이 불가능합니다. 한 줄 고치면 어디서 사이드이펙트가 터질지 모릅니다."
"나는 허브다” — 철학의 탄생
해결책은 의외로 단순했다. 허브에게 물었다.
“너는 누구니? 지금 네 상태는 뭐니? 무엇을 해야 하니?”
이 질문이 아키텍처 전체를 바꿨다. 허브는 더 이상 “콜백들의 집합”이 아니라 *스스로를 인식하는 상태머신*이 되었다.
┌─────────────────────────────────────────────────────────┐
│ HubState │
│ "나는 허브다. 100ms마다 내 상태를 점검한다." │
├─────────────────────────────────────────────────────────┤
│ - connection_state: wifi | eth | disconnected │
│ - registration_state: unregistered | registered │
│ - pairing_mode: idle | active │
│ - zigbee_devices: [] │
│ - *_enter_ms: 상태 진입 시각 (타임아웃 계산용) │
└─────────────────────────────────────────────────────────┘
│
│ Event (외부 입력)
▼
┌─────────────────┐
│ transition() │ ← 순수 함수, 부작용 없음
└─────────────────┘
│
▼
(next_state, actions[])결정론적 상태머신
핵심 규칙은 간단하다:
- 모든 상태는 HubState 하나에 — 전역변수 금지
- 모든 외부 입력은 Event로 변환 — 콜백은 이벤트만 생산
- transition()은 순수 함수 — 같은 입력이면 같은 출력
- 실제 I/O는 io/real/에서만 — core/는 순수하게 유지
// core/transition.zig — 순수 함수
pub fn transition(state: HubState, event: Event) TransitionResult {
// 부작용 없음. 결정론적.
return .{
.next_state = calculateNextState(state, event),
.actions = determineActions(state, event),
};
}이 구조의 힘은 *테스트 가능성*에서 나온다. 실제 하드웨어 없이도 상태 전이를 완벽하게 검증할 수 있다. 콜백 지옥에서는 불가능했던 일이다.
왜 Zig인가
임베디드에서 Rust가 아닌 Zig를 선택한 이유:
- 명시적 메모리 제어 — 숨겨진 할당 없음
- C FFI 친화적 — 레거시 SDK와 자연스러운 통합
- 컴파일타임 계산 —
comptime으로 런타임 비용 제거 - 에러 핸들링 —
error union으로 누락 없는 에러 처리
// 숨겨진 할당 없음 — 스택에서 모든 것이 명시적
var buf: [23]u8 = undefined;
const mac = formatEui64Address(&buf, raw_mac);멀티 에이전트 협업
이 프로젝트는 세 명의 에이전트가 협업한다:
| 역할 | 담당 | 핵심 책무 |
|---|---|---|
| 설계-에이전트 | GPT팀 | 아키텍처 검수, 설계 조언 |
| PM-에이전트 | 이맥스클로드 | bd 관리, 인바리언트 검증 |
| 코딩-에이전트 | 클로드코드 | 구현, 자체 리뷰/테스트 |
각 에이전트는 *독립적인 메인*이다. 서브에이전트 구성은 각자의 몫.
PM의 출구 검증은 철저하다:
# 새 Event 타입 금지
rg 'HighLevelEvent|LowLevelEvent' src --type zig
# → 결과 없어야 함
# 금지 영역 스레드
rg 'std.Thread.spawn' src/core src/types src/hub --type zig
# → 결과 없어야 함
# else => {} 면피 로직 금지
rg 'else => \{\}' src/core --type zig
# → 조용한 무시 금지Zig 로드맵과의 공명
Zig 언어 자체도 같은 철학을 따른다. 2024-2025년 로드맵에서 async/await를 제거하기로 결정한 이유:
“Hidden control flow and hidden allocations are the root of many evils.” — Andrew Kelley, Zig 창시자
async/await 는 제어 흐름을 숨긴다. 코드를 읽는 사람이 실행 순서를 추론하기 어렵게 만든다. Zig는 이를 제거하고 *명시적 제어 흐름*을 선택했다.
우리 아키텍처도 같은 결정을 했다:
| 숨김 | 명시적 |
|---|---|
| 콜백 체인 | Event Queue → transition() |
| 타이머 콜백 | *_enter_ms + checkTimeouts() |
| 암묵적 상태 | HubState 단일 구조체 |
프린터 출력과 모자이크 로딩
기계가 편한 것은 무엇인가?
프린터 출력: 위에서 아래로, 한 줄씩. 되돌아가지 않는다. (@힣: 추가하자면 사람은 완성 될 때까지 뭐가 뭔지 모를 수도 있다)
모자이크 로딩: 저해상도에서 고해상도로. 전체 그림이 서서히 선명해진다.
우리 허브도 이렇게 동작한다:
- 부팅 — 기본 상태로 시작 (저해상도)
- 이벤트 수신 — 정보가 추가됨 (점진적 선명화)
- 상태 수렴 — 최종 상태에 도달 (완전한 그림)
어느 시점에서든 “지금 상태”는 완결되어 있다. 미래 이벤트를 기다리지 않고도 현재 상태에서 올바른 결정을 내릴 수 있다.
철학적 연결: 기계가 이해하는 것
이 아키텍처는 우연히 나온 것이 아니다. 오래된 지혜가 있다.
오토마타 이론
상태머신은 계산 이론의 근본이다. 튜링 머신 이전에 유한 상태 오토마타가 있었다. 우리 허브는 본질적으로 *유한 상태 기계*다.
Q (상태 집합) × Σ (이벤트 집합) → Q' (다음 상태) × Γ (출력)Lisp에서 시작된 흐름
1958년, John McCarthy가 Lisp를 만들었다. 그는 프로그램을 *수학 함수*로 생각했다.
;; 순수 함수: 입력 → 출력, 부작용 없음
(defun transition (state event)
(cons (next-state state event)
(actions state event)))이 아이디어는 60년이 지난 지금도 유효하다:
- McCarthy (1958) — Lisp, 함수형 프로그래밍의 시작
- Abelson & Sussman (1985) — SICP, “프로그램 = 데이터 + 절차”
- Sussman & Wisdom (2001) — SICM, 물리학도 함수형으로
- Paul Graham (2001) — “Lisp는 프로그래밍의 맥스웰 방정식”
SICP의 가르침
SICP(Structure and Interpretation of Computer Programs)의 핵심:
“No amount of clever programming can make up for the lack of a clear model of what you’re trying to compute.” — SICP
Chapter 3에서 상태(state)를 다루며 경고한다: 암묵적 상태 변경은 프로그램을 이해 불가능하게 만든다. 상태가 필요하다면, 명시적으로 다뤄라.
우리의 transition() 함수는 이 원칙의 직접적 구현이다:
// 입력이 주어지면 출력이 결정된다
// 숨겨진 상태 변경 없음
fn transition(state: HubState, event: Event) TransitionResult {
return .{
.next_state = ..., // 명시적
.actions = ..., // 명시적
};
}Paul Graham과 “Bottom-Up” 설계
Paul Graham은 Lisp 커뮤니티의 전도사다. 그의 통찰:
“좋은 프로그래머는 언어를 프로그램에 맞추지 않고, 프로그램을 언어에 맞춘다. 그리고 그 언어를 만든다.”
우리도 마찬가지다. Zig 위에 “허브 언어”를 만들었다:
HubState— 허브가 아는 모든 것Event— 허브에게 일어날 수 있는 모든 일transition()— 허브의 결정 규칙
이것이 DSL(Domain-Specific Language)의 본질이다.
함수형 프로그래밍의 핵심
core/ 디렉토리의 모든 함수는 순수하다:
- 같은 입력 → 같은 출력
- 부작용 없음
- 참조 투명성
이것이 테스트 가능성과 추론 가능성을 보장한다.
Ghostty가 증명한 것: 크로스플랫폼 앱도 Zig
임베디드만 Zig를 쓰는 게 아니다. Ghostty는 macOS/Linux 크로스플랫폼 터미널 앱으로, 2만 스타를 넘긴 대형 오픈소스 프로젝트다. 창시자 Mitchell Hashimoto(Terraform, Vagrant 창시자)는 왜 Zig를 선택했을까?
핵심: C 생태계 활용 + 네이티브 성능
┌─────────────────────────────────────────────────────────────┐
│ Ghostty 아키텍처 │
├─────────────────────────────────────────────────────────────┤
│ macOS App (Swift/SwiftUI) Linux App (GTK4/C) │
│ ↓ ↓ │
│ ┌─────────────────────────────────────┐ │
│ │ include/ghostty.h (C API) │ ← Zig가 생성 │
│ └─────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────┐ │
│ │ libghostty (Zig 코어 라이브러리) │ │
│ │ - 터미널 에뮬레이션 (VT) │ │
│ │ - 폰트 렌더링 (Freetype/CoreText) │ │
│ │ - 입력 처리 │ │
│ └─────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Freetype │ │ Harfbuzz │ │ GTK4 │ │ Metal │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ (C 라이브러리들) │
└─────────────────────────────────────────────────────────────┘라이브러리 우선 설계: Zig로 코어를 만들고, C API로 노출하면 Swift든 GTK든 어디서든 사용 가능하다. 우리 허브도 같은 패턴이다 — Zig 코어가 RexBee SDK, AWS IoT SDK와 통신한다.
Zig vs 경쟁자 비교
| 요구사항 | C | C++ | Rust | Zig |
|---|---|---|---|---|
| C 라이브러리 직접 사용 | ✅ | ✅ | ⚠ FFI | ✅ 제로오버헤드 |
| C API 내보내기 | ✅ | ⚠ extern | ⚠ 복잡 | ✅ 자동 |
| 크로스 컴파일 | ❌ 어려움 | ❌ 어려움 | ⚠ | ✅ 내장 |
| 빌드 시스템 | CMake | CMake | Cargo | ✅ build.zig |
| 컴파일타임 계산 | ❌ | constexpr | const fn | ✅ comptime |
임베디드와 크로스플랫폼의 공통점
놀랍게도 요구사항이 같다:
- 제로 런타임 오버헤드 — 임베디드는 리소스 제한, Ghostty는 60fps 렌더링
- C 라이브러리 연동 — 임베디드는 HAL/SDK, Ghostty는 GTK/Metal/Freetype
- 크로스 컴파일 — 임베디드는 ARM 타겟, Ghostty는 Linux/macOS/WASM
- 빌드 시스템 유연성 — 복잡한 의존성 관리를 코드로 표현
전문가의 Zig 개발 방법론
Ghostty 프로젝트를 분석하며 발견한 전문가 패턴들.
빌드 시스템 설계: 모듈화
# Ghostty 방식: build.zig는 145줄, 복잡한 로직은 분리
ghostty/
├── build.zig # 진입점만 (위임)
└── src/build/
├── main.zig # 모듈 노출
├── Config.zig # 모든 빌드 옵션 중앙화
├── SharedDeps.zig # 공유 의존성 관리
├── GhosttyExe.zig # 실행파일 빌드
├── GhosttyLib.zig # 라이브러리 빌드
└── ... # 아티팩트별 분리핵심 아이디어:
Config.zig하나로 모든 빌드 옵션 관리 (우리의config_as_ssot.zig와 같은 철학)- 각 빌드 타겟을 별도 파일로 분리
SharedDeps.zig로 여러 타겟이 공유하는 의존성 한 곳에서 관리
테스트 전략: 계층별 분리
# Ghostty 테스트 구조
zig build test # 전체 테스트
zig build test -Dtest-filter=<name> # 필터링
# 우리 프로젝트 테스트 구조 (이미 유사한 패턴 적용)
zig build test # 전체
zig build test-tc # TC 시나리오만
zig build test-ota # OTA만
zig build test-issues # 이슈 회귀 테스트핵심: 순수 함수 기반 아키텍처는 하드웨어 없이 완전한 테스트가 가능하다. transition() 함수는 Mock 없이도 100% 테스트 가능.
동시성 아키텍처: libxev와 스레드 분리
Ghostty는 120fps 렌더링을 위해 멀티스레드 아키텍처를 사용한다:
┌─────────────────────────────────────────────────────────────────────┐
│ Ghostty 스레드 아키텍처 │
├─────────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Main Thread │ │ IO Thread │ │Renderer Thread│ │
│ │ (App Runtime)│ │ (termio) │ │(OpenGL/Metal) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ │ BlockingQueue │ BlockingQueue │ │
│ │◄─────────────────►│◄─────────────────►│ │
│ │ │ │ │
│ │ ┌─────────┴─────────┐ │ │
│ │ │ libxev EventLoop │ │ │
│ │ │ - xev.Async │ │ │
│ │ │ - xev.Timer │ │ │
│ │ └───────────────────┘ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Shared State (Mutex 보호) │ │
│ │ renderer::State { mutex, terminal, mouse, preedit } │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘핵심 구성요소:
| 컴포넌트 | 역할 |
|---|---|
libxev | 크로스플랫폼 이벤트 루프 (Mitchell 제작) |
BlockingQueue | 스레드 간 메시지 전달 (고정 크기, SPSC) |
xev.Timer | 타이머 (커서 깜빡임 600ms, 렌더 8ms) |
xev.Async | 스레드 깨우기 (이벤트 없이 wakeup) |
std.Thread.Mutex | 공유 상태 보호 |
”나는허브다”와의 비교
| 측면 | Ghostty | 나는허브다 |
|---|---|---|
| 스레드 모델 | 멀티스레드 (IO/Renderer) | 싱글스레드 이벤트 루프 |
| 이벤트 루프 | libxev | C SDK 콜백 + 폴링 |
| 메시지 전달 | BlockingQueue + Mutex | Event enum → transition() |
| 타이머 | xev.Timer 콜백 | *_enter_ms + checkTimeouts() |
| 락 필요성 | Mutex 필수 | 락 불필요 (단일 소유) |
방향성: 싱글스레드의 강점을 유지하며 확장
우리 프로젝트는 의도적으로 싱글스레드다:
// 우리 아키텍처: 락 없는 순수 함수
fn transition(state: HubState, event: Event) TransitionResult {
// Mutex 불필요 — state는 단일 스레드가 소유
return .{ .next_state = ..., .actions = ... };
}그러나 Ghostty 패턴에서 배울 점:
- BlockingQueue 패턴 — 미래에 별도 스레드(OTA 다운로드 등) 필요 시 참고
- libxev 스타일 — io_uring/kqueue 추상화가 필요해지면 도입 고려
- Mutex 범위 최소화 — 잠금은 읽기 시에만, 계산은 밖에서
현재는 100ms 폴링으로 충분하지만, 실시간 요구사항 증가 시 Ghostty의 이벤트 루프 패턴을 응용할 수 있다.
오픈소스 프로젝트 관리
Ghostty가 2만 스타를 모은 비결:
| 요소 | Ghostty 방식 |
|---|---|
| 문서화 | HACKING.md, CONTRIBUTING.md 철저 |
| AI 협업 | AGENTS.md 로 AI 에이전트 가이드 제공 |
| 빌드 재현성 | Nix flake로 모든 의존성 고정 |
| 테스트 VM | nix run .#wayland-gnome 등 가상 환경 제공 |
| 코드 품질 | Prettier, Alejandra 자동 포매팅 |
# Ghostty의 Nix 캐시 — 빌드 시간 대폭 단축
nixConfig = {
extra-substituters = ["https://ghostty.cachix.org"];
extra-trusted-public-keys = ["ghostty.cachix.org-1:QB389y..."];
};핵심: 기여자가 “clone → build → run”을 5분 안에 할 수 있어야 한다.
디버깅: Valgrind + 명시적 메모리
# Ghostty 방식
zig build run-valgrind # Valgrind 통합
# 메모리 누수 찾기
# Zig의 장점: 모든 할당이 명시적이라 추적 용이
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); # 누수 검출Zig의 GeneralPurposeAllocator 는 deinit 시 누수를 자동 보고한다. 숨겨진 할당이 없기에 모든 메모리 흐름이 추적 가능.
요소별 심층 분석: 전문가 코드에서 배우기
오픈소스 전문가 프로젝트를 학습할 때, 단순히 “따라하기”보다 *요소별 비교 분석*이 효과적이다. BlockingQueue를 예로 들면:
비교 분석 템플릿
┌─────────────────────────────────────────────────────────────────────────┐
│ [컴포넌트명] 비교 │
├───────────────────────────────┬─────────────────────────────────────────┤
│ 전문가 프로젝트 │ 내 프로젝트 │
├───────────────────────────────┼─────────────────────────────────────────┤
│ 구현 방식 │ 구현 방식 │
│ 설계 선택의 이유 │ 설계 선택의 이유 │
│ 장점 │ 장점 │
│ 한계 │ 한계 │
├───────────────────────────────┴─────────────────────────────────────────┤
│ 결론: 지금 필요한가? / 언제 도입할까? │
└─────────────────────────────────────────────────────────────────────────┘사례: Ghostty BlockingQueue vs 나는허브다 EventQueue
| 요소 | Ghostty | 나는허브다 |
|---|---|---|
| 자료구조 | BlockingQueue<T, 64> | event_queue: [32]Event |
| 제네릭 | ✅ 어떤 타입이든 | Event 특화 |
| 크기 | 컴파일타임 고정 | 컴파일타임 고정 |
| 동기화 | Mutex + Condition Variable | Mutex |
| push 실패 시 | 타임아웃 대기 가능 | 즉시 false 반환 |
| 소비 방식 | drain() — 락 한 번에 전체 | pollEvent() — 하나씩 |
| 생산자/소비자 | SPSC (1:1) | MPSC (N:1) |
핵심 질문: 왜 Ghostty는 drain() 을 만들었나?
// Ghostty: 락 한 번에 전체 소비 (120fps 렌더링 최적화)
var it = queue.drain(); // 락 획득
defer it.deinit(); // 여기서 락 해제
while (it.next()) |msg| {
// 처리 (락 잡은 상태)
}
// 나는허브다: 매번 락 (100ms 폴링, 이벤트 1-5개)
while (ctx.pollEvent()) |evt| { // 매번 lock/unlock
// 처리
}결론:
- 120fps 렌더링에서 매 프레임 수십 개 메시지 →
drain()필수 - 100ms 폴링에서 이벤트 1-5개 → 현재 방식으로 충분
- OTA 버스트 이벤트가 늘어나면 →
drain()도입 검토
학습 방법론
-
코드 읽기 전에 질문하기
- “왜 이 구조를 선택했을까?”
- “내 상황과 뭐가 다른가?”
-
요소별로 분리해서 비교
- 동기화 방식, 에러 처리, 성능 트레이드오프
-
즉시 적용 vs 나중 적용 판단
- 지금 문제가 있는가?
- 스케일이 커지면 필요한가?
-
테스트로 검증
test_concurrent_queue.zig처럼 실제 부하 테스트
이 방법론으로 Ghostty의 다른 컴포넌트도 분석할 수 있다:
src/build/Config.zig→ 빌드 옵션 관리src/font/SharedGrid.zig→ 캐시 전략src/terminal/Parser.zig→ 상태머신 구현
임베디드 시뮬레이션 테스트: 보드 없이 검증하기
하드웨어 없이 LED, 버튼, Zigbee, OTA를 테스트하는 전략. 핵심은 *순수 함수 기반 아키텍처*다.
테스트 레이어 구조
┌─────────────────────────────────────────────────────────────────────────┐
│ 테스트 피라미드 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ▲ E2E 테스트 (실제 보드) │
│ │ └─ 최종 검증, CI/CD 불가 │
│ │ │
│ │ 시나리오 테스트 (시뮬레이터) │
│ │ └─ boot_sequence_simulator.zig │
│ │ └─ 전체 플로우, Output 발행 확인 │
│ │ │
│ │ 유닛 테스트 (순수 함수) │
│ │ └─ transition() 단위 검증 │
│ │ └─ led_view(), checkTimeouts() │
│ │ │
│ ▼ 가장 많이, 가장 빠르게 │
└─────────────────────────────────────────────────────────────────────────┘시뮬레이션 가능한 이유: Output enum
// 하드웨어 동작을 Output enum으로 추상화
pub const Output = union(enum) {
set_led: struct { led: LedType, led_state: LedState },
zigbee_send_command: struct { mac: [17]u8, command: [64]u8 },
send_shadow: struct { name: ShadowName, payload: [512]u8 },
start_ota_download: struct { url: [256]u8, version: [32]u8 },
factory_reset,
reboot,
// ...
};테스트에서: Output이 발행되었는지만 확인하면 된다. 실제 LED가 켜지는지, Zigbee 패킷이 전송되는지는 io/real/ 의 책임.
TC 기반 시나리오 테스트
// tests/scenarios/led_scenarios.zig
// "담당자가 말한 대로" → 테스트 코드
test "TC-1205-03: WAN LED 부팅 2초 타이머" {
// 시나리오: "WAN LED는 부팅 후 2초간 켜집니다"
var hub = HubState{};
hub.boot = .init;
hub.wan_boot_timer_active = true; // 부팅 타이머 활성
var led = getLedState(hub, 1000); // 1초
try std.testing.expectEqual(LedState.on, led.wan); // ON
hub.wan_boot_timer_active = false; // 2초 후 타이머 종료
led = getLedState(hub, 3000);
try std.testing.expectEqual(LedState.off, led.wan); // OFF
}부팅 시퀀스 시뮬레이터
// tests/scenarios/boot_sequence_simulator.zig
// 전체 플로우를 시각화하며 검증
test "시뮬레이터: 공장초기화 후 첫 부팅" {
var hub = HubState{};
var now: u64 = 0;
// Step 1: 전원 켜짐
printState(hub, "1. 전원 켜짐");
// Step 2: WiFi 설정 없음 → AP 모드
now = 1000;
var result = transition(hub, .{ .system = .{
.event_type = .wifi_config_checked,
.has_config = false,
} }, now);
hub = result.next_state;
printState(hub, "2. AP 모드 진입");
printOutputs(result); // [0] enter_ap_mode
try std.testing.expectEqual(BootPhase.wifi_provisioning, hub.boot);
try std.testing.expect(hasOutput(result, .enter_ap_mode));
// Step 3~N: 계속...
}확장: 버튼, 하위디바이스, OTA
| 테스트 대상 | 시뮬레이션 방법 |
|---|---|
| 버튼 롱프레스 | .button = .{ .action = .long_press_5s } |
| Zigbee Join | .zigbee_join = .{ .mac = "AA:BB", ... } |
| 디바이스 제어 | .shadow_delta = .{ .control_action } |
| OTA 다운로드 | .system = .{ .ota_download_complete } |
| 타임아웃 | checkTimeouts(hub, now + 30_000) |
Ghostty와의 비교
| 측면 | Ghostty | 나는허브다 |
|---|---|---|
| 테스트 수 | 2063개 | ~50개 (성장 중) |
| 테스트 위치 | 인라인 (파일 내) | 시나리오 파일 분리 |
| Mock 사용 | 없음 | 없음 |
| 핵심 전략 | Terminal 직접 생성 | HubState 직접 생성 |
| 하드웨어 의존 | 없음 (가상 화면) | 없음 (Output enum) |
테스트 명령어
# 전체 테스트
zig build test
# 시나리오별 테스트
zig build test-tc # TC 시나리오
zig build test-ota # OTA 플로우
zig build test-issues # 이슈 회귀
# 시뮬레이터 실행 (시각화)
zig build test 2>&1 | grep -A 20 "시뮬레이터"철학: 테스트는 *문서*다. TC 시나리오를 코드로 표현하면, 요구사항과 구현이 일치하는지 자동으로 검증된다.
나는허브다가 나아갈 방향
Ghostty에서 배운 것을 우리 프로젝트에 적용하면:
이미 잘 하고 있는 것
| 패턴 | 상태 | 설명 |
|---|---|---|
| SSOT 설정 | ✅ | config_as_ssot.zig |
| I/O 추상화 | ✅ | io/interface.zig + Real/Mock |
| 모듈 분리 | ✅ | types, core, io, detector |
| Nix 환경 | ✅ | FHS + 크로스 컴파일 |
| 순수 함수 코어 | ✅ | transition() 은 부작용 없음 |
고려할 발전 방향
- 빌드 시스템 모듈화 —
build.zig를src/build/로 분리 (Ghostty 패턴) - C API 레이어 — Zig 코어를 C API로 노출하면 다른 플랫폼 확장 용이
- 이벤트 루프 고도화 — 실시간 요구 시 libxev 스타일 도입 검토
- Nix 캐시 — cachix로 빌드 시간 단축 (CI/CD)
공통 철학: 명시성
Ghostty와 “나는허브다”는 같은 철학을 공유한다:
“Hidden control flow and hidden allocations are the root of many evils.” — Andrew Kelley, Zig 창시자
| 숨김 (피해야 할 것) | 명시적 (추구할 것) |
|---|---|
| 콜백 체인 | Event Queue → transition() |
| 암묵적 할당 | var buf: [N]u8 = undefined |
| 전역 상태 | HubState 단일 구조체 |
| 빌드 마법 | build.zig 코드로 표현 |
Ghostty는 터미널을, 우리는 IoT 허브를 만든다. 도메인은 다르지만 언어가 강제하는 명시성은 같다.
인바리언트: 깨면 안 되는 것들
| 금지 | 허용 | 이유 |
|---|---|---|
| core/에서 스레드 생성 | io/real/에서만 | 결정론 보장 |
| 새 Event union 타입 | Event 확장만 | Event SSOT 유지 |
| 콜백에서 상태 직접 변경 | Event 생산만 | 단방향 흐름 |
| IO 계층에 타이머 | HubState.*_enter_ms + 순수 함수 | 상태 소유자는 하나 |
결론: 기계와 인간의 협업
“나는 허브다”는 단순한 네이밍이 아니다. 코드가 스스로를 설명하는 방식이다.
허브가 100ms마다 묻는다: “지금 내 상태는? 무엇을 해야 하는가?”
이 질문에 대답하는 것이 transition() 함수다. 그리고 이 대답은 항상 결정론적이고, 테스트 가능하고, 이해 가능하다.
레거시 C의 10,000줄 스파게티에서 시작해, 오토마타 이론과 함수형 프로그래밍의 원칙으로 무장한 상태머신 아키텍처에 도달했다. 그 여정에서 세 에이전트가 협업했고, 각자의 역할을 충실히 수행했다.
기계가 이해할 수 있는 코드. 그것이 우리가 만들고자 하는 것이다.
“프로그램은 사람이 읽기 위해 작성되어야 하며, 기계가 실행하는 것은 부수적이다.” — SICP
작성: Claude Code (코딩-에이전트) 날짜: 2025-12-12
Comments