키오스크

agnusdei·7일 전

현장에 설치된 수많은 키오스크 기기(Electron / Android)의 화면을 중앙에서 실시간으로 원격 관리하고, AI로 생성된 웹 앱을 안전하게 배포하기 위해서는 정교한 통신 및 배포 파이프라인이 필수적입니다.

단순히 "실시간 라이브러리를 붙였다" 수준을 넘어, L4 TCP 3-Way Handshake부터 L7 HTTP Upgrade, 커널 레벨의 소켓 버퍼 직접 쓰기(Push), Coolify를 통한 컨테이너 격리 배포 파이프라인, 그리고 실제 프로덕션 코드 구현까지 네트워크 엔지니어링 관점에서 총망라하여 정리합니다.


1. 전체 시스템 아키텍처

┌──────────────────┐               ┌──────────────────────────────────────┐
│ 관리자 웹 (Admin) │               │           백엔드 (NestJS)             │
│  - React / Vite  │ ── HTTPS ───▶ │  - REST API / OpenAPI                │
│  - AI 채팅 / 승인 │               │  - Socket.IO 게이트웨이 (/device)     │
└──────────────────┘               └──────────────────┬───────────────────┘
                                                      │
                       ┌──────────────────────────────┼──────────────────────────────┐
                       │ (배포 트리거)                 │ (내부 SSE)                   │ (WebSocket Push)
                       ▼                              ▼                              ▼
        ┌─────────────────────────────┐  ┌─────────────────────────┐  ┌─────────────────────────┐
        │  호스팅 인프라 (Coolify)     │  │ AI 생성 런타임 (Codex)   │  │ 키오스크 (Client)        │
        │   - 정적 Nginx 컨테이너      │  │  - 격리 작업공간        │  │  - React 셸 (Electron/   │
        │   - 고정 도메인 / 자동 SSL   │  │  - 정적 HTML/CSS/JS 생성 │  │    Capacitor Android)    │
        │   - 기기별 배포 격리         │  └─────────────────────────┘  │  - 샌드박스 iframe 렌더링 │
        └──────────────┬──────────────┘                               └────────────┬────────────┘
                       │                                                           │
                       └─────────────── HTTPS 정적 콘텐츠 요청 ──────────────────────┘
  • 관리자 웹 (Admin Web): 관리자가 AI 채팅을 통해 키오스크 UI를 생성하고, 격리된 미리보기(Preview) 확인 후 최종 배포를 승인합니다.
  • 백엔드 서버 (NestJS): 기기 페어링 및 인증, 실시간 소켓 연결 관리, 불변 아티팩트 빌드, 배포 무결성 검증을 담당합니다.
  • 배포 인프라 (Coolify): 기기마다 독립된 정적 Nginx 컨테이너를 프로비저닝하고, 고정 도메인(https://{deviceId}.<BASE_DOMAIN>)과 SSL 인증서를 자동으로 발급·라우팅합니다.
  • 키오스크 클라이언트: 서버와 상시 WebSocket을 유지하며, 푸시를 수신하는 즉시 화면을 무중단 갱신하고 Service Worker로 오프라인 실행을 보장합니다.

2. 1단계: L4 TCP 커넥션 수립 (SYN / ACK)

모든 웹소켓 통신의 바닥에는 OS 커널 레벨의 TCP 연결 지향형 스트림이 존재합니다.

클라이언트 (키오스크)                                            서버 (NestJS)
       │                                                              │
       │ ── 1. TCP SYN ─────────────────────────────────────────────> │
       │ <── 2. TCP SYN + ACK ─────────────────────────────────────── │ (3-Way Handshake)
       │ ── 3. TCP ACK ─────────────────────────────────────────────> │
       │                                                              │
       │ === 커널 레벨 소켓 파일 디스크립터(Socket FD) 및 버퍼 할당 완료 === │
  1. TCP SYN: 클라이언트 커널이 무작위 시퀀스 번호(ISNcISN_c)를 생성하여 SYN 패킷을 전송.
  2. TCP SYN + ACK: 서버 커널이 자신의 시퀀스 번호(ISNsISN_s)와 함께 클라이언트 번호에 1을 더한 ACK(ISNc+1ISN_c + 1)를 회신.
  3. TCP ACK: 클라이언트가 서버 번호에 1을 더한 ACK(ISNs+1ISN_s + 1)를 전송.

이 핸드셰이크가 끝나면 양측 OS 커널에는 소켓 파일 디스크립터(Socket FD)와 송수신 버퍼(TCP Send/Receive Buffer)가 할당되며, 출발지와 목적지의 IP:Port 4-튜플 가상 회선이 연결됩니다.


3. 2단계: L7 HTTP Upgrade 핸드셰이크

TCP 소켓이 열려도 곧바로 독자적인 바이너리 패킷을 쏘지 않습니다. 기존 방화벽, L7 역방향 프록시, NAT 장비들이 80/443 외의 낯선 포트와 비표준 패킷을 차단하는 것을 회피하기 위해, 처음에는 합법적인 일반 HTTPS 웹 요청으로 위장하여 진입합니다.

3.1 클라이언트의 업그레이드 요청

GET /socket/?transport=websocket HTTP/1.1
Host: <API_HOST>
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
  • Upgrade: websocket: 이 TCP 연결의 프로토콜을 웹소켓으로 승격해 달라는 선언.
  • Connection: Upgrade: 중간 프록시 장비에 프로토콜 업그레이드 트랜잭션임을 알리는 Hop-by-hop 헤더.
  • Sec-WebSocket-Key: 16바이트 난수의 Base64 인코딩 값. 단순 HTTP 캐시 오동작을 막고 서버의 웹소켓 지원 여부를 검증하기 위한 챌린지 키.

3.2 서버의 수락 응답 (101 Switching Protocols)

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  • 서버는 수신한 Sec-WebSocket-Key 뒤에 RFC 6455 표준에 정의된 고정 매직 GUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)를 이어 붙입니다.
  • 이를 SHA-1 단방향 해시한 뒤 Base64로 인코딩하여 Sec-WebSocket-Accept 헤더에 담아 보냅니다.
  • 클라이언트는 이 값을 자신이 계산한 값과 대조하여 정상적인 웹소켓 엔드포인트임을 최종 확정합니다.

