8.3 연결 및 세션(Session) 라이프사이클 제어
“인터넷은 언제나 당신을 배반한다.”
백엔드 서버든, 클라이언트의 크롬 브라우저든, 어플리케이션이 켜짐과 동시에 가장 먼저 맞닥뜨리는 시련은 ’분산망 접속(Connection)’이다. 특히 모바일 환경이나 클라우드 컨테이너에서 돌아가는 TypeScript 앱은 5초에 한 번씩 네트워크 인터페이스가 끊기거나 IP가 변동되는 환경(DHCP)에 노출된다.
이 챕터에서는 로보틱스와 웹 생태계를 가르는 핵심인 TCP와 WebSocket의 차이를 꿰뚫어 보고, Config 객체 하나로 오토 리커넥트(Auto Reconnect)와 TLS 암호화 장벽을 세우는 방법을 배운다. 그리고 Node.js의 메모리를 갉아먹는 유령 세션(Ghost Session)을 완벽하게 처형(Close)하는 라이프사이클(Lifecycle) 관리의 극단을 서술한다.
1. Zenoh 라우터 및 피어 연결 수립 (WebSocket 및 TCP 전송 계층)
어디서 구동되는가에 따라 찌르는 “창(Transport Layer)“의 종류가 달라야 한다. 서버 백엔드라면 TCP 단검을, 웹 브라우저라면 WebSocket 우회로를 타야 한다.
1.0.1 [Runbook] 런타임별 네트워크 인젝션(Injection) 전술
전술 1. Node.js 네이티브 TCP 연결 (가장 빠름)
- 환경: 백엔드 Uvicorn, NestJS 서버 내부 등
- 방식: 가장 가까운 스웜(Swarm) 라우터를 P2P 혹은 직접 IP 타겟팅으로 뚫어버린다.
import * as zenoh from "@eclipse-zenoh/zenoh";
async function connectTcp() {
const conf = zenoh.Config.default();
// [하드코어 타겟팅] 클라우드에 떠 있는 중앙 라우터의 TCP 7447 포트로 직행한다.
// 멀티캐스트 스카우팅을 꺼버리고 다이렉트 핑을 쏘는 명령어다.
conf.insertJson5("connect/endpoints", '["tcp/10.0.0.5:7447"]');
const session = await zenoh.open(conf);
console.log("TCP 네이티브 터널 개통 완료");
return session;
}
전술 2. 웹 브라우저 (React/Vue) WebSocket 터널링 연결
- 환경: 구글 크롬, 사파리 등 샌드박스 내부
- 방식:
[tcp/...]가 아니라, 라우터가 열어놓은 8000번 WebSocket 포트로 우회한다. - 선행조건: 로컬망의 Zenoh Router가
zenohd -e "tcp/0.0.0.0:7447" -e "ws/0.0.0.0:8000"처럼 WS 채널을 열어두고 있어야 한다.
import * as zenoh from "@eclipse-zenoh/zenoh";
async function connectBrowserWs() {
const conf = zenoh.Config.default();
// 브라우저에서는 무조건 클라이언트 모드('client')로 강제해야 한다.
conf.insertJson5("mode", '"client"');
// WS 프로토콜을 명시하여 브라우저의 WebSocket API를 사용하게 만든다.
conf.insertJson5("connect/endpoints", '["ws/172.16.0.5:8000"]');
const session = await zenoh.open(conf);
console.log("WebSocket 브라우저 터널 개통 완료");
return session;
}
이 두 가지 길을 프론트엔드/백엔드 아키텍처 환경 변수(Environment Variables)에서 치밀하게 분기시켜야만 “로컬 머신에서는 잘 되는데 배포하면 터져요!” 라는 비명을 막을 수 있다.
2. Config 객체를 활용한 세션 초기화 및 타임아웃 설정
zenoh.Config 객체는 단순한 문자열 모음집이 아니라, C/Rust 코어가 메모리를 어떻게 할당하고 소켓을 언제 포기할지 결정하는 ’생명 연장 장치’다. 서버와 달리, 무선 통신망에 널려 있는 로봇/IoT 단말을 제어할 때 기본 커넥션 타임아웃만 믿고 있다가는 앱이 멈춰버리기 일쑤다.
2.0.1 [Runbook] 극한 생존 환경의 타임아웃 튜닝
Zenoh는 모든 설정을 JSON5 구문으로 파싱해서 받아들인다.
import * as zenoh from "@eclipse-zenoh/zenoh";
async function bootOptimizedSession() {
const conf = zenoh.Config.default();
// 1. [접속 실패 타임아웃] 라우터가 안 켜져 있을 때 무한 대기하는 현상(블로킹) 방어
// 라우터 연결 시도 후 3초(3000ms) 내에 연결이 안 되면 즉각 throw Error 시킨다!
// -> 이 덕분에 프론트엔드 React 화면에 즉각 "로봇과 연결 중단됨!" 팝업을 띄울 수 있다.
conf.insertJson5("connect/timeout", "3000"); // 기본값 10,000 이상
// 2. [TCP 노딜레이(Nagle 비활성)]
// 아주 작은 센서 데이터(8byte)를 초당 10번씩 쏠 때 패킷 병목을 없애기 위해
// OS에게 "버퍼에 모았다 쏘지 말고 바로바로 전송해!" 라고 명령. (로보틱스 필수옵션)
conf.insertJson5("transport/tcp/nodelay", "true");
// 3. [공유 메모리 비활성화]
// 만약 Node.js 가 Windows 나 Mac 리눅스 서브시스템(WSL)에서 개발 모드로 돈다면
// /dev/shm 제로 카피 기능이 크래시를 일으킬 수 있다. 개발망에선 명시적으로 꺼버린다.
conf.insertJson5("transport/shared_memory/enabled", "false");
try {
const session = await zenoh.open(conf);
console.log("최적화 세션 부팅 (튜닝 완료)");
return session;
} catch (error) {
console.error("⛔ 타임아웃 내에 라우터를 찾지 못함. 백오프(Backoff) 시작 요망.");
throw error;
}
}
모든 앱의 생명(Session)은 Config 의 투명한 통제 아래 놓여 있어야만, 개발망과 프로덕션망 간의 아키텍처 붕괴를 막을 수 있다.
3. 비동기 이벤트 루프 기반의 연결 상태 모니터링
웹 앱을 띄웠는데 네트워크가 1분간 살짝 죽었다 살아났다 치자.
가장 하급(Junior) 개발자는 “연결할 수 없습니다” 에러 코드를 확인한 후 브라우저 화면 전체를 F5 로 새로고침 시켜버린다.
최상급(Senior) 엔지니어는 Zenoh 연결 상태를 백그라운드로 관찰하다가 끊어졌을 때는 “오프라인 모드” 배지를 띄우고, 재연결되면 다시 초록불(Live)로 슬쩍 바꿀 뿐 사용자의 스크롤 위치를 0.1픽셀도 건드리지 않는다.
과거 C++ 바인딩에서는 직접 스레드를 걸어 핑(Ping)을 때려야 했지만, 최신 TypeScript 환경에서는 이벤트 드리븐 아키텍처에 맞물려 있다. (다만 언어 바인딩의 버전에 따라 Node 상태 모니터링은 Liveliness 토큰을 응용하여 구현해야 하는 경우가 많다). 가장 보편적이고 확실한 방법은 주기적인 Heartbeat Query 구조를 TS 단에서 가로채(Hooking) UI 상태를 결정하는 것이다. (관련 자세한 기법은 8.6절 Liveliness 에서 상세히 다룬다.)
4. 네트워크 단절 시 자동 재연결(Auto-reconnect) 로직 구현
프론트엔드나 클라우드 워커에서 5분마다 session.close() 에러가 난다고 치자.
TypeScript의 비동기 루프 안에서 가장 쉽게, 그리고 강력하게 세션을 물고 늘어지는 지수 백오프(Exponential Backoff) 불사조 전술이다. 앱을 죽이지 않고, 뒤에서 묵묵히 통로를 다시 뚫어낸다.
4.0.1 [Runbook] 예외 격리 및 불사조 재부팅 팩토리
import * as zenoh from "@eclipse-zenoh/zenoh";
class ZenohConnectionManager {
private session: zenoh.Session | null = null;
private isQuitting = false;
async connectWithRetry(endpoints: string) {
let attempt = 1;
while (!this.session && !this.isQuitting) {
try {
const conf = zenoh.Config.default();
conf.insertJson5("mode", '"client"');
conf.insertJson5("connect/endpoints", `["${endpoints}"]`);
conf.insertJson5("connect/timeout", "2000"); // 2초 타임아웃 꽉 잡기
console.log(`[연결 시도 ${attempt}] 라우터 탐색 중...`);
this.session = await zenoh.open(conf);
console.log("✅ Zenoh 클러스터 진입 성공!");
// 연결이 성공하면 여기에 각종 Publisher, Subscriber 들을
// 일제히 다시 선언(Declare)해주는 콜백을 연결시킨다.
this.onConnected();
} catch (err) {
// 뒤로 갈수록 대기 시간이 기하급수적으로 길어지는 백오프 전술
const delayMs = Math.min(1000 * Math.pow(1.5, attempt), 30000); // 최대 30초
console.warn(`❌ 접속 실패. ${delayMs/1000}초 뒤 재도전...`);
// 이벤트 루프(JS)를 잠시 쉰다
await new Promise(res => setTimeout(res, delayMs));
attempt++;
}
}
}
private onConnected() {
// ... Pub/Sub 등록
}
async shutdown() {
this.isQuitting = true;
if (this.session) {
await this.session.close();
console.log("세션 파괴 완료");
}
}
}
이 클래스를 인스턴스화시켜 두면 라우터 전원이 뽑혔을 때 에러를 뿜으며 React 앱이나 Node 인스턴스가 박살 나는 일은 절대 일어나지 않는다. “통신 장애“를 “일시적 상태“로 격하시키는 것이 프론트엔드 생태계의 최고선이다.
5. TLS/SSL 인증서 적용 및 인증(Authentication) 처리
군사용 드론망이나 메디컬 대시보드의 데이터가 TCP 7447 포트로 평문(Plaintext)으로 날아다닌다는 건 보안팀의 분노를 사기에 충분하다.
하지만 웹 개발자가 복잡한 VPN을 구축할 필요가 없다. Zenoh는 Config 차원에서 TLS 핸드셰이크(Handshake)와 전자 서명 인증 구조를 완벽하게 플러그인화 해두었다. (클레임 기반)
5.0.1 [Runbook] 암호화 터널(WSS / TLS) 구축 전술
import * as zenoh from "@eclipse-zenoh/zenoh";
import * as fs from "fs";
async function secureConnect() {
const conf = zenoh.Config.default();
// 1. 프로토콜을 tls 혹은 브라우저의 경우 wss 로 잡는다.
// tls = 네이티브 보안 소켓, wss = 보안 웹소켓
conf.insertJson5("connect/endpoints", '["tls/10.1.1.50:7447"]');
// 2. [Node.js 백엔드 전용] 인증서 체인 및 비밀키 등록
// CA(인증기관)의 공개키를 밀어넣어서 상대 라우터가 진짜 호스트인지 검사
const rootCaPem = fs.readFileSync("/etc/certs/root_ca.pem", "utf8");
conf.insertJson5("transport/tls/root_ca_certificate", JSON.stringify(rootCaPem));
// 이 노드(나 자신)의 신원을 증명하기 위한 인증서와 비공개 키 삽입
const clientCert = fs.readFileSync("/etc/certs/my_client.crt", "utf8");
const privateKey = fs.readFileSync("/etc/certs/my_client.key", "utf8");
conf.insertJson5("transport/tls/certificate", JSON.stringify(clientCert));
conf.insertJson5("transport/tls/private_key", JSON.stringify(privateKey));
// 브라우저의 경우 (WSS), WSS 커넥션 단에서 시스템(브라우저)에 내장된 인증서를
// 자동으로 신뢰하므로 이런 복잡한 PEM 하드코딩 과정이 대폭 생략된다.
try {
const session = await zenoh.open(conf);
console.log("🔒 무적의 암호화(TLS) 터널 진입 성공");
return session;
} catch (e) {
console.error("🔒 인증서 검증 실패로 접속 거부됨!", e);
throw e;
}
}
이 방식을 쓰면, AWS나 Azure에 올려둔 Zenoh 라우터에 악의적인 해커 포트스캔이 들어오더라도 인증서 서명이 다르면 커널단(TLS)에서 트래픽을 즉각 폐기처분(Drop) 시켜버린다. 인프라의 가장 바깥쪽 그물을 단 10줄의 TypeScript로 짜낸 것이다.
6. 세션의 안전한 종료(Close)와 메모리 누수 방지
프론트엔드 개발자들이 가장 많이 저지르는 죄악이다.
React 훅스(useEffect) 쪽에서 Zenoh 접속 코드를 띄운 뒤, 사용자가 다른 페이지(라우터)로 넘어갔을 때 그 세션을 파괴하지 않고 방치하는 짓 말이다.
JavaScript의 허접한 가비지 컬렉터(GC)는 하부에 바인딩된 거대한 Rust WASM 메모리 객체와 열려있는 WebSocket 포트를 제때 치워주지 못한다. 결국 탭을 열고 닫을 때마다 연결이 증식(Leak)하여, 브라우저는 뻗기 직전이 되고 라우터(Router) 서버는 좀비 클라이언트들의 연결 유지 트래픽(Hearbeat)을 견디지 못하고 터져버린다.
6.0.1 [Runbook] 명시적 세션 처형 메커니즘
무조건 어플리케이션(혹은 컴포넌트) 라이프사이클 종료 시점에 철저한 언마운트(Unmount) 칼질을 해줘야 한다.
// React 컴포넌트 내부 예시 (혹은 Node.js 프로세스 종료 훅)
import { useEffect } from "react";
import * as zenoh from "@eclipse-zenoh/zenoh";
function DroneHUD() {
useEffect(() => {
let mounted = true;
let session: zenoh.Session | null = null;
let sub: zenoh.Subscriber | null = null;
const initZenoh = async () => {
session = await zenoh.open();
// 1. 구독과 퍼블리셔 등에서 반환되는 객체도 철저히 수집한다!
sub = await session.declareSubscriber("drone/1/+", (s) => {
if(mounted) console.log(s.keyExpr);
});
}
initZenoh();
// 🛡️ 언마운트 함수 (컴포넌트 소멸 시 브라우저가 호출함)
return () => {
console.log("🛑 화면 전환 감지. Zenoh 자원 완전 소각 개시...");
mounted = false;
// 비동기로 돌아가는 자원 해제 과정이지만, Promise.resolve 등으로
// 백그라운드에서 반드시 실행되도록 던져놔야 한다.
const cleanup = async () => {
if (sub) {
// 더 이상 수신할 필요 없음을 라우터에게 통보! (라우팅 낭비 방어)
sub.undeclare();
sub = null;
}
if (session) {
// 소켓을 닫고, WebAssembly 구역 메모리 포인터를 파괴함!
await session.close();
session = null;
}
};
cleanup();
};
}, []);
return <div>드론 모니터링 HUD 작동 중...</div>;
}
이 “클린업(Cleanup)” 로직 하나가 있느냐 없느냐로, “장난감 장판 스크립트“와 “실제 공업용 엔터프라이즈 프론트엔드 웹 앱” 수준이 갈린다. C++ 생태계의 RAII 원칙을 TypeScript 생태계에서 수동으로 완벽히 재현해 낸 것이다.