20.5.3 TypeScript 기반 Zenoh 애플리케이션 트러블슈팅

20.5.3 TypeScript 기반 Zenoh 애플리케이션 트러블슈팅

클라우드 백엔드, 프론트엔드 React 프로젝트, 자율 자율주행 관제 모니터 대시보드. 데이터가 최종 소비자와 만나는 가장 화려하고 트렌디한 엔드포인트에는 어김없이 JavaScript 계열의 언어인 TypeScript (Node.js/Browser)가 사용된다.

C 언어가 물리적인 메모리 누수로 사람을 괴롭힌다면, 이 언어는 “모든 코어 로직이 단 하나의 스레드(Single Thread)에서 도는” Node.js 특유의 이벤트 루프(Event Loop) 아키텍처로 인해 병목 교착의 지옥을 선물한다. 그리고 브라우저라는 극도로 폐쇄적인 샌드박스 보안 환경은 Zenoh 라우터와의 자유로운 UDP 네트워크 교류를 송두리째 막아버려 CORS(Cross-Origin Resource Sharing) 위약금과 WebSocket 터널링의 한계라는 장벽을 세운다.

이 절에서는 우아해야 할 TypeScript 코드가 수천 개의 데이터를 맞이하여 뻗어나갈 때, 브라우저 콘솔과 서버 로그를 다각도로 튜닝하여 비동기 논블로킹(Non-blocking) 본연의 우월함을 되찾고 웹의 철창을 매끄럽게 통과하는 풀스택 트러블슈팅의 정수를 전개할 것이다.

1. 이벤트 루프(Event Loop) 블로킹 및 비동기 처리 콜백 지연

Node.js 환경의 TypeScript 클라이언트가 라우터로부터 1초에 만 개의 센서 데이터를 Subscribe를 통해 받고 있다 하자.
만성적으로 “데이터베이스 도착 지연(Latency)“이 생겨서 확인해 보면, 네트워크 핑스피드는 1ms인데 TS 앱 내부의 콘솔 로그가 3초나 뒤에 찍히고 있는 초유의 사태가 발생한다.

1.0.1 단계: 콜백 지옥(Callback) 큐의 폭주와 CPU 독점

수신된 페이로드 sample.value는 거대한 원시 버퍼 혹은 복잡한 JSON이다. 만약 콜백 함수 내부에서 무거운 JSON.parse나 이미지 디코딩 함수 같은 동기적 연산(Synchronous CPU Intensive Task)을 남발하면 단일 스레드 기반인 Node.js의 V8 이벤트 루프(Event Loop)가 꼼짝없이 물려버린다(Blocked).
루프가 이 파싱에 전력투구하는 동안, 들어오는 수만 개의 후속 Zenoh 네트워크 트래픽은 램 버퍼에 쌓이거나 소켓 단에서 버려지는 참사가 벌어지는 것이다.

강제 회피 기동:
콜백 안에서는 무거운 연산을 피하라. 오직 가벼운 배열(Queue Array) 구조에 푸시(Push)만 밀어 넣고 즉각 이벤트 루프를 풀어주어라(Fire).
비동기로 빠진 데이터를 모아 일괄 처리용 웹 워커(Worker Threads) 프로세스로 던지거나, 백그라운드 스케줄러(setImmediate)로 쪼개어(Chunking) CPU 독점을 분산시켜야만 네트워크 계층 수신부가 질식사하는 것을 막을 수 있다.

1.0.2 단계: 순차적 퓨처 대기로 인한 파이프라인 지연 (await 함정)

루프를 막는 동기 함수는 없지만, 코딩을 이렇게 했다고 치자.

session.declare_subscriber("sensor/**", async (sample) => {
    // DB 조회를 위해 한 패킷마다 10ms씩 가만히 대기!
    await DB.save(sample); 
});

이러면 들어오는 패킷이 직렬(Sequential)로 밀리면서 지연이 기하급수적으로 폭증한다. 수신과 비즈니스 로직을 분리하여 느슨한 결합 처리를 구비하는 것이 트러블슈팅의 끝판왕이다. 비동기 Promise.all 기반의 병렬 처리 기법을 즉각 탑재하라.

2. V8 엔진 메모리 초과(OOM) 및 가비지 컬렉션(GC) 튜닝

어제 올려둔 관제 대시보드 스크립트 도커 컨테이너가 아무런 알람 배출도 없이 죽어버린 채 로그(Log) 마지막 라인에 흉측한 메시지를 뱉고 사망했다.
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

