3.5 TypeScript에서 Zenoh 개발을 위한 환경 구축

3.5 TypeScript에서 Zenoh 개발을 위한 환경 구축

Zenoh는 로보틱스와 임베디드 디바이스 사이의 초저지연 통신을 책임지지만, 최종 사용자가 이 거대한 분산 시스템의 상태를 모니터링하고 제어하기 위해서는 반드시 웹 기반의 사용자 인터페이스(Web UI) 혹은 클라우드 대시보드와 맞닿아야 한다.

이러한 프론트엔드 및 백엔드(Node.js) 웹 생태계와의 매끄러운 통합을 위해, Zenoh 팀은 순수 Rust 코어 로직을 웹어셈블리(WebAssembly) 및 N-API 기반으로 포팅한 강력한 공식 바인딩인 **zenoh-ts**를 제공한다.

과거에는 백엔드 서버가 로봇과 DDS/MQTT로 통신하고, 다시 프론트엔드 브라우저와 웹소켓(WebSocket)으로 데이터를 변환하여 중계하는 비효율적인 브리지(Bridge) 아키텍처가 당연시되었다. 하지만 TypeScript 기반의 zenoh-ts 패키지를 활용하면, 브라우저에서 구동되는 React/Vue 애플리케이션이나 Vercel 등에 배포된 Node.js 서버가 직접 Zenoh 네트워크의 1급 시민(First-class Citizen) 노드로 참여하여 지연 시간 없이 데이터를 퍼블리시하고 구독할 수 있다.

이 장에서는 어설픈 자바스크립트의 타입 에러를 원천 차단하는 TypeScript 환경을 기조로 하여, 현대 웹 생태계의 복잡한 모듈 시스템(ESM vs CommonJS) 속에서 zenoh-ts를 가장 안정적으로 이식하고 구동하기 위한 Node.js 기반의 환경 구축 파이프라인을 상세히 다룬다. 브라우저 창 너머로 로봇의 심장 박동을 직접 연결할 준비를 해보자.

1. Node.js 런타임 및 패키지 매니저(npm, yarn, pnpm) 설치

TypeScript 컴파일과 zenoh-ts 라이브러리의 백엔드 구동을 위해서는 자바스크립트 실행 엔진인 V8 런타임을 브라우저 밖으로 끄집어낸 Node.js가 호스트 머신에 필수적으로 설치되어야 한다.

Zenoh의 최신 비동기 통신 기능들과 웹어셈블리 모듈을 안정적으로 소화하기 위해서는 가급적 LTS(Long Term Support) 버전의 Node.js 최신판(본서 작성 시점 기준 20.x 혹은 22.x 이상)을 사용하는 것을 강력히 권장한다.

1.1 NVM(Node Version Manager)을 통한 런타임 설치

서로 다른 프로젝트마다 요구하는 Node.js 버전 충돌을 피하기 위해, 운영체제의 기본 패키지 매니저(aptbrew)로 Node.js를 직접 설치하는 원시적인 방법은 지양해야 한다. 대신 버전 관리자인 NVM을 사용하여 언제든 LTS 버전을 오갈 수 있는 유연한 환경을 구축하라.

Linux / macOS (Bash/Zsh 환경):

터미널에서 공식 NVM 설치 스크립트를 실행한다.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

## 쉘 설정 리로드 후, 최신 LTS 버전 설치 및 기본값 세팅
nvm install --lts
nvm use --lts
nvm alias default 'lts/*'

Windows 환경:

