콘텐츠로 이동

TypeBox로 관리하는 프로토콜의 단일 진실 공급원 (Single Source of Truth)

서버와 클라이언트 사이의 통신 규약을 관리하다 보면 금세 머리가 아파지곤 해요. 서버의 데이터 구조를 바꿨는데 클라이언트 타입을 업데이트하는 걸 깜빡하거나, 문서와 실제 API 응답이 달라서 디버깅에 시간을 허비하는 일은 개발자라면 누구나 겪는 고통이죠.

이런 문제를 해결하려면 프로토콜을 정의하는 단 한 곳의 ‘단일 진실 공급원(Source of Truth)‘이 필요합니다. 우리는 TypeBox를 사용해 Gateway WebSocket 프로토콜을 정의하고, 이를 기반으로 런타임 검증, JSON Schema 추출, Swift 코드 생성까지 한 번에 처리하고 있어요.

  • Node.js 및 pnpm 환경
  • TypeScript 및 TypeBox에 대한 기본적인 이해
  • Gateway 아키텍처에 대한 배경 지식

Gateway WebSocket 메시지는 세 가지 프레임 중 하나로 구성됩니다. 5분 안에 구조를 파악해 보세요.

  • Request: { type: "req", id, method, params }
  • Response: { type: "res", id, ok, payload | error }
  • Event: { type: "event", event, payload, seq?, stateVersion? }

가장 중요한 규칙은 첫 번째 프레임이 반드시 connect 요청이어야 한다는 거예요. 그 이후에는 자유롭게 메서드를 호출하거나 이벤트를 구독할 수 있습니다.

연결 후 health 체크를 수행하는 가장 간단한 흐름입니다.

import { WebSocket } from "ws";
const ws = new WebSocket("ws://127.0.0.1:18789");
ws.on("open", () => {
ws.send(
JSON.stringify({
type: "req",
id: "c1",
method: "connect",
params: {
minProtocol: 3,
maxProtocol: 3,
client: {
id: "cli",
displayName: "example",
version: "dev",
platform: "node",
mode: "cli",
},
},
}),
);
});
ws.on("message", (data) => {
const msg = JSON.parse(String(data));
if (msg.type === "res" && msg.id === "c1" && msg.ok) {
ws.send(JSON.stringify({ type: "req", id: "h1", method: "health" }));
}
if (msg.type === "res" && msg.id === "h1") {
console.log("health:", msg.payload);
ws.close();
}
});

스키마 관리 및 실행 파이프라인

섹션 제목: “스키마 관리 및 실행 파이프라인”

모든 스키마 정의는 src/gateway/protocol/schema.ts에서 시작됩니다. 여기서 정의된 스키마는 다음과 같은 파이프라인을 거쳐 활용돼요.

  1. pnpm protocol:gen: JSON Schema(draft-07)를 생성하여 dist/protocol.schema.json에 저장합니다.
  2. pnpm protocol:gen:swift: macOS 앱에서 사용할 Swift 모델을 생성합니다.
  3. pnpm protocol:check: 두 생성기를 모두 실행하고 결과물이 최신 상태로 커밋되었는지 확인합니다.

런타임에서는 AJV를 통해 모든 인바운드 프레임을 검증합니다. 특히 서버 핸드셰이크 시 connect 요청의 파라미터가 ConnectParams와 일치하는지 엄격하게 확인하죠.

system.echo라는 새로운 요청 메서드를 추가하는 과정을 단계별로 살펴볼게요.

src/gateway/protocol/schema.ts 파일에 요청 파라미터와 결과 스키마를 추가합니다.

export const SystemEchoParamsSchema = Type.Object(
{ text: NonEmptyString },
{ additionalProperties: false },
);
export const SystemEchoResultSchema = Type.Object(
{ ok: Type.Boolean(), text: NonEmptyString },
{ additionalProperties: false },
);
// ProtocolSchemas에 등록하고 타입을 export 하세요

src/gateway/protocol/index.ts에서 AJV 검증기를 export 합니다.

export const validateSystemEchoParams = ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);

src/gateway/server-methods/system.ts에 실제 동작을 작성합니다.

export const systemHandlers: GatewayRequestHandlers = {
"system.echo": ({ params, respond }) => {
const text = String(params.text ?? "");
respond(true, { ok: true, text });
},
};

마지막으로 pnpm protocol:check를 실행해 변경 사항을 반영하고 Swift 모델을 재생성하면 끝입니다!

  • 프로토콜 버전 불일치: 클라이언트가 보낸 minProtocol과 maxProtocol 범위 안에 서버 버전이 없으면 연결이 거부됩니다.
  • 알 수 없는 프레임: Swift 모델은 하위 호환성을 위해 알 수 없는 프레임 타입을 unknown 케이스로 유지하며 raw payload를 보존합니다.

더 자세한 설정이나 구현 방법이 궁금하다면 AI Setup Assistant에게 질문해 보세요!

OpenClaw

OpenClaw Expert

아직 막혀 있나요?

이 문서에서 답을 못 찾았다면 OpenClaw Expert에게 바로 물어보세요.