04 — Native Integration (챕터 인덱스)
이 챕터가 답하는 질문: “Electron이 진짜 데스크톱 앱이 되려면 OS에 닿아야 한다 — 그 닿음을 어디서·누가·어떻게 하느냐?” 한 줄 답 (Pyramid Top): “Node API와 OS API에는 Main 프로세스만 닿고, 렌더러는 IPC를 통해 결과만 받는다 — Renderer가 OS에 직접 닿는 순간 Electron의 보안 모델은 전부 무너진다.”
챕터 지도
Why — 왜 네이티브 통합 챕터가 따로 존재하는가
웹 개발자가 가장 헷갈리는 지점이 여기다. 브라우저에서 fs.readFile을 부를 수 없는 이유는 권한이 없어서가 아니라 권한 모델 자체가 다르기 때문이다.
| 환경 | OS 접근 방식 | 권한 부여 시점 | 권한 범위 |
|---|---|---|---|
| 브라우저 | Web API (sandbox 안) | 사용자 prompt | 도메인 + 1회성 |
| Electron Renderer | 직접 못 닿음 (default) | — | 0 |
| Electron Main | Node fs/child_process 전부 | 앱이 시작되는 순간 | OS 사용자 전체 권한 |
Renderer가 XSS에 한 번 뚫리면 사용자 홈 디렉토리 전체가 노출된다 —
~/.ssh/id_rsa까지. 그래서 모든 OS 접근은 Main만이 한다 가 디폴트 룰이다.
이 챕터는 그 룰을 어떻게 지키면서 동시에 데스크톱 앱답게 만드는가의 7가지 패턴을 다룬다.
본문 7개 인덱스
| # | 파일 | 다루는 주제 | 핵심 키워드 |
|---|---|---|---|
| 01 | fs · path | Node fs/path로 사용자 데이터 읽고 쓰기. app.getPath('userData')로 OS별 경로 해결. 웹의 File System Access API와 권한 모델 비교 | fs · path · app.getPath · userData · File System Access API · notarization entitlement |
| 02 | shell · OS API | shell.openExternal/openPath, clipboard, nativeImage, powerMonitor. URL 열기의 보안 함정 (사용자 입력 직접 넘기지 말기) | shell · clipboard · nativeImage · powerMonitor · openExternal RCE |
| 03 | Menu · Tray | Menu/MenuItem/Tray. macOS 메뉴바 vs Windows 시스템 트레이의 철학 차이. 컨텍스트 메뉴 | Menu · MenuItem · Tray · accelerator · role · macOS menu bar |
| 04 | Notification · dialog | Electron Notification(웹 표준의 확장), dialog.showOpenDialog/showMessageBox. 비동기 vs 동기 함정 | Notification · dialog.showOpenDialog · showMessageBox · sheet · modal |
| 05 | Native Modules · N-API | C++ 모듈을 Electron ABI에 맞춰 빌드. electron-rebuild · prebuild. better-sqlite3 · sharp · keytar | node-gyp · N-API · node-addon-api · electron-rebuild · prebuild · ABI 불일치 |
| 06 | Custom Protocol | protocol.registerFileProtocol/registerStreamProtocol로 로컬 자원을 안전하게 노출. file:// 직접 노출 금지 | protocol.handle · registerFileProtocol · app:// · file:// 금지 · CSP |
| 07 | CLI · child_process | child_process.spawn으로 외부 CLI(ffmpeg·git) 호출. 쉘 인젝션 방지. macOS/Windows 경로·실행 권한 차이 | spawn · execFile · shell=true 금지 · argv array · PATH · cmd.exe vs bash |
어떻게 읽는가
- Electron 첫 OS 통합: 01 → 04 → 03. 파일 읽고 / 다이얼로그로 고르고 / 메뉴로 노출하면 1차 데스크톱 경험 완성.
- 무거운 데이터 다루는 앱(SQLite/이미지): 05 → 06 → 01. 네이티브 모듈 ABI 정착 → 로컬 자원 노출 채널 정리 → 파일 입출력.
- 트레이 상주 앱 / 시스템 유틸리티: 03 → 02 → 04. 메뉴바 상주 → OS 이벤트 구독 → 알림으로 사용자 호출.
- 외부 도구 wrapper(영상 변환 · git GUI): 07 → 01 → 04. 외부 CLI 안전 호출 → 결과 파일 처리 → 진행 다이얼로그.
What — 챕터를 관통하는 7가지 사실
| # | 사실 | 어디서 다루나 |
|---|---|---|
| 1 | 사용자 데이터는 app.getPath('userData') 하나로 다 해결된다 — 절대 직접 ~/.config/MyApp 같은 식으로 작성하지 말 것 | 01-fs-and-path |
| 2 | macOS notarization 후에는 ~/Documents·~/Desktop도 별도 entitlement 없으면 못 읽는다 | 01-fs-and-path |
| 3 | shell.openExternal(userInput)는 RCE 통로다 — javascript: 스킴 한 줄로 끝남 | 02-shell-and-os |
| 4 | macOS 메뉴바와 Windows 트레이는 겉모습은 비슷해도 OS 모델이 다르다 — 같은 코드로 두 경험을 못 만든다 | 03-menus-and-tray |
| 5 | 네이티브 모듈은 Node ABI가 아니라 Electron ABI에 맞춰야 한다 — electron-rebuild가 그 일을 한다 | 05-native-modules-and-n-api |
| 6 | file://를 그냥 렌더러에 노출하면 사용자 홈 전체가 fetch 한 줄로 읽힌다 — custom scheme + protocol handler가 정답 | 06-protocol-and-custom-scheme |
| 7 | child_process.exec(userInput)은 쉘 인젝션 — spawn(cmd, [args], {shell:false}) + argv 배열이 표준 | 07-cli-and-child-process |
What-if — 권한이 너무 커서 위험하다
OS 권한이 디폴트로 전부 켜져 있다는 사실이 이 챕터의 모든 함정의 출처다.
| 함정 | 증상 | 원인 챕터 |
|---|---|---|
app.getPath 안 쓰고 hardcoded path | Windows에서 ~/가 /c/Users/raw/가 아니라 풀려서 ENOENT | 01 |
notarization 통과 후 ~/Documents 안 열림 | read access denied — entitlement 누락 | 01 |
shell.openExternal(req.body.url) 그대로 호출 | javascript:require('child_process').exec(...) 한 줄로 OS 명령 실행 | 02 |
| Windows에 macOS Tray 코드 그대로 | 트레이는 뜨지만 메뉴바 통합은 안 됨 | 03 |
Notification을 Renderer에서만 띄움 | 권한 prompt가 Chrome 스타일로 떠서 사용자 혼란 | 04 |
Electron 메이저 업그레이드 후 import 'better-sqlite3' | NODE_MODULE_VERSION 119 vs 125 에러 — ABI 불일치 | 05 |
<img src="file:///etc/passwd">가 떠 버림 | webSecurity:false 또는 file:// 노출 | 06 |
spawn('git', ['log', branchName]) — branchName = "; rm -rf /" | shell=false면 안전, shell=true면 재앙 | 07 |
Insight — 한 단락 이야기
“VS Code의 파일 트리가 빠른 이유는 fs를 안 쓰기 때문이다”
VS Code 팀은 초기에 모든
fs.readdir을 Renderer에서 IPC로 부르다 수만 개 파일이 있는 모노레포에서 freeze가 발생했다. 해법은 두 가지였다 — (a) fs 호출을 Main에서 Utility Process로 분리해 메인 이벤트 루프를 비워두기, (b) native module(node-watch / chokidar의 native backend)로 OS 레벨 inotify·FSEvents를 직접 구독하기. 즉 “Renderer ↔ Main의 IPC만 잘 짜면 된다”는 1차 답이 아니라 Main도 너무 바쁘면 안 된다 → Utility Process로 한 번 더 분리 → 거기서 네이티브 모듈을 부른다는 2차 답이 정답이었다. 이 챕터의 진짜 메시지가 이거다 — “Main이 OS 창구”라는 룰을 지키되, Main 안에서도 작업을 잘게 쪼개라.
한 단락 요약
네이티브 통합 챕터는 Renderer가 OS에 직접 못 닿는다는 룰 위에서 Main이 어떻게 OS의 일곱 면(파일·쉘·메뉴·알림·네이티브 모듈·로컬 자원·외부 CLI)에 안전하게 닿는가를 일곱 문서로 펼친다. 각 문서가 공통으로 던지는 질문은 “이 호출이 사용자 입력을 직접 받는가? 받는다면 어떻게 검증하는가?” — 권한이 너무 커서 검증 없는 호출이 곧 RCE다. 이 챕터를 끝내면 “OS에 어떻게 닿느냐”가 아니라 *“OS에 닿기 전에 무엇을 검증했느냐”*를 먼저 묻게 된다.