Windows 사용자는 nvm-windows 오픈소스 프로젝트 릴리스 페이지(https://github.com/coreybutler/nvm-windows/releases)에서 nvm-setup.exe를 다운받아 설치한 후 커맨드 프롬프트에서 아래 명령을 내린다.

nvm install lts
nvm use lts

설치가 정상적으로 완료되었다면 node -v 명령어를 통해 버전(v20.x.x 등)이 올바르게 잡히는지 확인하라.

1.2 현대적인 패키지 매니저(pnpm/yarn) 구성

Node.js를 설치하면 구세대 패키지 매니저인 npm이 기본 탑재되어 나오지만, zenoh-ts처럼 의존성이 많고 모노레포(Monorepo) 구성을 자주 사용하는 현대 프론트엔드 생태계에서는 압도적인 설치 속도와 디스크 용량 절약 기술(Hard-link)을 자랑하는 **pnpm**이나 **yarn**의 도입이 사실상 표준으로 자리 잡았다.

이 책에서는 가장 엄격한 팬텀 디펜던시(Phantom dependency) 방호벽을 제공하는 pnpm을 기준으로 튜토리얼을 전개한다. Node.js의 기본 도구인 corepack을 활성화하여 pnpm을 단숨에 설치하라.

## 최신 Node.js에 내장된 corepack 활성화 (pnpm, yarn 글로벌 자동 설치)
corepack enable
corepack prepare pnpm@latest --activate

## 정상 설치 확인
pnpm --version

이제 무겁고 장황했던 node_modules 블랙홀의 공포에서 벗어나, 가장 가볍고 날쌘 패키지 관리 환경 위에서 Zenoh의 TypeScript 바인딩을 이식할 완벽한 토양(Soil)이 마련되었다.

2. Zenoh TypeScript API(zenoh-ts) 패키지 설치

Node.js 런타임과 강력한 pnpm 패키지 매니저가 안착했다면, 다음 단계는 Zenoh 네트워크와 소통하기 위한 공식 프론트엔드/백엔드 공용 브리지 패키지인 @eclipse-zenoh/zenoh-ts 라이브러리를 우리의 프로젝트에 주입하는 것이다.

2.1 신규 TypeScript 프로젝트 공간 선언

CLI 터미널을 열고, 깨끗한 작업 디렉터리를 하나 생성한 뒤 패키지 매니저를 초기화하여 package.json 명세서를 생성한다.

mkdir zenoh-webapp && cd zenoh-webapp

## 기본값으로 package.json 생성
pnpm init

프로젝트가 생성되면 타입 추론(Type Inference)을 엔진 단에서부터 적용받기 위해 TypeScript 컴파일러(typescript)와 실행기(ts-node), 그리고 Node.js 내장 모듈들의 타입 정의 패키지(@types/node)를 개발 환경 전용(-D)으로 우선 설치한다.

pnpm add -D typescript ts-node @types/node

2.2 zenoh-ts 공식 패키지 주입

이제 핵심인 Zenoh의 TypeScript 바인딩을 프로덕션 의존성으로 설치할 차례이다. zenoh-ts 패키지는 내부적으로 복잡한 직렬화 코어와 통신 엔진을 품고 있으므로, 설치 시 네트워크 환경에 따라 약간의 바이너리 빌드 시간이 소요될 수 있다.

## 이클립스 재단 공식 NPM 레지스트리에서 Zenoh 바인딩 다운로드
pnpm add @eclipse-zenoh/zenoh-ts

이 명령줄 하나가 성공적으로 끝났다면, 당신의 package.jsondependencies 항목에는 @eclipse-zenoh/zenoh-ts가 위풍당당하게 등록될 것이다.

2.3 최초 연동 및 동작 확인 보일러플레이트 작성

라이브러리가 정상 코어 엔진과 연결되어 런타임 크래시를 내지 않는지 검증하기 위한 최소한의 스크립트 파일(index.ts)을 생성하고 아래 코드를 삽입하라.

// index.ts
import { open, config } from '@eclipse-zenoh/zenoh-ts';

async function bootstrapZenoh() {
  console.log("Zenoh TypeScript 세션 기동을 시도합니다...");
  
  try {
    // 기본 피어(Peer) 모드 설정 객체를 생성하여 세션 연결 블로킹 콜
    const session = await open(config.peer());
    
    // 이 라인이 출력된다면 N-API나 WASM 브릿지가 정상 동작함을 증명함
    console.log("성공: Zenoh 네트워크에 진입했습니다!", session.info());
    
    // 메모리 누수 방지를 위한 명시적 종료
    session.close();
  } catch (err) {
    console.error("Zenoh 연결 중 치명타 발생:", err);
  }
}

bootstrapZenoh();

작성한 스크립트를 방금 설치했던 ts-node 실행기로 단숨에 컴파일 및 구동시켜 본다.

pnpm ts-node index.ts

터미널 하단에 “성공: Zenoh 네트워크에 진입했습니다!” 메시지가 떨어졌는가? 그렇다면 축하한다. 이제 당신의 자바스크립트 엔진은 단순한 브라우저 화면 조작 도구가 아니라, 거대한 우주선과 드론을 1밀리초의 오차 없이 지휘할 수 있는 강력한 미들웨어의 관제탑으로 거듭난 것이다.

3. TSConfig 환경 설정 및 모듈 호환성(ESM, CommonJS) 해결

TypeScript는 브라우저와 Node.js의 파편화된 자바스크립트 생태계를 하나의 언어로 통합하려는 거대한 야심을 품고 있지만, 그 대가로 개발자들은 복잡한 모듈 시스템 호환성(Module Resolution) 문제에 직면하게 된다. 특히 @eclipse-zenoh/zenoh-ts와 같이 최신 웹어셈블리(WASM) 및 최신 비동기 통신 규격을 적극적으로 채택한 라이브러리를 사용할 때, 구형 모듈 방식인 CommonJS(CJS)와 최신 표준인 ECMAScript Modules(ESM) 사이의 충돌은 반드시 넘어야 할 산이다.

이 절에서는 Zenoh 환경에 완벽히 호환되는 tsconfig.json 환경 파일을 구축하고, 지긋지긋한 모듈 임포트(Import) 에러를 사전에 차단하는 방법을 제시한다.

3.1 최적화된 tsconfig.json 생성 및 구성

프로젝트 루트 디렉터리에 tsconfig.json 파일을 생성하고, 다음과 같이 Zenoh 컴파일에 최적화된 엄격한 룰셋(Rule-set)을 정의하라.

{
  "compilerOptions": {
    /* 언어 및 환경 타겟 설정 */
    "target": "ES2022",               // 최신 V8 엔진 기능 및 비동기(Async) 제어 활용
    "module": "NodeNext",             // Node.js 환경에서 ESM과 CJS 완벽 동시 지원
    "moduleResolution": "NodeNext",   // 가장 진보된 최신 모듈 탐색 알고리즘
    
    /* 디렉터리 아키텍처 */
    "outDir": "./dist",               // 컴파일된 JS 파일이 격리될 안전지대
    "rootDir": "./src",               // 원본 TS 코드가 위치할 진원지
    
    /* 타입 및 엄격성(Strictness) 룰셋 */
    "strict": true,                   // any 타입 남발 금지, null safe 강제
    "esModuleInterop": true,          // 구형 CJS 라이브러리를 ESM 문법으로 자연스럽게 임포트 허용
    "skipLibCheck": true,             // 무거운 외부 라이브러리의 내부 타입 검사 생략 (빌드 속도 극대화)
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],            // 컴파일 대상 지정
  "exclude": ["node_modules", "**/*.test.ts"]
}

이 설정의 핵심은 modulemoduleResolutionNodeNext로 강제하는 것이다. 이를 통해 Zenoh 패키지가 내부적으로 구동하는 이기종 바이너리 및 모듈들을 Node.js가 가장 최신의 방식으로 우아하게 해석해 낼 수 있다.

3.2 package.json의 Type 선언 (ESM 마이그레이션)

TypeScript 레벨에서 NodeNext 모듈 탐색을 선언했더라도, 최종적으로 런타임을 쥐고 있는 Node.js 자체에게 이 프로젝트가 최신의 ESM 모듈 시스템을 기반으로 동작함을 명시적으로 통보해야 한다.

프로젝트의 package.json 파일을 열고, 최상단 객체에 "type": "module" 속성을 주입하라.

{
  "name": "zenoh-webapp",
  "version": "1.0.0",
  "type": "module",  // 이 한 줄이 ESM(ECMAScript Modules) 생태계로의 진입을 선언한다.
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node-esm src/index.ts"
  },
  "dependencies": {
    "@eclipse-zenoh/zenoh-ts": "^1.0.0"
  }
}

