⚡ Electron2. 창 & 라이프사이클01 · BrowserWindow의 해부

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가지

    1. OS에 네이티브 창 생성 요청 (Win32/Cocoa/X11/Wayland API 호출).
    2. 그 창에 Chromium의 WebContentsView 를 자식으로 붙임.
    3. WebContents에 대응하는 새 렌더러 프로세스 fork(Chromium의 zygote 메커니즘).
    4. 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+)의미안전 권장
1preloadundefined렌더러 시작 전 실행되는 격리된 스크립트 경로필수 (있어야 contextBridge 가능)
2contextIsolationtruepreload와 페이지 JS의 컨텍스트 분리항상 true
3sandboxtrue (preload 있어도)렌더러를 Chromium sandbox에 가둠true 권장
4nodeIntegrationfalse렌더러에서 require/process 직접 노출항상 false
5nodeIntegrationInWorkerfalseWeb Worker에서 Node API거의 모든 경우 false
6webSecuritytrueCORS·same-origin 강제항상 true (개발 중에도)
7allowRunningInsecureContentfalsehttps 페이지의 http 리소스 허용false
8experimentalFeaturesfalseChromium 실험 기능false
9enableBlinkFeatures''특정 Blink 기능 강제 켜기보통 비움
10defaultEncoding'ISO-8859-1'기본 텍스트 인코딩'UTF-8'로 변경 권장
11backgroundThrottlingtrue비활성 창의 타이머 throttle음악/모니터링 앱은 false
12spellchecktrue내장 스펠체커입력 앱은 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가지 사실

#사실
1BrowserWindow 1개 = OS 창 1개 + webContents 1개 + 렌더러 프로세스 1개
2한 창의 메모리 비용은 보통 50~80MB (빈 페이지 기준), 콘텐츠 따라 200MB+
3show: false + ready-to-show + show() 패턴은 흰 깜빡임 제거의 표준
4webPreferences 30+ 옵션 중 안전 4-종 세트contextIsolation·sandbox·nodeIntegration:false·webSecurity
5close 이벤트는 preventDefault()로 막을 수 있다 — “정말 종료?” 확인 다이얼로그 패턴
6closed 이벤트에서 반드시 창 참조를 nullify (또는 Set/Map에서 제거)
7BrowserWindow.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의 보이스 패널 같은 “한 창에 여러 독립 렌더러” 패턴의 토대다.


다음 문서


한 단락 요약

BrowserWindow3-in-1 객체다 — OS 핸들 / webContents / 렌더러 프로세스. 그 셋을 동시에 관리한다는 사실을 잊으면 흰 깜빡임, 메모리 누수, XSS → RCE가 매번 같은 자리에서 터진다. show:false 패턴, 안전 4-종 webPreferences, closed에서의 nullify — 이 세 가지만 지켜도 *기본 함정의 80%*를 피한다.