01 · BrowserWindow의 해부
이 문서가 답하는 질문:
new BrowserWindow({...})한 줄이 실행되면 OS·Chromium·V8에 정확히 무엇이 생기는가?webPreferences의 30개 옵션 중 진짜 중요한 12개는 무엇인가?
한 줄 답 (Pyramid Top)
BrowserWindow는 세 가지를 하나의 객체로 묶은 추상화다 — ① OS의 네이티브 윈도우 핸들(HWND/NSWindow/GtkWindow), ② Chromium의WebContents인스턴스, ③ 그 콘텐츠를 그릴 별개의 렌더러 프로세스. 이 셋이 한 묶음이라는 사실을 잊으면 창을 닫아도 죽지 않는 좀비 렌더러가 생긴다.
Why — 왜 그냥 “창”이 아니라 “해부”인가
웹의 window.open()은 브라우저가 만들어 주는 한 칸이다. 같은 origin이면 같은 프로세스(보통)에 살고, 닫으면 GC가 알아서 정리한다. Electron의 BrowserWindow는 그렇지 않다.
-
BrowserWindow생성자가 하는 일은 최소 4가지- OS에 네이티브 창 생성 요청 (Win32/Cocoa/X11/Wayland API 호출).
- 그 창에 Chromium의
WebContentsView를 자식으로 붙임. WebContents에 대응하는 새 렌더러 프로세스 fork(Chromium의 zygote 메커니즘).webPreferences옵션을 그 렌더러 프로세스의 V8 컨텍스트에 적용.
-
이 4가지 중 어느 것도 자동으로 GC되지 않는다. 자바스크립트 객체로서의
BrowserWindow참조를 놓아도 OS 핸들·렌더러 프로세스는close이벤트가 와야 해제된다.
그래서 창 누수는 일반 메모리 누수의 1000배쯤 비싸다 — 한 창당 50~80MB의 렌더러 프로세스가 잔존한다.
How — 어떻게 한 묶음이 되는가
1) 객체 구조
BrowserWindow.webContents— webContents 객체에 접근 (win.webContents.loadURL(...)).BrowserWindow.id— 정수 ID. 다중 창 관리 시 키로 사용.BrowserWindow.getAllWindows()— 살아 있는 모든 창. 디버깅 1순위.
2) 최소 코드
// main.js — Electron Main 프로세스에서만 가능
const { app, BrowserWindow } = require('electron');
const path = require('node:path');
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
show: false, // ← 핵심: 깜빡임 방지
backgroundColor: '#1e1e1e', // ← show:false 동안 보일 배경
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // 보안 디폴트 (v12+ 기본 true)
sandbox: true, // 디폴트로 켜는 게 모범
nodeIntegration: false, // 절대 true로 두지 말 것
webSecurity: true,
},
});
win.once('ready-to-show', () => win.show()); // ← 흰 깜빡임 제거
win.loadFile('index.html');
return win;
}
app.whenReady().then(createWindow);
show: false+ready-to-show+show()패턴은 Electron 공식 권장. 안 하면 첫 1~2초 동안 흰 배경이 보였다 사라진다 — 모든 사용자가 “왜 깜빡거리지?”라고 묻는 그 현상.
3) webPreferences — 30개 옵션 중 핵심 12개
| # | 옵션 | 디폴트 (Electron 28+) | 의미 | 안전 권장 |
|---|---|---|---|---|
| 1 | preload | undefined | 렌더러 시작 전 실행되는 격리된 스크립트 경로 | 필수 (있어야 contextBridge 가능) |
| 2 | contextIsolation | true | preload와 페이지 JS의 컨텍스트 분리 | 항상 true |
| 3 | sandbox | true (preload 있어도) | 렌더러를 Chromium sandbox에 가둠 | true 권장 |
| 4 | nodeIntegration | false | 렌더러에서 require/process 직접 노출 | 항상 false |
| 5 | nodeIntegrationInWorker | false | Web Worker에서 Node API | 거의 모든 경우 false |
| 6 | webSecurity | true | CORS·same-origin 강제 | 항상 true (개발 중에도) |
| 7 | allowRunningInsecureContent | false | https 페이지의 http 리소스 허용 | false |
| 8 | experimentalFeatures | false | Chromium 실험 기능 | false |
| 9 | enableBlinkFeatures | '' | 특정 Blink 기능 강제 켜기 | 보통 비움 |
| 10 | defaultEncoding | 'ISO-8859-1' | 기본 텍스트 인코딩 | 'UTF-8'로 변경 권장 |
| 11 | backgroundThrottling | true | 비활성 창의 타이머 throttle | 음악/모니터링 앱은 false |
| 12 | spellcheck | true | 내장 스펠체커 | 입력 앱은 true, 외엔 끄면 메모리 절약 |
보안 디폴트 4개가 모두 안전한 쪽으로 변한 건 Electron 12 (2021). 그 이전 코드는 명시적으로 끄지 않으면 위험했다. 자세한 변천은 05 보안 참고.
4) 추가로 자주 쓰는 BrowserWindow 옵션 (webPreferences 바깥)
new BrowserWindow({
// 모양
frame: false, // 타이틀바 제거 (커스텀 타이틀바)
titleBarStyle: 'hiddenInset', // macOS — 신호등은 남기고 타이틀만 숨김
transparent: true, // 투명 창 (성능 비용)
hasShadow: false,
vibrancy: 'sidebar', // macOS 블러 효과
// 동작
alwaysOnTop: false,
fullscreenable: true,
resizable: true,
minimizable: true,
// 위치
x: 100, y: 100, // 없으면 OS가 배치
center: true,
minWidth: 800, minHeight: 600,
// 부모-자식
parent: mainWindow, // 모달 다이얼로그용
modal: true,
});5) 라이프사이클 이벤트 — BrowserWindow 자체
close vs closed 차이:
close— 닫기 시도 시점.event.preventDefault()로 막을 수 있음.closed— 이미 닫혔음. 이때 참조를 nullify 해야 GC.
6) 메모리 누수 함정 — 교과서 코드
// ❌ 나쁜 코드 — 전형적 누수
let win;
function createWindow() {
win = new BrowserWindow({...});
win.loadFile('index.html');
}
// win.on('closed', ...) 없음 → 창은 닫혀도 win 참조가 남음
// → 다음 createWindow() 호출하면 또 하나 생김// ✅ 올바른 코드
let win;
function createWindow() {
win = new BrowserWindow({...});
win.loadFile('index.html');
win.on('closed', () => { win = null; }); // ← 필수
}다중 창은 Set 또는 Map으로:
const windows = new Set();
function createWindow() {
const win = new BrowserWindow({...});
windows.add(win);
win.on('closed', () => windows.delete(win));
return win;
}이 패턴은 05 다중 창에서 본격 다룬다.
What — 이 문서를 관통하는 7가지 사실
| # | 사실 |
|---|---|
| 1 | BrowserWindow 1개 = OS 창 1개 + webContents 1개 + 렌더러 프로세스 1개 |
| 2 | 한 창의 메모리 비용은 보통 50~80MB (빈 페이지 기준), 콘텐츠 따라 200MB+ |
| 3 | show: false + ready-to-show + show() 패턴은 흰 깜빡임 제거의 표준 |
| 4 | webPreferences 30+ 옵션 중 안전 4-종 세트는 contextIsolation·sandbox·nodeIntegration:false·webSecurity |
| 5 | close 이벤트는 preventDefault()로 막을 수 있다 — “정말 종료?” 확인 다이얼로그 패턴 |
| 6 | closed 이벤트에서 반드시 창 참조를 nullify (또는 Set/Map에서 제거) |
| 7 | BrowserWindow.getAllWindows()로 언제든 살아 있는 창 목록을 볼 수 있다 — 누수 디버깅의 1순위 |
What-if — 무엇이 잘못되나
| 상황 | 결과 | 해법 |
|---|---|---|
nodeIntegration: true 두고 외부 URL 로딩 | XSS 한 번에 OS 명령 실행 | 디폴트(false) 유지, preload + contextBridge로만 노출 |
show: true (디폴트) + 무거운 페이지 | 1~2초 흰 배경 깜빡임 | show:false + ready-to-show 패턴 |
창 참조 전역 변수, closed에서 nullify 안 함 | 메모리 누수 + 다음 창 만들 때 ID 충돌 | windows.delete(win) 또는 win = null |
| 모달 다이얼로그를 부모 없이 띄움 | 부모 위에 떠야 할 모달이 따로 놂 | parent: mainWindow, modal: true |
transparent: true + 큰 GPU 작업 | 프레임 드랍 / 렌더링 글리치 | 정말 필요할 때만, GPU 가속과 함께 검토 |
backgroundThrottling 디폴트로 두고 백그라운드 음악 앱 | 음악이 끊김 | backgroundThrottling: false |
How (advanced) — 잘 안 쓰지만 알아두면 좋은 4가지
A. 부모-자식 창 (모달)
const child = new BrowserWindow({
parent: mainWindow,
modal: true,
width: 400, height: 200,
});
child.loadFile('confirm.html');- macOS: 모달 시트로 부모 창에 물려서 표시.
- Windows: 별도 창이지만 부모 위에 항상 표시되고 부모는 비활성.
B. BrowserView / WebContentsView — 한 창 안의 여러 webContents
const { BrowserWindow, WebContentsView } = require('electron');
const win = new BrowserWindow({ width: 1200, height: 800 });
const view = new WebContentsView();
win.contentView.addChildView(view);
view.setBounds({ x: 0, y: 80, width: 1200, height: 720 });
view.webContents.loadURL('https://example.com');- 사용처: 탭 브라우저, 사이드바 + 메인 콘텐츠 분리, 광고 격리.
BrowserView는 Electron 30+에서 deprecated →WebContentsView로 마이그레이션.
C. nativeTheme — 다크모드 OS 연동
const { nativeTheme } = require('electron');
nativeTheme.themeSource = 'system'; // 'light' | 'dark' | 'system'
nativeTheme.on('updated', () => {
console.log('OS theme is now:', nativeTheme.shouldUseDarkColors ? 'dark' : 'light');
});- 창 생성 시
backgroundColor를 테마에 맞춰 주면 깜빡임이 더 줄어든다.
D. screen — 다중 모니터 정보
const { screen } = require('electron');
const displays = screen.getAllDisplays();
const cursorPoint = screen.getCursorScreenPoint();
const targetDisplay = screen.getDisplayNearestPoint(cursorPoint);
new BrowserWindow({
x: targetDisplay.workArea.x + 100,
y: targetDisplay.workArea.y + 100,
...
});- 멀티 모니터에서 현재 마우스가 있는 모니터에 창을 띄우는 패턴.
E. dialog — 시스템 다이얼로그
const { dialog } = require('electron');
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile', 'multiSelections'],
filters: [{ name: 'Images', extensions: ['png','jpg'] }],
});
// result.canceled, result.filePaths- Main 프로세스에서만 호출 가능. Renderer에서는 IPC로 요청.
Insight — Cheng Zhao의 한 줄
“BrowserWindow는 BrowserView 위에 OS 창을 얹은 것이지, 그 반대가 아니다.”
Electron의 창 모델은 Chromium의
views::Widget위에 만들어졌다. 즉 콘텐츠가 먼저, 창은 나중이라는 설계. 이래서 한 창 안에 여러WebContentsView를 끼울 수 있고, 반대로 창 없이 webContents만 가질 수도 있다 (오프스크린 렌더링).이 설계는 VS Code의 분할 패널, Slack의 호출 위젯, Discord의 보이스 패널 같은 “한 창에 여러 독립 렌더러” 패턴의 토대다.
다음 문서
- 창을 만들었다면 — 02 app 라이프사이클에서 언제 만들고 언제 죽이는가.
- 콘텐츠가 살아 있는 동안 무슨 일이 일어나는가 — 03 webContents.
한 단락 요약
BrowserWindow는 3-in-1 객체다 — OS 핸들 / webContents / 렌더러 프로세스. 그 셋을 동시에 관리한다는 사실을 잊으면 흰 깜빡임, 메모리 누수, XSS → RCE가 매번 같은 자리에서 터진다.show:false패턴, 안전 4-종webPreferences,closed에서의 nullify — 이 세 가지만 지켜도 *기본 함정의 80%*를 피한다.