03 — Web vs Electron vs Native
이 문서가 답하는 질문: “내 앱이 브라우저 안에 있을 때, Electron 안에 있을 때, 네이티브로 짤 때 — 권한·배포·시작비용·OS API·보안 모델이 정확히 어떻게 달라지는가? 그 차이가 내 의사결정에 어떻게 들어와야 하는가?” 한 줄 답 (Pyramid Top): “브라우저 웹은 격리·안전·즉시 갱신을 OS 접근 불가의 대가로 얻는다. 네이티브는 OS 전부에 닿는 대신 플랫폼별 코드를 N번 짠다. Electron은 그 사이에서 같은 코드로 OS에 닿는다 — 격리·배포·보안의 책임을 직접 지는 대가로.”
Why — 왜 3축 비교가 필요한가
회의실에서 자주 듣는 잘못된 프레임 두 가지.
| 잘못된 프레임 | 실제 |
|---|---|
| ”웹앱이냐 데스크톱 앱이냐” | 이항 비교. 실제로는 웹 / Electron / 네이티브 세 자리가 있고 PWA·Tauri 등 중간 자리도 있다 |
| ”Electron은 브라우저에 Node 끼운 것” | 권한 모델과 배포 모델이 완전히 다르다. 브라우저 직관으로 풀 수 없는 문제가 매일 발생 |
이 문서는 세 자리(웹·Electron·네이티브)를 동일한 6개 축으로 평가한다 — 권한·배포·시작비용·OS API·보안 모델·UI 통합. 축이 같아야 비교가 비교가 된다.
이 문서는 챕터 전체에서 가장 핵심이다 — 다음 챕터들의 모든 디자인 결정이 이 비교의 어떤 칸에서 따라 나오는지 매번 참조된다.
How — 6축 비교 한 장
세 자리가 6개 축에서 각각 어떻게 다른지를 축 단위로 본다 — 자리 단위로 보면 어떤 자리가 무조건 우월하다는 잘못된 결론에 빠진다.
What — 축별 비교
6축 전체 비교표
| 축 | 브라우저 웹 | Electron | 네이티브 |
|---|---|---|---|
| 실행 단위 | tab (origin 격리) | BrowserWindow (process per window) | OS 창 (in-process) |
| OS API 접근 | sandbox 안의 Web API만 | Node fs + Electron API + 네이티브 모듈 | 플랫폼 SDK 전부 |
| 보안 모델 | same-origin policy + permission prompt | context isolation + sandbox + preload bridge | OS의 권한 모델 (entitlement·UAC 등) |
| 배포 | URL — 즉시 갱신, 설치 없음 | 서명된 binary + auto-update | 서명된 binary + 스토어·MSI·.app |
| 시작 비용 | 브라우저는 이미 떠 있음 (즉시) | 매번 새 Chromium + Node ( | 매번 OS 프로세스 (~ms 단위) |
| 메모리 | 탭당 | 빈 앱부터 | 보통 |
| 번들 크기 | 0 (URL만) | ||
| UI 통합 | OS 룩앤필 안 됨 — 브라우저 안 | OS 창·메뉴·트레이는 OK / Chromium 페이지는 웹 룩 | OS 룩앤필 그대로 |
| 권한 모델 | origin 단위 + prompt | 프로세스 분리 + IPC 통로 | OS의 user/group + capability |
| 공격면 | XSS · CSRF · 클릭재킹 | 위 전부 + XSS → OS 명령 위험 | 메모리 안전성 · injection · privilege escalation |
| 갱신 | 즉시 (URL이 새 코드) | 사용자 동의 + 재시작 | 사용자 동의 + 재시작 + 설치 권한 |
| 코드 공유 | 1 코드 → 모든 OS | 1 코드 → 모든 OS | 보통 N 코드 (cross-platform 토킷 없으면) |
| 개발 속도 | 매우 빠름 | 빠름 | 느림 |
| 런타임 의존 | 사용자 브라우저 | 앱 자체에 동봉 | OS 동봉 SDK |
| 오프라인 | 어려움 (Service Worker로 일부) | 자연스러움 | 자연스러움 |
축 1 — OS API 접근
| 작업 | 브라우저 웹 | Electron | 네이티브 |
|---|---|---|---|
| 임의 경로 파일 읽기·쓰기 | 불가 (File System Access API는 user gesture + prompt) | OK (fs로 자유) | OK |
| 트레이/메뉴바 아이콘 | 불가 | Tray | 플랫폼 API |
| 글로벌 단축키 | 불가 | globalShortcut | 플랫폼 API |
| 시스템 알림 | OK (Notification) | OK | OK |
| 클립보드 | 일부 (gesture 필요) | OK (clipboard) | OK |
| 자식 프로세스 실행 | 불가 | child_process.spawn | OK |
| 네이티브 GUI 위젯 | 불가 (HTML만) | 불가 (Chromium 안 — HTML만) | OK |
| Bluetooth · USB | Web Bluetooth/USB API (prompt) | OK | OK |
| Camera · Mic | OK (prompt) | OK | OK |
→ Electron은 OS API 접근에서 네이티브와 거의 동등이다. 단, 네이티브 GUI 위젯은 못 쓴다 — 그 안의 UI는 Chromium이 그리는 HTML이다. macOS의 NSTableView를 진짜로 그리고 싶다면 Electron이 아니다.
축 2 — 보안 모델
핵심: 브라우저 웹의 origin 경계는 공격이 origin 안에 갇히게 한다. XSS가 일어나도 그 도메인의 데이터가 위험할 뿐 OS는 안전하다.
Electron의 경계는 프로세스 + bridge다. XSS가 일어나면 preload가 노출한 함수만큼이 위험하다. 그래서 bridge를 좁게 만드는 것이 보안의 핵심이고, 그 결과로 contextIsolation: true가 디폴트가 됐다. 자세히는 05-security.
네이티브는 OS 경계를 그대로 빌린다. macOS의 entitlement, Windows의 UAC가 그 자리.
축 3 — 배포 모델
웹은 URL을 갱신하면 끝. 사용자 동작 없음.
Electron은 binary가 사용자 디스크에 깔려 있다. 갱신하려면 새 binary를 사인하고 → auto-update 서버에 올리고 → 클라이언트가 받고 → 재시작. 이 사이클이 Chromium 6주 동기화와 맞물려 분기 단위 정기 업무가 된다. macOS는 notarization(Apple 서명 + 공증)이 추가로 필요하다. Windows는 EV code signing이 SmartScreen 신뢰를 얻는 사실상의 조건.
네이티브는 같지만 플랫폼별로 다른 채널. App Store 심사, Windows Store, 자체 MSI, .dmg, deb/rpm 패키지 등.
축 4 — 시작 비용 / 메모리
| 시나리오 | 웹 (탭) | Electron | 네이티브 |
|---|---|---|---|
| Cold start | 즉시 (브라우저 떠 있음) | ||
| Warm start | 즉시 | 즉시 | |
| 빈 앱 메모리 | (브라우저 공통 비용에 포함) | ||
| 창 1개 추가 | 탭 1개 | Renderer 1개 | 보통 거의 무료 |
| 디스크 | 0 |
→ Electron의 진짜 비용은 런타임을 매번 띄우는 것이다. 사용자의 브라우저는 이미 떠 있어서 비용을 공유받지만, Electron 앱은 매번 자기 Chromium을 띄운다.
VS Code 팀이 V8 snapshot과 shared process를 만든 이유가 여기 — 시작 시간을 수 초 → 1초 미만으로 줄였다. 자세히는 07-performance-memory.
축 5 — 권한 모델
| 작업 | 브라우저 웹 | Electron | 네이티브 |
|---|---|---|---|
| ”이 사이트가 카메라를 쓰려고 함” | 디폴트 prompt | Electron의 setPermissionRequestHandler로 개발자가 결정 | OS prompt (TCC on macOS, UAC dialog 등) |
| “이 코드가 파일에 접근” | sandbox 안의 API 제한 | Main이 자유 / Renderer는 IPC 거침 | 프로세스 권한대로 |
| ”이 코드가 다른 origin 호출” | CORS preflight | 디폴트 webSecurity: true로 CORS 적용 / 끄면 위험 | 제한 없음 |
→ Electron에서 권한 prompt는 디폴트로 자동 거부된다. setPermissionRequestHandler로 내가 직접 결정해야 한다 — 이 점이 의외다. 카메라·마이크·알림·지오로케이션 등이 prompt가 안 떠서 안 됨으로 디버깅된다.
축 6 — UI 통합
| 측면 | 웹 | Electron | 네이티브 |
|---|---|---|---|
| OS 창 chrome (제목 표시줄·트래픽 라이트) | 없음 | BrowserWindow 옵션으로 표시/숨김 | 디폴트로 OS 룩 |
| 네이티브 메뉴바 | 없음 | Menu API | 디폴트 |
| 다크 모드 자동 추종 | media query | nativeTheme.shouldUseDarkColors + media query | 디폴트 |
| 접근성 (스크린리더) | Chromium의 a11y tree → OS | 같음 | 네이티브가 가장 깊음 |
| 폰트 렌더링 | Chromium | Chromium (같음) | OS 텍스트 시스템 |
| 입력 메서드 (CJK IME) | Chromium | Chromium | OS native — 가장 깔끔 |
→ OS의 진짜 룩앤필이 필요하면 네이티브. OS 창 chrome 정도면 충분하면 Electron으로 흉내낼 수 있다. 그 안의 콘텐츠는 어차피 HTML이라는 사실은 변하지 않는다.
결정 트리
→ 보통 공개 API·문서·간단 도구는 웹, 생산성 도구·메신저·노트·IDE는 Electron, 그래픽 성능이 핵심인 앱은 네이티브.
What-if — 잘못 쓰면
1) “Electron인데 웹처럼 짠다”
증상: Renderer에서 fetch('https://api.example.com')을 했더니 CORS 차단. localStorage가 각 BrowserWindow마다 다르게 보임.
원인: 브라우저 직관으로 Electron을 쓰는 경우. Renderer는 Chromium 페이지고, CORS·storage 격리가 그대로 적용된다 — 단, 세션이 독립이라 storage가 origin이 같아도 BrowserWindow마다 다르게 동작할 수 있다.
대응: 데이터 fetching·storage는 Main이 책임지고 Renderer는 IPC로 받는다. CORS는 Main에서 직접 호출하거나, session.webRequest로 헤더 조작.
2) “네이티브 룩앤필이 필요한데 Electron으로 선택”
증상: macOS의 NSToolbar·Sidebar·Inspector 같은 진짜 네이티브 위젯이 필요한데 HTML로 흉내내려다 어색함이 누적된다. 원인: Electron의 UI는 Chromium이 그리는 HTML. OS 네이티브 위젯이 아니다. 대응: 네이티브 룩이 제품의 핵심 가치면 그 부분은 네이티브로. 또는 cross-platform 토킷(Qt·Avalonia)으로.
3) “PWA로 충분한데 Electron으로 갔다”
증상: 200KB 웹앱을 200MB Electron으로 패키징. 사용자가 binary 다운로드를 거부. 원인: OS API가 없거나 거의 없는 앱에 Electron은 과잉 인프라. PWA가 설치 가능한 웹으로 같은 효과를 줄 수 있다. 대응: 결정 트리의 Q1을 솔직하게 답한다. PWA의 Web App Manifest + Service Worker로 풀리면 그쪽이 정답.
4) “Electron이니까 모든 게 동기”라고 보면
증상: Renderer에서 ipcRenderer.invoke('read-file')을 했는데 수백 ms 지연. UI 멈춤.
원인: IPC는 비동기 메시지 패싱. Main의 fs.readFile 호출 + 직렬화 + 메시지 큐 왕복이 수십~수백 ms.
대응: IPC를 비동기로 짠다. UI는 낙관적으로 먼저 그리고, 결과가 오면 갱신. 잦은 IPC는 배치하거나 MessagePort로 직접 채널을 만든다.
5) “보안은 나중에 본다”고 미루면
증상: 외부 URL 로딩 한 번에 preload가 노출한 함수가 RCE 통로가 됨.
원인: Electron의 보안은 디폴트 설정의 누적이다. 한 번 잘못 켜둔 옵션을 나중에 끄기는 깨진 기능을 다시 만드는 일과 같다.
대응: 설계 단계에서 contextIsolation: true · sandbox: true · webSecurity: true · CSP 명시 · 외부 URL 차단을 함께 켠다. 자세히는 05-security.
Insight — 흥미로운 이야기
”Origin은 브라우저의 발명이지 인터넷의 본질이 아니다”
브라우저의 same-origin policy는 1995년 Netscape가 임시 보안 모델로 만든 게 영구화된 것이다. URL이 같으면 같은 신뢰 도메인이라는 발상은 웹이 문서 중심이던 시절에 만들어졌고, 오늘의 SPA·SaaS 시대에는 너무 거칠다. 그래서 CORS·CSP·SameSite·CSRF token 같은 수십 개의 보완책이 누적됐다.
Electron은 origin을 그대로 빌리되 — 의미를 다시 정의했다. Electron 앱의 root URL은 file://이거나 app://이거나 custom protocol일 수 있고, 그 origin이 Renderer의 보안 경계가 된다. 하지만 진짜 경계는 preload bridge다 — origin은 내부 격리를 위한 도구일 뿐. 이 경계 재정의가 Electron의 핵심 디자인 결정이다.
”PWA의 한계가 Electron이 살아 있는 이유”
PWA(Progressive Web App)는 “설치 가능한 웹앱” — Service Worker로 오프라인, Web App Manifest로 OS 통합, File System Access API로 일부 파일 접근까지 닿는다. 이론적으로는 Electron을 대체할 수 있어야 한다.
실제로는 그렇지 못하다. 이유는 API 분포의 차이다.
| 기능 | Web API | Electron API |
|---|---|---|
| 임의 경로 파일 | File System Access API (Chrome/Edge만, prompt 필요) | fs 자유 |
| 트레이/메뉴바 | 없음 | Tray |
| 글로벌 단축키 | 없음 | globalShortcut |
| 자식 프로세스 | 없음 | child_process |
| 네이티브 모듈 | 없음 | N-API |
PWA는 공통 API의 보수적 확장인 반면 Electron은 Node API 전부를 들고 있다. VS Code 같은 IDE가 PWA로 못 가는 이유 — 자식 프로세스(tsc·eslint)·임의 파일 시스템·네이티브 git이 필요하다.
”Electron이 Chromium에 의존하는 게 약점인가 강점인가”
약점으로 보는 시각: Chromium 한 종류만 쓰니 엔진 다양성이 없다, 메모리 비용이 크다, Apple/Google의 정책에 묶인다.
강점으로 보는 시각: Chromium의 세계 최고급 보안 엔지니어링을 공짜로 빌린다, 모든 OS에서 같은 렌더링 결과가 나온다 (네이티브 webview는 OS마다 다름), DevTools가 그대로 작동한다.
같은 사실이 두 평가를 받는다 — 이것은 Electron이 위치한 자리가 근본적인 트레이드오프임을 보여준다. 한쪽 끝(Tauri는 OS webview)은 binary는 작고 메모리는 적지만 OS 호환성 차이가 발생, 다른 쪽 끝(네이티브)은 최고 성능이지만 N번 짠다. Electron은 통일된 Chromium을 비용으로 사고 코드 공유를 얻는다.
요약
세 자리가 6축에서 다르게 트레이드오프된다. 웹은 격리·즉시 갱신을 OS 접근 불가의 대가로, 네이티브는 OS 전부를 N-번 코드의 대가로, Electron은 둘의 사이에서 공유 코드 + OS 접근을 받고 격리·배포·보안 책임을 진다. Electron을 고르는 것은 제3의 길을 고르는 게 아니라 그 책임을 받아들이는 것이다. 다음 챕터들은 그 책임을 어떻게 운영하는가의 디테일이다.
다음 문서: 04-mental-model — 그래서 Electron 앱 한 개는 실제로 어떻게 생겼나. main.js + preload.js + index.html 세 파일로 시작한다.