8.4.4 다양한 타입의 데이터 페이로드(Payload) 전송 기법

8.4.4 다양한 타입의 데이터 페이로드(Payload) 전송 기법

TS 바인딩에서 에러가 제일 많이 나는 곳이 바로 payload 파라미터다!
C/Rust와 달리 JS 세계에서는 바이너리(ArrayBuffer)와 텍스트(String)를 엄격히 구별한다. 하지만 Zenoh에게 당신의 데이터는 무조건 순백의 바이트 덩어리(Uint8Array/Buffer) 로 보여야 한다.

이 폴더에서는 프론트/백엔드 개발자가 가장 자주 마주하는 세 가지 데이터 형태(원시 String, 거대한 JSON 구조체, 무손실 바이너리)를 완벽하게 직렬화/역직렬화하여 라우터 그물에 던지는 테크닉과 런북을 개별 MD 파일로 쪼개어 서술한다.

1. 원시 문자열(String) 및 숫자 처리

간단한 제어 명령(“STOP”, “START”)이나 숫자 하나 정밀하게 보낼 때, 이걸 JSON으로 말고 거대한 오브젝트로 만들어 쏘는 건 대역폭 낭비다.

1.0.1 [Runbook] 심플 텍스트 인코딩 전술

브라우저 내장(혹은 Node 내장) 모듈인 TextEncoderTextDecoder 만 쓰면 완벽히 호환된다.

// --- 송신부 (퍼블리셔 혹은 쿼리 리플라이) ---
const simpleCommand = "EMERGENCY_HALT";

// 1. 단순 문자를 UTF-8 바이너리 배열로 압축 해체
const encoder = new TextEncoder();
const payloadBytes = encoder.encode(simpleCommand);

// 2. 발사
pub.put(payloadBytes); 
// --- 수신부 (서브스크라이버 콜백) ---
const decoder = new TextDecoder("utf-8");

const callback = (sample: zenoh.Sample) => {
    // 1. 수신된 바이너리 덩어리(sample.payload)를 다시 V8 문자열 체계로 환원
    const rawString = decoder.decode(sample.payload);
    
    if (rawString === "EMERGENCY_HALT") {
        console.error("긴급 제어 명령 수신!");
    } else {
        // 만약 숫자(ex: "98.5")가 날아왔다면 문자열로 받은 뒤 Float로 캐스팅한다.
        const batteryLvl = parseFloat(rawString);
        console.log(`Battery: ${batteryLvl}%`);
    }
};

가장 빠르고 가볍다. 다만 C++ 노드와 통신할 경우, C++ 쪽에서 이 문자열을 받을 때 끝에 \0 (널 종료 문자)가 포함되어 있는지 여부를 조심해야 한다. TS TextEncoder는 순수 문자열만 바이트로 만들며 널 문자를 자동 삽입하지 않는다.

2. 복잡한 JSON 객체의 직렬화 및 역직렬화

사실 TypeScript 개발자가 가장 많이 쓸 수법은 이것이다. 웹 서버 백엔드, Redux 스토어, MongoDB 도큐먼트 등등 모든 것이 딕셔너리({}) 모델로 짜여 있으니까.
여기에 TypeScript 고유의 제네릭(Generic) 인터페이스를 더해, 패킷에 완벽한 “마법의 제약“을 건다.

2.0.1 [Runbook] TS 타입 세이프(Type-Safe) JSON 패킹 전술

import * as zenoh from "@eclipse-zenoh/zenoh";

// 1. 완벽한 명세서(Interface) 작성 (에러의 근원 차단)
interface DroneTelemetry {
    droneId: string;
    lat: number;
    lng: number;
    armed: boolean;
    metadata: Record<string, any>;
}

// 송수신용 공통 인코더/디코더 장착
const encoder = new TextEncoder();
const decoder = new TextDecoder();

// --- 쏘는 놈 (Publisher) ---
async function broadcastTelemetry(pub: zenoh.Publisher, data: DroneTelemetry) {
    // TS 가 data 객체의 구조를 완벽히 검증하고, JSON 바이트로 구워버린다.
    const jsonStr = JSON.stringify(data);
    await pub.put(encoder.encode(jsonStr));
    console.log("JSON 객체 발사");
}

