1. 서론: 웹 기반 UI와 네이티브 OS 기능이 결합될 때 발생하는 샌드박스 장벽
현대적인 데스크톱 플랫폼은 웹 기술(HTML5, CSS3, Web Components)의 유려한 표현력과 네이티브 운영체제(C#, C++, Win32 API)의 강력한 시스템 제어 능력을 결합한 하이브리드 아키텍처(Hybrid Architecture)로 진화하고 있습니다. ZeliDesk 역시 최신 Chromium 엔진 기반의 Microsoft Edge WebView2를 내장하여 공교육 콘텐츠와 대시보드를 렌더링하고 있습니다.
그러나 웹 브라우저 엔진은 보안상의 이유로
철저한 샌드박스(Sandbox) 안에 갇혀 있습니다.
웹 페이지 내부의 JavaScript는 로컬 디스크의 실행 파일(.exe)을 실행하거나,
물리 RS232 시리얼 포트를 열거나, 윈도우 창의 드로어를 조작할 수 없습니다.
| 보안 및 설계 축 | 단순/취약한 Web-Native 연동 | ZeliDesk.Core WebMessagePayload 거버넌스 |
|---|---|---|
| IPC 메시지 규격 | 임의 문자열 또는 비구조화 JSON 전달 | 정형화된 WebMessagePayload 불변 DTO 계약 |
| 실행 타깃 검증 | 웹이 넘겨준 경로/명령을 cmd.exe에 직접 주입 |
LauncherItemRegistry 화이트리스트 사전 검증 |
| 역직렬화 안정성 | 대소문자 불일치나 누락 시 런타임 예외 발생 | PropertyNameCaseInsensitive = true 안전 매핑 |
| 런처 타입 격리 | 모든 대상을 동일한 프로세스로 실행 | 4대 타입(Web, Native, System, Internal) 분리 디스패치 |
| 하드웨어 브릿지 | 웹에서 시리얼 통신 불가 | 시리얼 포트 프록시 명령(serial_*) 원자적 중계 |
ZeliDesk.Core는 이 보안 난제를 해결하기 위해
초경량 IPC 프로토콜 DTO인 WebMessagePayload와
선언적 메타데이터 모델인 LauncherItem을 구축했습니다.
2. WinUI 3와 WebView2 간의 비동기 메시징 메커니즘 (postMessage ↔ WebMessageReceived)
WebView2는 렌더러 프로세스와 호스트 프로세스 간의 통신을 위해
비동기 파이프 기반의 메시지 채널을 제공합니다.
웹 페이지의 JavaScript에서는 window.chrome.webview.postMessage(payload)를 호출하고,
C# 호스트 애플리케이션은 WebMessageReceived 이벤트를 통해 이를 수신합니다.
이 통신은 COM 프록시나 직접적인 메모리 공유가 아닌 순수 문자열(JSON) 직렬화 기반의 이벤트 디스패치로 이루어지므로, 웹 페이지의 스크립트 엔진 크래시가 네이티브 C# 프로세스로 번지지 않는 프로세스 간 물리적 방화벽(Process Isolation)을 형성합니다.
3. 단일 진실 공급원(SSOT) DTO: WebMessagePayload의 3대 핵심 필드 (Command, Target, Data)
ZeliDesk.Core의 WebMessagePayload는
웹과 네이티브 사이에서 오가는 모든 제어 신호를 표현할 수 있도록
단 3개의 직관적인 문자열 필드로 설계되었습니다:
namespace ZeliDesk.Core.Models
{
/// <summary>
/// WebView2와 WinUI 3 간의 양방향 IPC 메시징을 위한 단일 진실 공급원(SSOT) 페이로드 DTO
/// </summary>
public class WebMessagePayload
{
public string Command { get; set; } // 수행할 네이티브 액션 (예: open_tab, launch_native, serial_open)
public string Target { get; set; } // 실행 대상 식별자 또는 포트 번호 (예: Chrome, COM3)
public string Data { get; set; } // 명령에 수반되는 추가 직렬화 파라미터 (선택)
}
}
이 DTO는 C#의 System.Text.Json 직렬화 엔진과 결합하여,
웹 측에서 카멜 케이스(command)나 파스칼 케이스(Command) 중
어떤 방식으로 JSON 키를 전송하더라도
PropertyNameCaseInsensitive = true 옵션을 통해 0.001ms 내에 완벽한 C# 객체로 역직렬화됩니다.
4. 임의 명령 실행 방어: LauncherItemRegistry 화이트리스트 기반 타깃 정규화 알고리즘
웹 페이지에서 launch_native 명령을 보내왔을 때,
만약 C# 호스트가 Target에 적힌 문자열을 그대로 Process.Start(payload.Target)에 넘긴다면
악의적인 스크립트가 powershell.exe -Command "..." 같은 명령을 실행시킬 수 있는
치명적인 보안 홀이 발생합니다.
ZeliDesk는 철저한 화이트리스트 검증(Whitelist Validation)과 타깃 정규화(Normalization) 알고리즘을 강제합니다:
case "launch_native":
// 1. 공백 제거 및 소문자 변환으로 비교 키 정규화
string normalizedTarget = payload.Target?.Replace(" ", "").ToLowerInvariant();
// 2. 사전에 등록된 공식 LauncherItemRegistry 화이트리스트 카탈로그에서만 검색
var nativeItem = _allItems.FirstOrDefault(i =>
string.Equals(i.Name, payload.Target, StringComparison.OrdinalIgnoreCase) ||
string.Equals(GetCleanName(i), payload.Target, StringComparison.OrdinalIgnoreCase) ||
string.Equals(i.Target, payload.Target, StringComparison.OrdinalIgnoreCase) ||
(GetCleanName(i).Replace(" ", "").ToLowerInvariant() == normalizedTarget) ||
(i.Target?.Replace(" ", "").ToLowerInvariant() == normalizedTarget)
);
// 3. 화이트리스트에 등록되지 않은 임의의 실행 파일이나 명령은 100% 무시
if (nativeItem != null)
{
// 선언적 실행 로직 디스패치...
}
break;
이 3중 정규화 검증을 통해, 웹 측에서는 친숙한 이름(예: "한글", "계산기", "Excel")이나 정식 실행 파일명 중 무엇으로 호출하더라도 오직 개발팀이 사전에 안전성을 검증하여 등록한 72종 공식 도구 목록 안에서만 안전하게 실행됩니다.
5. 4대 런처 타입(Web, NativeApp, SystemCmd, InternalApp) 및 시리얼 하드웨어 디스패치 파이프라인
검증을 통과한 LauncherItem은
LauncherItemType 열거형에 따라 전용 실행 파이프라인으로 분기됩니다:
| LauncherItemType | 실행 대상 및 목적 | 실제 처리 파이프라인 |
|---|---|---|
Web |
교육청 나이스, AIDT 디지털교과서 등 웹 URL | SelectOrCreateTab(targetWebItem) (브라우저 탭 생성 및 가상화) |
NativeApp |
한글(HWP), MS 오피스, 포토샵 등 로컬 프로그램 | NativeLauncher.Launch(target, args) (ShellExecute 예외 격리 실행) |
SystemCmd |
Windows 제어판, 디스크 관리, 장치 관리자 | NativeLauncher.Launch(target, args) (시스템 환경변수 기반 호출) |
InternalApp |
ZeliOffice, ZeliGen, ZeliMD 등 젤리데스크 내장 앱 | LaunchInternalApp(target) (단일 인스턴스 활성화 및 윈도우 스냅) |
여기에 더해 serial_list, serial_open, serial_write와 같은
하드웨어 제어 명령이 수신되면,
직전 백서(#860)에서 분석한 SerialPortManager의 인스턴스로 즉각 라우팅되어,
웹 페이지와 물리 아두이노/전자칠판 센서 간의 양방향 텔레메트리가 실시간으로 중계됩니다.
6. 핵심 구현 분석: ZeliDesk.Core 모델 및 MainWindow.xaml.cs 연동 실코드 워크스루
다음은 ZeliDesk 솔루션의 하이브리드 IPC 통신을 지탱하는
LauncherItem.cs와 MainWindow.xaml.cs의 실제 핵심 구현입니다:
// ── ZeliDesk.Core.Models.LauncherItem.cs ──
namespace ZeliDesk.Core.Models
{
public enum LauncherItemType
{
Web, // WebView2 내부 탭으로 열기
NativeApp, // 로컬 설치 프로그램 실행
SystemCmd, // Windows 시스템 설정/명령
InternalApp // ZeliDesk 자체 내장 유틸리티
}
public class LauncherItem
{
public string Name { get; set; }
public string Target { get; set; } // URL 또는 실행파일 경로/명령
public string Icon { get; set; } // 이모지 또는 글리프 아이콘
public LauncherItemType Type { get; set; }
public string Category { get; set; } // 그룹 분류 (교육, 업무, 시스템, AI)
public string Args { get; set; } // 실행 인자 (선택)
}
}
// ── MainWindow.xaml.cs (WebView2 수신 디스패처) ──
private void MyWebView_WebMessageReceived(WebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
{
try
{
// 1. 웹 메시지 문자열 추출
var json = args.TryGetWebMessageAsString();
if (string.IsNullOrEmpty(json)) return;
// 2. 단일 진실 공급원 WebMessagePayload로 안전 역직렬화
var payload = JsonSerializer.Deserialize<WebMessagePayload>(
json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
if (payload == null || string.IsNullOrEmpty(payload.Command)) return;
// 3. 명령별 안전 분기 처리
switch (payload.Command.ToLowerInvariant())
{
case "open_tab":
var targetWebItem = _allItems.FirstOrDefault(i =>
string.Equals(i.Name, payload.Target, StringComparison.OrdinalIgnoreCase) ||
string.Equals(GetCleanName(i), payload.Target, StringComparison.OrdinalIgnoreCase));
if (targetWebItem != null && targetWebItem.Type == LauncherItemType.Web)
{
SelectOrCreateTab(targetWebItem);
}
break;
case "launch_native":
string normalizedTarget = payload.Target?.Replace(" ", "").ToLowerInvariant();
var nativeItem = _allItems.FirstOrDefault(i =>
string.Equals(i.Name, payload.Target, StringComparison.OrdinalIgnoreCase) ||
string.Equals(GetCleanName(i), payload.Target, StringComparison.OrdinalIgnoreCase) ||
string.Equals(i.Target, payload.Target, StringComparison.OrdinalIgnoreCase) ||
(GetCleanName(i).Replace(" ", "").ToLowerInvariant() == normalizedTarget) ||
(i.Target?.Replace(" ", "").ToLowerInvariant() == normalizedTarget)
);
if (nativeItem != null)
{
if (nativeItem.Type == LauncherItemType.NativeApp || nativeItem.Type == LauncherItemType.SystemCmd)
NativeLauncher.Launch(nativeItem.Target, nativeItem.Args);
else if (nativeItem.Type == LauncherItemType.InternalApp)
LaunchInternalApp(nativeItem.Target);
}
break;
case "serial_open":
EnsureSerialManager();
var openData = ParseSerialOptions(payload.Data);
bool opened = _serialManager.Open(payload.Target, openData.baud, openData.parity, openData.dataBits, openData.stopBits);
SendMessageToWeb("serial_opened", new { port = payload.Target, success = opened });
break;
}
}
catch (Exception ex)
{
Debug.WriteLine($"[WebBridge Error] {ex.Message}");
}
}
7. 결론: 가장 안전하고 가벼운 하이브리드 데스크톱 IPC의 설계 표준
현대 애플리케이션 개발에서 웹과 네이티브의 융합은 피할 수 없는 시대적 흐름입니다. 그러나 그 융합의 접점마다 엄격한 보안 경계선(Security Boundary)이 그어져 있지 않다면, 사소한 스크립트 취약점 하나가 운영체제 전체의 침해 사고로 직결될 수 있습니다.
ZeliDesk.Core의 WebMessagePayload와 LauncherItem 아키텍처는
명료한 3대 필드 계약(Contract), 화이트리스트 사전 검증, 4대 런처 타입의 완전 격리를 통해
웹의 편리함과 네이티브의 강력함을 가장 안전한 방식으로 결합해 냈습니다.
전국 학교 교실의 교사와 학생들이 클릭 한 번으로 웹 교과서와 네이티브 오피스 도구를 자유자재로 넘나들며 수업에 몰입할 수 있는 배경에는, 단 1비트의 허점도 허용하지 않는 ZeliDesk의 정밀한 IPC 거버넌스가 든든하게 자리 잡고 있습니다.