4. 3단계: 프로토콜 스위칭의 본질 — "이 순간부터 파서(Parser)가 바뀐다"

HTTP 101 Switching Protocols 응답이 오고 가는 바로 그 순간, 양측 네트워크 엔진의 내부 상태 머신에 결정적인 변화가 일어납니다.

"소켓 파이프라인(동일한 Socket FD)을 그대로 유지한 채, 바이트 스트림을 해석하는 파서(Parser)를 텍스트 라인 파서에서 비트 단위 바이너리 프레임 파서로 완전히 갈아끼웁니다."

핸드셰이크 전(HTTP) vs 핸드셰이크 후(WebSocket) 비교

구분핸드셰이크 이전 (HTTP 모드)핸드셰이크 이후 (WebSocket 모드)
파서 (Parser)HTTP 텍스트 파서RFC 6455 바이너리 프레임 파서
해석 규칙줄바꿈(\r\n)을 찾는 문자열 파싱첫 1바이트부터 비트(bit) 단위 디코딩
헤더 크기요청마다 수백 바이트 ~ 수 KB (Cookie, User-Agent 등)단 2 ~ 6 바이트 (FIN, Opcode, Length)
통신 방향반이중(Half-Duplex) / 요청-응답(Pull) 모델완전 전이중(Full-Duplex) / 상호 독립 전송
서버 동작클라이언트가 GET을 요청해야만 대답(수동적)원할 때 언제든 데이터를 밀어 넣음(능동적 푸시)
[ 핸드셰이크 이전: HTTP 텍스트 파서 ]
TCP Receive Buffer ──▶ [ HTTP Text Parser ]
                        - 바이트를 ASCII/UTF-8 문자열로 변환
                        - \r\n(줄바꿈)을 찾으며 헤더 라인 파싱
                        - 빈 줄(\r\n\r\n) 도달 시 헤더 파싱 종료

                         ▼ (101 응답 발생: 파서 전환!)

[ 핸드셰이크 이후: RFC 6455 바이너리 프레임 파서 ]
TCP Receive Buffer ──▶ [ Binary Frame Parser ]
                        - 더 이상 GET, POST, \r\n 같은 텍스트 라인을 찾지 않음
                        - 들어오는 첫 바이트부터 비트 단위 플래그로 즉각 디코딩
                        - FIN(1bit), Opcode(4bit), Mask(1bit), Length(7bit) 해석

이 순간부터는 GET /... 같은 텍스트를 전송하면 정상적인 요청이 아니라 프로토콜 에러(Protocol Error)로 간주되어 소켓이 즉시 강제 종료됩니다.


5. 4단계: RFC 6455 바이너리 프레임 규격 상세