// --- 받는 놈 (Subscriber) ---
async function listenTelemetry(session: zenoh.Session) {
    await session.declareSubscriber("drone/*/telemetry", (sample) => {
        try {
            // 1. 바이트를 문자로 풀고,
            const rawJson = decoder.decode(sample.payload);
            
            // 2. JSON.parse 객체를 DroneTelemetry 타입이라고 "단언(Cast)"한다!
            const telemetryData = JSON.parse(rawJson) as DroneTelemetry;
            
            // 이 순간부터 에디터(VSCode)는 telemetryData.lat 에 자동 완성을 지원해준다.
            if (telemetryData.armed) {
                console.log(`무장 상태 드론: ${telemetryData.lat}, ${telemetryData.lng}`);
            }
        } catch (e) {
            // 만약 C++ 드론이 쓰레기값을 보내서 JSON 파싱이 터져도 
            // TS 서버 전체가 죽지 않도록 방어막 전개
            console.error("파싱 불가능한 더미 JSON 패킷 수신됨", e);
        }
    });
}

이 방법은 웹 개발자들에게 최고의 환희(생산성)를 안겨주지만, 데이터 한 건당 중괄호 배열과 딕셔너리 키 문자열들이 통신 용량(Overhead)으로 추가된다는 점을 잊지 말라. (1초에 1천 건 이상 쏠 때는 8.4.4.3 장으로 넘어가야 한다.)

3. Uint8Array를 활용한 고속 바이너리 데이터 스트리밍

카메라 센서의 이미지 프레임 스트림, 압축된 .tar.gz 펌웨어 바이너리 조각, 혹은 C++ 들이 던진 팩 구조체(Packed Struct)를 주고받을 때 텍스트나 JSON은 절대 금물이다. 메모리 복사와 인코딩 지연이 발생한다.
TypeScript(JS)의 뷰어인 DataViewUint8Array를 활용해 하부 메모리를 직접 칼질하는 기법이다.

3.0.1 [Runbook] 영거리 V8 메모리 투척 전술

// --- C++ 측이 보낸 바이너리 예시 ---
// float x (4 byte), float y (4 byte) = 총 8 byte 패킷을 생으로 쐈다고 치자.

const callback = (sample: zenoh.Sample) => {
    // sample.payload 는 이미 TS 환경에서 Uint8Array 계열의 객체다.
    const buffer = sample.payload.buffer; // 밑바닥 ArrayBuffer 덩어리를 꺼냄!
    const byteOffset = sample.payload.byteOffset;
    const byteLength = sample.payload.byteLength;

    // DataView 를 씌워서, 이 바이트 덩어리를 "C언어 포인터"처럼 쪼갠다!
    // JSON 파싱이고 나발이고 없이, 메모리 읽기 한 줄로 끝나는 극강의 퍼포먼스.
    const dv = new DataView(buffer, byteOffset, byteLength);

    // 첫 0번지부터 4바이트 긁어서 실수(Float)로 만듦. (C++ 메모리가 리틀 엔디안이라 가정)
    const x = dv.getFloat32(0, true); 
    // 그 다음 4번지부터 4바이트 긁음.
    const y = dv.getFloat32(4, true); 

    console.log(`[초고속 파싱 완료] X: ${x.toFixed(2)}, Y: ${y.toFixed(2)}`);
};

// --- 이번엔 반대로 프론트엔드가 생으로 쏠 때 ---
function sendFastBinary(pub: zenoh.Publisher) {
    // 정확히 8바이트 메모리 공간을 V8 힙에 파냄
    const buffer = new ArrayBuffer(8);
    const dv = new DataView(buffer);
    
    dv.setFloat32(0, 100.5, true);  // x
    dv.setFloat32(4, -80.2, true);  // y
    
    // 이 8바이트 껍데기만 포장해서 발사! (JSON 변환 오버헤드 0%)
    pub.put(new Uint8Array(buffer));
}

이 기술을 응용하면 브라우저의 HTML <canvas> 에서 그리는 화면 이미지용 Uint8ClippedArray(픽셀 데이터)를 Zenoh망 너머의 클라우드로 프레임 지연 없이 통째로 쏴버리는 “엣지 - 클라우드 브라우저 화면 동기화 파이프라인” 도 쉽게 그려낼 수 있다.