히스토리

  • [2025-12-19 Fri] 임베디드 시뮬레이션 테스트 전략 추가 (테스트 피라미드, Output enum 추상화)
  • [2025-12-19 Fri] 요소별 심층 분석 방법론 추가 (BlockingQueue 사례, 학습 템플릿)
  • [2025-12-19 Fri] Ghostty 동시성 아키텍처 비교 추가 (libxev, BlockingQueue, Mutex 패턴)
  • [2025-12-19 Fri] Ghostty 분석 추가: 크로스플랫폼 앱의 Zig 선택 이유, 전문가 개발 방법론
  • [2025-12-13 Sat 01:26] 줄줄이 떠들었다. 그리고 작성해준 글.

관련노트

나는허브다: 상태머신과 에이전트 협업

들어가며: 레거시 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[])

결정론적 상태머신

핵심 규칙은 간단하다:

  1. 모든 상태는 HubState 하나에 — 전역변수 금지
  2. 모든 외부 입력은 Event로 변환 — 콜백은 이벤트만 생산
  3. transition()은 순수 함수 — 같은 입력이면 같은 출력
  4. 실제 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를 선택한 이유:

  1. 명시적 메모리 제어 — 숨겨진 할당 없음
  2. C FFI 친화적 — 레거시 SDK와 자연스러운 통합
  3. 컴파일타임 계산comptime 으로 런타임 비용 제거
  4. 에러 핸들링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 단일 구조체

프린터 출력과 모자이크 로딩

기계가 편한 것은 무엇인가?

프린터 출력: 위에서 아래로, 한 줄씩. 되돌아가지 않는다. (@힣: 추가하자면 사람은 완성 될 때까지 뭐가 뭔지 모를 수도 있다)

모자이크 로딩: 저해상도에서 고해상도로. 전체 그림이 서서히 선명해진다.

우리 허브도 이렇게 동작한다:

  1. 부팅 — 기본 상태로 시작 (저해상도)
  2. 이벤트 수신 — 정보가 추가됨 (점진적 선명화)
  3. 상태 수렴 — 최종 상태에 도달 (완전한 그림)

어느 시점에서든 “지금 상태”는 완결되어 있다. 미래 이벤트를 기다리지 않고도 현재 상태에서 올바른 결정을 내릴 수 있다.

철학적 연결: 기계가 이해하는 것

이 아키텍처는 우연히 나온 것이 아니다. 오래된 지혜가 있다.

오토마타 이론

상태머신은 계산 이론의 근본이다. 튜링 머신 이전에 유한 상태 오토마타가 있었다. 우리 허브는 본질적으로 *유한 상태 기계*다.

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 경쟁자 비교

요구사항CC++RustZig
C 라이브러리 직접 사용⚠ FFI✅ 제로오버헤드
C API 내보내기⚠ extern⚠ 복잡✅ 자동
크로스 컴파일❌ 어려움❌ 어려움✅ 내장
빌드 시스템CMakeCMakeCargo✅ build.zig
컴파일타임 계산constexprconst fn✅ comptime

임베디드와 크로스플랫폼의 공통점

놀랍게도 요구사항이 같다:

  1. 제로 런타임 오버헤드 — 임베디드는 리소스 제한, Ghostty는 60fps 렌더링
  2. C 라이브러리 연동 — 임베디드는 HAL/SDK, Ghostty는 GTK/Metal/Freetype
  3. 크로스 컴파일 — 임베디드는 ARM 타겟, Ghostty는 Linux/macOS/WASM
  4. 빌드 시스템 유연성 — 복잡한 의존성 관리를 코드로 표현

전문가의 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)싱글스레드 이벤트 루프
이벤트 루프libxevC SDK 콜백 + 폴링
메시지 전달BlockingQueue + MutexEvent enum → transition()
타이머xev.Timer 콜백*_enter_ms + checkTimeouts()
락 필요성Mutex 필수락 불필요 (단일 소유)
방향성: 싱글스레드의 강점을 유지하며 확장

우리 프로젝트는 의도적으로 싱글스레드다:

// 우리 아키텍처: 락 없는 순수 함수
fn transition(state: HubState, event: Event) TransitionResult {
    // Mutex 불필요 — state는 단일 스레드가 소유
    return .{ .next_state = ..., .actions = ... };
}

그러나 Ghostty 패턴에서 배울 점:

  1. BlockingQueue 패턴 — 미래에 별도 스레드(OTA 다운로드 등) 필요 시 참고
  2. libxev 스타일 — io_uring/kqueue 추상화가 필요해지면 도입 고려
  3. Mutex 범위 최소화 — 잠금은 읽기 시에만, 계산은 밖에서

현재는 100ms 폴링으로 충분하지만, 실시간 요구사항 증가 시 Ghostty의 이벤트 루프 패턴을 응용할 수 있다.

오픈소스 프로젝트 관리

Ghostty가 2만 스타를 모은 비결:

요소Ghostty 방식
문서화HACKING.md, CONTRIBUTING.md 철저
AI 협업AGENTS.md 로 AI 에이전트 가이드 제공
빌드 재현성Nix flake로 모든 의존성 고정
테스트 VMnix 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 VariableMutex
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() 도입 검토
학습 방법론
  1. 코드 읽기 전에 질문하기

    • “왜 이 구조를 선택했을까?”
    • “내 상황과 뭐가 다른가?”
  2. 요소별로 분리해서 비교

    • 동기화 방식, 에러 처리, 성능 트레이드오프
  3. 즉시 적용 vs 나중 적용 판단

    • 지금 문제가 있는가?
    • 스케일이 커지면 필요한가?
  4. 테스트로 검증

    • 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() 은 부작용 없음

고려할 발전 방향

  1. 빌드 시스템 모듈화build.zigsrc/build/ 로 분리 (Ghostty 패턴)
  2. C API 레이어 — Zig 코어를 C API로 노출하면 다른 플랫폼 확장 용이
  3. 이벤트 루프 고도화 — 실시간 요구 시 libxev 스타일 도입 검토
  4. 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