프로토콜 스위칭 이후 전송되는 모든 패킷은 아래의 비트 단위 바이너리 프레임 구조를 갖습니다.

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Masking-key (클라이언트 -> 서버 전송 시 32-bit XOR 마스킹 키)     |
+---------------------------------------------------------------+
|                     Payload Data (실제 데이터)                  |
+---------------------------------------------------------------+

비트(bit) 단위 필드 분석

1번째 바이트 (Byte 0)

  • FIN (1 bit): 이 프레임이 메시지의 마지막 조각인가? (1: 단일 또는 분할 메시지의 끝, 0: 뒤에 프레임이 더 이어짐).
  • RSV 1~3 (3 bit): 프로토콜 확장을 위한 예약 비트 (기본값 000).
  • Opcode (4 bit): 페이로드 데이터의 성격을 정의.
    • 0x1: 텍스트 데이터 (UTF-8 문자열, 예: JSON)
    • 0x2: 순수 바이너리 데이터 (파일, 바이너리 버퍼)
    • 0x8: 커넥션 종료 (Close)
    • 0x9: Ping (L7 Heartbeat)
    • 0xA: Pong (Heartbeat 응답)

2번째 바이트 (Byte 1)

  • MASK (1 bit): 페이로드가 마스킹(난수화)되었는지 여부.
    • 클라이언트 → 서버: 무조건 1. (RFC 6455 필수 규격. 투명 프록시의 캐시 오염 공격을 방지하기 위해 클라이언트는 반드시 페이로드를 32비트 난수 키로 XOR 마스킹해야 함).
    • 서버 → 클라이언트: 무조건 0. 마스킹 키가 생략됨.
  • Payload length (7 bit): 데이터 길이.
    • 0 ~ 125: 데이터의 실제 바이트 크기.
    • 126: 뒤따라오는 2바이트(16비트)를 읽어 길이로 사용 (최대 64KB).
    • 127: 뒤따라오는 8바이트(64비트)를 읽어 길이로 사용 (대용량 데이터).

3~6번째 바이트 (클라이언트 전송 시에만 존재)

  • Masking-key (4 byte / 32 bit): 클라이언트가 생성한 4바이트 난수 키. 실제 데이터의 ii번째 바이트는 Payload[i] XOR MaskingKey[i % 4] 형태로 변환되어 날아가며, 서버는 동일한 XOR 연산으로 원본을 복원.

6. 5단계: "클라이언트 버퍼에 직접 쓴다" — 서버 푸시(Server Push)의 OS 커널 메커니즘

"서버가 푸시한다"는 말의 본질은 "클라이언트가 요청하지도 않았는데, 클라이언트 컴퓨터 커널의 수신 버퍼(TCP Receive Buffer)에 데이터가 이미 쓰여져 들어와 있는 상태"를 만듭니다.

서버가 원격지 클라이언트의 물리 RAM을 직접 쓰는 것은 불가능하므로, 양측 OS 커널과 랜카드(NIC)가 협력하여 이를 수행합니다.

[서버 프로세스]
       │ 1. write(socket_fd, buffer, length) 시스템 콜
       ▼
[서버 OS 커널 (Send Buffer)]
       │ 
       ▼ ─── 2. TCP PSH(Push) 플래그 켜서 패킷 방출! ───┐
                                                         │ (인터넷 선로)
                                                         ▼
                                                [클라이언트 랜카드(NIC)]
                                                         │
                                                         ▼ 3. 하드웨어 인터럽트 (DMA 쓰기)
                                           [클라이언트 OS 커널 (Receive Buffer)]
                                             ★ 여기에 데이터가 이미 '써짐'! ★
                                                         │
                                                         ▼ 4. "버퍼링 말고 즉시 올려보내!"
                                              [클라이언트 앱 (이벤트 루프)]
                                                 socket.on('content:refresh') 즉각 실행

