3-4. 세 OS와 일곱 언어, 그리고 전수 감사 (5/24 ~ 6/20)

마지막 구간은 두 가지를 한다. macOS에만 있던 것을 Windows·Linux로 펴고, Electron 패리티 감사 목록을 하나씩 지운다.

자식 창과 크로스 플랫폼 (5/24~5/25)

5월 24일에 WebContentsView를 CEF Views 기반 자식 윈도우로 다시 세우고 생명주기 이벤트와 크로스 플랫폼 E2E를 붙였다.

5월 25일은 90커밋, 이 프로젝트에서 가장 큰 날이다.

  • Linux — 윈도우 라이프사이클 이벤트, 클립보드, 셸 API
  • Windows — Win32 기반 클립보드·셸
  • 자동 업데이터
  • 크래시 리포터
  • 패키징

이때부터 "macOS에서 되는 기능"과 "세 OS에서 되는 기능"의 간격을 좁히는 것이 기본 작업 단위가 된다.

플러그인군과 IPC 개선 (5/27~5/29)

  • Linux GTK tray 백엔드
  • plugin-log, plugin-store, plugin-http 등 공식 플러그인 확장
  • deferred response — printToPDF/capturePage를 진짜 Promise 패턴으로(#16)
  • Linux/Windows GPU 가속 활성화(#12)

5월 29일에는 deferred-response의 잠재 결함 — UAF, slot exhaustion, cross-kind, TTL — 을 찾아 고쳤다(PR #57). 비동기 응답 슬롯을 도입하면 따라오는 문제들을 한 번에 정리한 작업이다.

언어가 둘 더 늘었다 (6/5)

언어방식
Luavendored Lua 5.4 + cjson, 시스템 의존 0, 1급 outbound API(invoke/send/on)
Pythonembedded CPython 3.13 — GIL 처리, 번들 stdlib, packaged .app/CLI 번들, PYTHONHOME 자립 해석

새 언어를 들일 때 기준은 하나였다.

"파이썬 루아도 확인해보세요 모든 언어 플랫폼이 지원되어야 해요" — 2026-06-06

그래서 Python은 데스크톱에서 끝내지 않고 같은 날 iOS·Android 임베드 백엔드까지 붙여 시뮬레이터·에뮬레이터 e2e 5/5를 통과시켰다. 다음 날 suji init의 multi 스캐폴딩에 lua·python이 추가되면서 공식 지원 언어가 일곱이 됐다.

배포 쪽도 함께 정리했다 — app.minimumSystemVersion으로 최소 배포 타겟 지정(6/4), app.icon으로 .png → .icns 생성(6/5), 프로덕션 .app 배포 안정화.

Electron 패리티를 갈아 넣은 사흘 (6/6~6/8)

docs/electron-parity-audit.md의 목록을 대량으로 소화한 구간이다. 거의 모든 항목이 전 5 SDK에 동시에 노출됐다.

영역추가된 것
BrowserWindowfocus/isNormal, getBounds/getSize/getPosition, isVisible/isFocused/blur, setAlwaysOnTop, getAllWindows/getFocusedWindow, getContentBounds/setContentBounds, destroy, setSize/setPosition + min/max, setResizable·Minimizable·Maximizable·Closable, setMovable·Focusable·Enabled·FullScreenable·Kiosk
apprequestSingleInstanceLock 트리오, second-instance 이벤트(Windows는 named pipe), before-quit, setAsDefaultProtocolClient
sessionsetProxy, setPermissionRequestHandler, setDownloadPath + will-download
Menuid/visible, accelerator, role, getApplicationMenu/getMenuItemById, sendActionToFirstResponder, icon, insert, will-show/will-close
기타screen.getDisplayMatching과 display-added/removed/metrics-changed, powerMonitor 배터리·shutdown 이벤트, nativeTheme·nativeImage 게터, globalShortcut registerAll/suspend, clipboard writeBookmark·writeFindText·multi, tray setToolTip·getBounds, notification groupId·removeGroup, webContents stop·insertCSS·setWindowOpenHandler, webRequest setRequestHeaders

플러그인 세 개와 PTY (6/9~6/10)

  • window-state — 창 bounds/maximized 저장·복원 (Electron/Tauri 동등)
  • positioner — 창을 화면/트레이/커서 위치로 배치 (Tauri positioner 동등)
  • upload — multipart 업로드/다운로드, URL·PATH allowlist deny-by-default, SSRF·traversal 검사
  • terminal — forkpty 기반 PTY 플러그인, 스트리밍 출력

큰 IPC 페이로드를 청크 분할 + emit 동적 버퍼로 무제한화한 것도 이때다.

콘텐츠 서빙 (6/14~6/18)

  • main.ts 진입점 지정, 인증 이벤트와 Window 정합
  • 로컬 파일 서빙 — suji://app/__localfile__ + Range 요청, allowFileAccess 옵션, sqlite busy_timeout
  • suji-video:// 영상 전용 스킴 — 백엔드 재귀 Mach-O 서명으로 공증 통과, CSP(media/connect-src)와 교차출처 fetch CORS(ACAO)

전수 감사로 닫는다 (6/20)

마지막 작업일의 커밋 넷이 전부 감사다.

  1. 코어·보안 전수조사 1차 — loader / events / config / updater / util / fs 게이트
  2. 플랫폼·CEF 전수조사 2차 — session / clipboard / tray / page-output / auth / packaging
  3. /code-review max 발견 수정 — 1차·2차 수정에서 새로 만든 버그를 교정
  4. 재리뷰 발견 수정 — canonicalizePath root 회귀와 pool-full emit 정리

고치는 과정에서 생긴 결함까지 다시 잡고 닫는 것이 이 프로젝트의 마감 방식이다.

이 구간에서 부딪힌 천장

패리티를 밀다 보면 "포팅할 수 없는 것"이 나온다. 이 질문 자체가 6월 8일 세션에서 명시적으로 나왔다.

"CEF 특성으로 인한 불가능이 있다면 말씀해주세요" — 2026-06-08

가장 크게 걸린 것이 CEF 동기 콜백이다.

on_before_resource_load나 on_before_popup 같은 CEF 콜백은 동기 반환 이후의 수정을 무시한다. 그래서 Electron의 동적 per-request/per-popup JS 콜백 패턴 — 요청마다 JS로 물어보고 그 답으로 결정하는 방식 — 은 구현이 불가능하다. async round-trip으로 답을 받아 적용하면 이미 늦다.

echo 서버 e2e로 실증한 내용이다.

  • webRequest.onBeforeSendHeaders — RV_CONTINUE_ASYNC 후 resolve에서 set_header_by_name을 불러도 헤더가 wire에 도달하지 않는다. 동기 OnBeforeResourceLoad 구간에서 적용해야 한다(PR #131).
  • webContents.setWindowOpenHandler — on_before_popup은 동기 1/0 반환이라 async JS 상담이 불가능하다(PR #132).

해결은 선언적 동기 정책 + 관찰용 이벤트로 바꾸는 것이다. webRequest.setRequestHeaders({urls}, headers)로 URL glob 매칭 시 동기 주입하고, windows.setWindowOpenHandler("allow"|"deny") 전역 정책에 web-contents:new-window 이벤트를 얹는다. 포팅할 수 없는 항목은 docs/electron-parity-audit.md에 정직 경계로 남긴다(setCertificateVerifyProc, setSSLConfig, cookies-changed가 같은 부류다).

반복해서 새는 곳

패리티 작업에서 계속 반복된 실수가 둘 있다.

하나, 구현하고 문서에 표시를 안 한다. audit의 [low] 백로그를 구현하는 PR들이 코드만 넣고 마킹을 빠뜨려 30개 넘게 밀렸다. audit이 진척의 단일 출처인데 마킹이 없으면 다음에 "미구현"으로 오인해 중복 구현으로 이어진다. 결국 전수 점검으로 PR #170·#171에서 31개를 일괄 마킹해 0으로 수렴시켰다. 이후 규칙은 기능 PR마다 세 축을 동시에 갱신하는 것 — CLAUDE.md API 주석, audit 마킹, 5개 SDK 동형.

둘, "전 SDK 지원"이라는 주장이 정확하지 않았다. 전수조사에서 Zig SDK(src/core/app.zig)가 일관된 낙오자로 드러났다. 코어 명령은 있는데 Zig 타입드 래퍼만 없는 경우가 여럿이라 "전 6개 언어" 표기가 실제와 달랐다(lua·python 백엔드는 handle/invoke/send/on 4개 프리미티브만 노출한다). 반대로 문서가 구현보다 뒤처진 경우도 있었다 — @suji/node와 Rust SDK는 문서에 없는 모듈을 이미 export하고 있었다. 이후로는 패리티 주장을 쓰기 전에 각 SDK 표면을 grep으로 확인한다.

지금

2026년 9월 14일 기준 공개 저장소의 최신 푸시는 6월 20일이지만, 개발은 계속되고 있다. maru·zntc와 시간을 나눠 쓰는 구조라 작업 구간이 뭉쳐 나타날 뿐이다. 다음 구간의 기록은 이어서 붙인다.