2.0.1 V8 힙(Heap) 한계 돌파를 위한 조치

Node.js 백엔드 앱이 OOM을 맞았을 때 가장 시급한 미봉책은 디폴트 메모리 세팅 한계치(일반적으로 1.5GB)를 부숴주는 세팅이다.
실행 시그니처에 다음과 같이 --max-old-space-size 파라미터를 강제 삽입하여 할당 상한을 늘려 즉각 서버부터 살려놓는다. (단위는 MB)

node --max-old-space-size=4096 dist/server.js

2.0.2 근본적인 버퍼 릭(Buffer Leak) 프로파일링

제한을 늘리는 건 임시방편이다. V8 가비지 컬렉터(GC)가 왜 도저히 청소를 포기하게 되었는지 코드를 해부해야 한다.
대역폭이 거대한 Zenoh 통신(LiDAR 영상 등)을 받을 때, 백엔드 TypeScript 코드가 외부 맵(Map) 컨테이너 전역 변수에 그 상태들을 캐싱하고 주기를 비워주지 않으면 GC는 이 객체들이 “아직 쓰이고 있구나“라고 착각해 절대 해제하지 않는다.

V8 헤비-덤프(Heap Snapshot) 분석 툴을 동원하라.
node --inspect 모드로 실행한 채 구글 크롬 브라우저의 chrome://inspect 디버거와 연결한다. OOM이 나기 직전 메모리 스냅샷을 촬영(Take snapshot)하여, Retained Size가 수백 메가바이트에 이르는 객체 트리를 추적하면 당신의 전역 Array 변수나 EventEmitter의 리스너(Listener) 누수를 적나라하게 색출해 낼 수 있다.
이 무거운 쓰레기를 주기적으로 null로 가비지 처리하는 로직을 심어 엔진의 구강을 청결히 유지하라.

3. 브라우저 환경에서의 WebSocket 연결 실패 및 CORS 이슈

Zenoh 프론트엔드 대시보드(React/Vue) 코드는 웹 브라우저 위에서 가동된다. 크롬 같은 현대 브라우저 샌드박스는 일반적인 호스트 운영체제처럼 마음대로 하부 TCP/UDP 소켓을 개방하여 멀티캐스트 세션을 여는 행위 자체를 거부한다. 오로지 웹소켓(WebSocket) 터널링만이 클라우드 서버와 이 프론트엔드를 호흡하게 만든다.

이 웹소켓 장벽 구간에서 당신의 대시보드 UI를 먹통으로 만드는 전형적인 프로토콜 실패 현상을 정복해야 한다.

3.0.1 단계: 치명적인 Mixed-Content (보안 연결 에러)

브라우저에서 당신의 관제 대시보드 도메인이 https://smart-factory.com 기반으로 호스팅되어 있다면, 이 브라우저는 절대로 평문 HTTP 웹소켓(ws://)을 열어주지 않고 무자비하게 차단한다(Mixed Content Policy 위반).
프론트엔드의 타겟 zenoh.open() 커넥트 경로 문자열을 무조건 암호화 프로토콜인 wss:// 라우터 주소로 코딩 강제 수정하라.

3.0.2 단계: CORS(Cross-Origin Resource Sharing) 거부 현상

라우터의 REST API 모디파이어(Modifier)인 HTTP/WS 인터페이스 통신을 찔렀으나 브라우저 콘솔 로그에 붉은색 글씨로 CORS preflight channel did not succeed 창이 폭포수처럼 쏟아진다면, 클라우드 서버의 Nginx/라벨 단 역방향 프록시(Reverse Proxy) 또는 Zenoh WebServer 자체에서 다른 출처(Origin)의 접근 빗장을 걸어둔 것이다.

// zenohd.json5 내 REST 플러그인 또는 웹서버 플러그인 설정 단
plugins: {
  rest: {
    // 모든 도메인에서의 호출을 포용하기 위해 와일드카드로 빗장을 풀어버린다.
    cors: "*" 
  }
}

또는 라우터 앞단의 Nginx 설정 리플라이 헤더에 강제로 Access-Control-Allow-Origin: *Upgrade 지시문을 세팅하여 웹소켓 프로토콜 승격(Upgrade Request)이 공중 분해되는 파멸을 미연에 방지하라. 웹 디버깅의 승패는 언제나 크롬 NetworkWS 탭 헤더의 101 Switching Protocols 스탯 승인 여부에 달려있다.