상세 동작 단계

  1. 서버의 능동적 시스템 콜 (write):
    • 일반 HTTP에서 서버는 클라이언트의 read() 요청을 기다립니다.
    • 웹소켓에서 서버는 클라이언트의 요청 여부와 상관없이, 이미 열려 있는 소켓 FD를 대상으로 Node.js/libuv의 uv_write() 시스템 콜을 호출하여 자기 커널의 Send Buffer에 데이터를 씁니다.
  2. TCP PSH(Push) 플래그의 역할:
    • 원래 TCP 스택은 네트워크 효율을 위해 데이터가 어느 정도 찰 때까지 버퍼링(Nagle 알고리즘 등)을 수행할 수 있습니다.
    • 하지만 웹소켓 데이터는 TCP 헤더의 PSH 플래그를 1로 셋팅하여 전송합니다.
    • 이 플래그는 수신 측 OS 커널에게 "버퍼에서 대기하지 말고, 들어온 즉시 애플리케이션으로 밀어 올려라!"고 지시하는 표준 TCP 플래그입니다.
  3. 클라이언트 수신 버퍼에 쓰기:
    • 클라이언트 랜카드(NIC)가 패킷을 수신하는 순간, DMA(Direct Memory Access)를 통해 클라이언트 OS 커널의 Receive Buffer 메모리에 데이터를 직접 기록(Write)합니다.
  4. 이벤트 루프 인터럽트 (Wake-up):
    • 클라이언트 OS 커널은 대기 중이던 클라이언트 프로세스의 epoll/kqueue 이벤트 루프를 즉각 깨웁니다.
    • 클라이언트 앱 입장에서는 가만히 있었는데 "내 집 문 앞(커널 수신 버퍼)에 택배가 이미 도착해 있고 벨이 띵동 울린 것"과 완벽히 동일하므로, 지연시간 없이 0.001초 만에 화면 전환을 실행할 수 있습니다.

7. 6단계: 기기 보안 및 2단계 페어링 인증

공공장소에 배치되는 키오스크의 특성을 고려하여 6자리 일회용 코드 페어링과 토큰 해싱을 사용하는 2단계 보안 인증 방식을 적용합니다.

[키오스크 기동]
  │
  ├─ 1. installationId(UUID) 생성
  ├─ 2. socket.emit('pairing:request') ───▶ [NestJS 서버]
  │                                           │
  │ ◀── 3. 6자리 페어링 코드 발급 ───────────────┤ (10분 유효, Rate Limit)
  │
[화면에 6자리 코드 표시]
  │
[관리자가 Admin 웹에서 6자리 코드 입력 및 승인]
  │
  │ ◀── 4. socket.emit('device:paired', { deviceToken }) ─┤ (고엔트로피 난수)
  │
[키오스크 로컬 보안 저장소에 보관]                         [DB에 SHA-256 해시값만 저장]
- Electron: safeStorage                                (authTokenHash)
- Android: Keystore Secure Storage
  1. 최초 등록 (6자리 코드 페어링):
    • 키오스크는 자체 installationId를 생성한 뒤 소켓으로 임시 코드를 요청합니다.
    • 서버는 10분 유효한 6자리 숫자 코드를 발급하고, 관리자가 이를 웹 대시보드에서 승인합니다.
  2. 장기 연결 인증 (디바이스 토큰):
    • 승인 즉시 키오스크에 고유 deviceToken이 전달됩니다.
    • 키오스크는 이를 OS 보안 저장소(Electron safeStorage, Android Keystore)에 암호화하여 보관합니다.
    • 서버 DB는 토큰 원문을 저장하지 않고 SHA-256 해시값만 보관합니다.
    • 소켓 재접속 시 auth: { deviceToken }을 검증하여 통과한 소켓만 device:{deviceId} 룸에 바인딩합니다.
  3. 원격 접근 차단 (Revoke):
    • 관리자가 기기를 해제하면 서버 DB의 토큰 해시가 즉시 무효화되고 활성 소켓이 강제 단절(disconnectSockets(true))됩니다.

8. 7단계: 쿨리파이(Coolify) PaaS 배포 파이프라인과 Zero-Trust 검증

생성된 웹 화면을 각 키오스크에 서빙하는 인프라 엔진으로 오픈소스 PaaS인 Coolify를 활용합니다.

  1. 기기별 컨테이너 격리: 기기마다 독립된 Nginx 컨테이너 애플리케이션(kiosk-device-{hash})을 생성하여 특정 기기의 장애가 다른 기기에 전파되지 않습니다.
  2. 고정 도메인 및 자동 SSL/TLS: 각 기기에 https://{deviceId}.<BASE_DOMAIN> 형태의 고유 서브도메인을 할당하고, Let's Encrypt 인증서 발급과 Traefik 역방향 프록시 라우팅을 자동화합니다.
  3. 인라인 Dockerfile을 통한 무결성 빌드: 서버가 아래와 같은 멀티스테이지 Dockerfile을 동적으로 주입하여 배포 아티팩트(site.tar.gz)의 SHA-256 해시를 다운로드 시점에 검증합니다.