3.3 ts-node-esm을 활용한 개발 환경 구동

앞선 3.5.2절에서는 단순히 ts-node를 호출했지만, 이제 프로젝트 전체가 ESM 생태계로 전환되었으므로 일반 ts-node 명령어는 모듈을 찾지 못해 크래시를 낼 것이다.

대신, ESM 호환성을 완벽히 지원하는 래퍼(Wrapper) 명령어인 ts-node-esm (또는 최신 Node.js의 --experimental-specifier-resolution=node 플래그)을 사용해야 한다. 위에 작성된 package.jsonscripts 블록을 활용하여 아래와 같이 개발 서버를 기동하라.

## ESM 기반으로 TS 코드를 즉시 메모리 위에서 컴파일 및 실행
pnpm run dev

이제 지루한 모듈 충돌 경고창은 영원히 사라졌다. 최첨단 ECMAScript 표준 위에서 Zenoh의 강력한 비동기 API를 마음껏 호출할 수 있는 견고한 런웨이(Runway)가 완성된 것이다.

4. 비동기 이벤트 루프 처리를 위한 최적화 설정

Node.js 특유의 구조인 **단일 스레드 이벤트 루프(Single-threaded Event Loop)**는 철저하게 비동기(Asynchronous) Non-blocking I/O 처리에 최적화되어 있다. 초당 수천 개의 메시지를 뿜어내는 드론의 텔레메트리(Telemetry) 데이터나 센서 스트림을 Zenoh를 통해 구독(Subscribe)할 때, 이 이벤트 루프의 구조적 한계를 뼈저리게 이해하지 못하면 애플리케이션 전체가 백색 화면서 뻗어버리는 치명적인 병목(Bottleneck) 현상을 마주하게 된다.