# 아티팩트 다운로드 및 무결성 검증 스테이지
FROM alpine:3.22 AS artifact
RUN apk add --no-cache ca-certificates curl
ARG KIOSK_ARTIFACT_URL
ARG KIOSK_ARTIFACT_SHA256
RUN curl -fsSL "$KIOSK_ARTIFACT_URL" -o /tmp/site.tar.gz \
    && echo "$KIOSK_ARTIFACT_SHA256  /tmp/site.tar.gz" | sha256sum -c - \
    && mkdir -p /site && tar -xzf /tmp/site.tar.gz -C /site

# 최종 정적 Nginx 서빙 스테이지
FROM nginx:1.29-alpine
COPY --from=artifact /site/ /usr/share/nginx/html/
# 강력한 Content-Security-Policy(CSP) 설정 적용
EXPOSE 80
  1. Zero-Trust 배포 검증:
    • Coolify가 "배포 성공"을 반환하더라도 서버는 이를 그대로 믿지 않습니다.
    • 서버가 실제 기기 도메인의 https://{deviceId}.<BASE_DOMAIN>/__kiosk_manifest.json을 직접 HTTP GET으로 호출하여 배포 ID, 버전 번호, Canonical SHA-256 해시가 정확히 일치하는지 확인한 뒤에만 활성화(ACTIVE) 상태로 전환하고 소켓 푸시를 트리거합니다.

9. 8단계: 실제 구현 코드 (NestJS 서버 & React 클라이언트)

이 프로젝트에서 실제로 동작하는 서버와 클라이언트의 소켓 통신 구현 코드입니다.

9.1 서버 사이드: 인증, 룸 관리, 실시간 푸시 (NestJS)

서버는 핸드셰이크 단계에서 토큰을 검증하고, 기기를 고유 룸(device:{deviceId})에 바인딩한 뒤 필요할 때 즉시 푸시를 발송합니다.

// apps/server/src/gateways/device.gateway.ts
import { WebSocketGateway, WebSocketServer, type OnGatewayInit, type OnGatewayConnection } from '@nestjs/websockets';
import type { Namespace, Socket } from 'socket.io';

@WebSocketGateway({ namespace: '/device', path: '/socket' })
export class DeviceGateway implements OnGatewayInit, OnGatewayConnection {
  @WebSocketServer() 
  server!: Namespace;

  // 1. 핸드셰이크 단계 미들웨어: 소켓 연결 시 토큰 인증
  afterInit(server: typeof this.server): void {
    server.use(async (socket, next) => {
      const deviceToken = socket.handshake.auth?.deviceToken;
      if (!deviceToken) return next(new Error('UNAUTHORIZED'));

      // DB의 SHA-256 토큰 해시 대조
      const principal = await this.tokens.authenticate(deviceToken);
      if (!principal) return next(new Error('UNAUTHORIZED'));

      socket.data.deviceId = principal.deviceId;
      next();
    });
  }

  // 2. 연결 체결: 기기 전용 가상 룸(Room)에 조인
  async handleConnection(socket: Socket): Promise<void> {
    const deviceId = socket.data.deviceId;
    await socket.join(`device:${deviceId}`);

    // 재접속 시 누락된 최신 배포가 있다면 즉각 동기화 푸시
    const current = await this.devices.getCurrentDeployment(deviceId);
    if (current && current.isNewerThanLastAck) {
      this.pushRefresh(deviceId, current);
    }

    // 4. 클라이언트의 렌더링 완료 ACK 수신
    socket.on('content:ack', async (payload, ack) => {
      await this.prisma.device.update({
        where: { id: deviceId },
        data: { lastAcknowledgedDeploymentId: payload.deploymentId }
      });
      if (typeof ack === 'function') ack({ ok: true });
    });
  }

  // 3. 서버 푸시(Server Push) 실행 메서드
  pushRefresh(deviceId: string, payload: ContentRefreshEvent): void {
    // 특정 기기 소켓 룸으로 'content:refresh' 이벤트를 직접 방출!
    this.server.to(`device:${deviceId}`).emit('content:refresh', payload);
  }
}

9.2 클라이언트 사이드: 소켓 수신, 검증, 화면 교체 및 ACK 회신 (React/Electron)

클라이언트는 상시 대기하고 있다가 푸시 이벤트를 감지하면, 무결성을 검증하고 화면을 교체한 뒤 서버로 ACK를 전송합니다.

// apps/client/src/socket/device-socket.ts
import { io, type Socket } from 'socket.io-client';

export function createDeviceSocket(adapter: PlatformAdapter) {
  // 1. WebSocket 전송 계층으로 즉시 연결 수립
  const socket = io('/device', {
    path: '/socket',
    transports: ['websocket'], // HTTP Long-Polling 폴백 비활성화
    auth: async (callback) => {
      const deviceToken = await adapter.getDeviceToken(); // OS 보안 저장소에서 로드
      callback({ deviceToken });
    },
  });

  // 2. 서버 푸시(content:refresh) 감지 및 화면 갱신
  socket.on('content:refresh', async (content) => {
    console.log(`새 배포 버전 ${content.version} 푸시 감지!`);

    // Zero-Trust: 실제 HTTPS 배포 도메인의 manifest 무결성 검증
    const isValid = await verifyManifest(content.url, content.contentHash);
    if (!isValid) return;

    // 샌드박스 iframe src 갱신 (화면 교체)
    updateKioskIframe(content.url);

    // 3. 화면 반영 성공 후 서버로 역방향 확인(ACK) 패킷 전송
    socket.timeout(5000).emitWithAck('content:ack', {
      deploymentId: content.deploymentId,
      version: content.version,
      loadedAt: new Date().toISOString(),
    });
  });

  // 4. 30초 주기 애플리케이션 Heartbeat 전송
  setInterval(() => {
    if (socket.connected) {
      socket.emit('device:heartbeat', { timestamp: Date.now() });
    }
  }, 30000);
}

10. 9단계: 장애 복구 및 오프라인 신뢰성 메커니즘

  1. L7 프로토콜 레벨 Ping/Pong 프레임 (Half-Open Socket 감지):
    • 서버가 주기적으로 빈 바이너리 Ping 프레임(Opcode 0x9)을 전송.
    • 클라이언트는 이를 받으면 동일한 페이로드를 담아 즉각 Pong 프레임(Opcode 0xA)으로 회신.
    • 제한 시간 내에 Pong이 오지 않으면 서버는 소켓 디스크립터를 강제 폐기(destroy)하여 고스트 커넥션을 제거.
  2. 재접속 시 자동 상태 동기화 (Self-healing on Reconnect):
    • 배포 푸시가 전송되는 순간 키오스크의 전원이 꺼져 있었거나 일시적 네트워크 장애가 발생하더라도 문제없습니다.
    • 키오스크가 다시 연결되어 소켓 핸드셰이크가 체결되는 즉시, 서버가 DB의 최신 ACTIVE 배포와 기기의 lastAcknowledgedDeploymentId를 대조하여 누락된 버전을 즉시 다시 푸시합니다.
  3. 오프라인 캐시 유지:
    • 배포 패키지 내부의 Service Worker가 모든 정적 에셋을 로컬에 영구 캐싱합니다.
    • 현장 인터넷 회선이 단절되거나 기기가 재부팅되어도 마지막으로 성공한 화면 버전을 즉시 표시합니다.
  4. 런타임 보안 격리:
    • 키오스크 화면 렌더링 iframe은 sandbox="allow-scripts allow-same-origin" 속성으로 제한되어, AI가 생성한 웹 코드가 Electron의 Node.js 네이티브 바인딩이나 OS API에 접근하는 것을 원천 차단합니다.

최종 요약

  1. L4 단계: TCP 3-Way Handshake로 양측 커널에 소켓 파일 디스크립터(Socket FD)와 버퍼를 생성.
  2. L7 핸드셰이크 단계: 80/443 방화벽을 통과하기 위해 HTTP Upgrade: websocket 요청 후 101 Switching Protocols 합의를 완료.
  3. 파서 스위칭: 동일한 TCP 파이프라인에서 HTTP 텍스트 파서를 언마운트하고 RFC 6455 바이너리 프레임 파서를 마운트.
  4. 오버헤드 절감: 수 KB의 HTTP 헤더 대신 비트 단위 플래그(FIN, Opcode, Mask)가 포함된 단 2~6바이트 초경량 바이너리 헤더로 통신.
  5. 서버 푸시의 본질: 서버가 시스템 콜(write())을 통해 TCP PSH 세그먼트를 방출하면, 클라이언트 OS 커널의 TCP Receive Buffer에 데이터가 이미 쓰여져 들어와 있는 상태가 됨으로써 지연 없는 초고속 실시간 화면 갱신이 달성됨.
  6. Coolify 연동: 기기별 Nginx 컨테이너 및 고정 도메인을 자동 구성하고, Zero-Trust Manifest 검증이 완료된 시점에 소켓 푸시를 쏴 안전한 배포 라이프사이클을 완성.

0개의 댓글