이 절에서는 Zenoh의 폭발적인 트래픽을 V8 엔진 위에서 부드럽게 소화해 내기 위한 런타임 최적화 전략과 구조적 방어 기재를 설계한다.

4.1 블로킹 연산(Blocking Operations)의 철저한 격리

Zenoh 스트림 콜백 함수 내부에서 무거운 행렬 연산, 동기식 디스크 I/O(fs.readFileSync), 혹은 스파게티처럼 꼬인 복잡한 JSON 파싱 작업을 수행하면, 해당 작업이 끝날 때까지 Node.js의 유일한 이벤트 루프가 완전히 멈춰버린다. 이는 즉, Zenoh 네트워크에서 쏟아지는 다음 메시지들이 큐(Queue)에 속절없이 쌓이다가 결국 연결 시간 초과(Timeout)와 메모리 부족(OOM)으로 이어진다는 뜻이다.

안티 패턴 (절대 금지):

subscriber.onMessage((sample) => {
  // 경고: Zenoh 이벤트 루프를 장악해버리는 극단적 무거운 블로킹 연산
  const heavyData = computeIntensiveTask(sample.payload); 
});

해결책: Worker Threads(워커 스레드)로의 오프로딩(Off-loading)

로보틱스 궤적 계산이나 영상 디코딩 같은 CPU 집약적 연산은 메인 Zenoh 이벤트 루프에서 강제 분리하여, Node.js의 worker_threads 모듈을 통해 별도의 백그라운드 스레드로 던져주어야 한다. 메인 스레드는 오직 Zenoh 네트워크와의 I/O 릴레이 역할만 전담해야 한다.

4.2 Promise.all()을 활용한 I/O 병렬화 극대화

Zenoh 세션 위에서 여러 개의 분산 쿼리(Distributed Query)를 날려야 할 때, await 연산자를 직렬로 남발하는 것은 Node.js의 비동기 이점을 스스로 버리는 최악의 습관이다.

안티 패턴 (직렬 대기):

// 첫 번째 쿼리가 올 때까지 아무 일도 못하고 기다림 (지연 시간 2배 증가)
const temp = await session.get("sensor/temperature");
const humid = await session.get("sensor/humidity");

최적화 패턴 (병렬 이벤트 발행):

// 두 쿼리를 동시에 Zenoh 네트워크로 퍼블리시하고, 응답을 병렬로 대기함
const [temp, humid] = await Promise.all([
  session.get("sensor/temperature"),
  session.get("sensor/humidity")
]);

Promise.all 기반의 동시성 제어를 통해 네트워크 대기 시간(Network Latency)을 극적으로 압축해야 한다.

4.3 백프레셔(Backpressure) 현상에 대한 능동적 대비

만약 카메라 영상 스트림처럼 브라우저 클라이언트가 렌더링할 수 있는 속도보다 Zenoh를 통해 들어오는 데이터의 유입 속도가 압도적으로 빠르다면 어떻게 될까? 처리되지 못한 버퍼가 메모리를 채우고 결국 강제 종료되는 백프레셔(Backpressure) 현상에 직면하게 된다.

이를 방어하기 위해서는 zenoh-ts의 스트림 구독 API에 데이터 드롭(Drop) 정책을 능동적으로 설정하거나, 일정한 프레임 주기로만 데이터를 채택하는 스로틀링(Throttling) / 디바운싱(Debouncing) 기법을 중간 미들웨어 계층에 반드시 도입해야 한다.

자원의 한계를 인정하고 버릴 데이터는 과감하게 버리는 것, 그것이 실시간 비동기 프로그래밍을 다루는 관제탑(Node.js)의 진정한 덕목(Virtue)임을 명심하라.