03 · webContents

이 문서가 답하는 질문: BrowserWindow.webContents는 정확히 무엇이고, 페이지 로드·네비게이션·세션·인쇄·devtools가 왜 모두 거기 붙어 있는가?


한 줄 답 (Pyramid Top)

webContents창의 콘텐츠 영역 그 자체를 표현하는 Chromium 객체다 — 창 껍데기BrowserWindow와 분리된, 실제로 페이지를 로드하고 렌더하고 이벤트를 발생시키는 주체. 그래서 loadURL·reload·print·openDevTools·session·webRequest 같은 *“콘텐츠가 하는 거의 모든 것”*이 여기 모인다.


Why — 왜 BrowserWindow에서 분리됐는가

Chromium의 설계 자체가 “창은 가벼운 껍데기, 콘텐츠는 무거운 실체” 다.

  • 한 창 안에 여러 콘텐츠 영역이 가능해야 한다 (탭 브라우저처럼).
  • 창 없이 콘텐츠만 가지는 경우도 있다 (Offscreen rendering, headless 테스트).
  • 콘텐츠를 다른 창으로 옮길 수도 있다 (win.setBrowserView() 등 전환 패턴).

Electron은 이 분리를 그대로 노출했다.

webContents.id언제든 전역에서 찾을 수 있다 — webContents.fromId(42). IPC 핸들러나 메뉴 콜백에서 주체가 누구인지 식별할 때 1순위로 쓴다.


How — webContents의 7가지 책임

① 로드 — loadURL / loadFile

const win = new BrowserWindow({...});
 
// 로컬 HTML
win.loadFile('index.html');
 
// 외부 URL (보안 주의!)
win.webContents.loadURL('https://example.com/');
 
// 옵션: HTTP 헤더, POST data
win.webContents.loadURL('https://api.example.com/x', {
  extraHeaders: 'Authorization: Bearer xxx\n',
  postData: [{ type: 'rawData', bytes: Buffer.from('hello') }],
});

loadFile vs loadURL('file://...'): 같아 보이지만 loadFileapp.getAppPath() 기준 상대 경로 처리를 알아서 한다. asar 패키징 안의 파일도 그대로 동작. 직접 file:// URL 짜는 것보다 안전.

② 네비게이션 이벤트

이벤트의미preventDefault?
did-start-loading로딩 시작
did-finish-load로드 완료 (성공)
did-fail-load로드 실패 (4xx/5xx, 네트워크 오류)
dom-readyDOM 파싱 완료, 리소스는 아직일 수 있음
will-navigate페이지 안에서 다른 URL로 이동 시도✅ — 외부 URL 차단에 필수
will-redirect서버 리다이렉트 진행 중
did-navigate / did-navigate-in-page이동 완료
console-message페이지의 console.log❌ — 디버깅용
render-process-gone렌더러 프로세스가 죽음 (crash)❌ — 복구 필수

외부 URL 차단 (보안 권장 패턴)

app.on('web-contents-created', (event, wc) => {
  wc.on('will-navigate', (event, url) => {
    if (!url.startsWith('https://myapp.com/')) {
      event.preventDefault();
      require('electron').shell.openExternal(url);
    }
  });
  wc.setWindowOpenHandler(({ url }) => {
    require('electron').shell.openExternal(url);
    return { action: 'deny' }; // 새 BrowserWindow 만들지 않음
  });
});

이 패턴은 05 보안에서 강조하는 방어선의 두 줄will-navigate현재 창의 이동을, setWindowOpenHandler새 창을 막는다.

③ 콘텐츠 제어 — reload·print·executeJavaScript

win.webContents.reload();
win.webContents.reloadIgnoringCache();
win.webContents.goBack();
win.webContents.goForward();
win.webContents.print({ silent: false, printBackground: true });
win.webContents.printToPDF({ pageSize: 'A4' }).then(buf => fs.writeFile('out.pdf', buf));
win.webContents.zoomFactor = 1.25;
 
// JS 직접 실행 (preload 무관)
const title = await win.webContents.executeJavaScript('document.title');

executeJavaScript디버깅용. 운영 코드에서는 IPC(03 IPC)로 통신하는 게 안전. 외부 콘텐츠에서 호스트 코드 실행은 RCE 통로가 될 수 있다.

④ DevTools 제어

win.webContents.openDevTools({ mode: 'detach' });
// 'right' | 'bottom' | 'undocked' | 'detach' (별도 창)
 
win.webContents.toggleDevTools();
win.webContents.closeDevTools();
  • 프로덕션 빌드에서는 단축키만 막아도 우회 가능 — nodeIntegration: false보안 디폴트로 막아야 한다.
  • DevTools 자체는 로컬 렌더러이므로 원격 코드와 같은 보안 모델은 아니다.

⑤ session — 쿠키/캐시/스토리지

win.webContents.session.cookies.set({
  url: 'https://example.com',
  name: 'token',
  value: 'abc',
  httpOnly: true,
});
 
const all = await win.webContents.session.cookies.get({});

session은 별도 챕터 04에서 본격 다룬다 — 그저 webContents의 한 부속이라는 것만 기억.

⑥ webRequest — 모든 네트워크 가로채기

win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
  details.requestHeaders['X-App-Version'] = app.getVersion();
  cb({ requestHeaders: details.requestHeaders });
});
 
win.webContents.session.webRequest.onBeforeRequest(
  { urls: ['*://ads.example.com/*'] },
  (details, cb) => cb({ cancel: true }) // 광고 차단
);
  • 모든 fetch/XHR/이미지 요청을 가로챌 수 있다. Chrome 확장의 webRequest API와 같은 모델.
  • Authorization 헤더 자동 주입, 광고 차단, 오프라인 캐시, 비밀번호 자동입력 차단 등의 용도.

⑦ render-process-gone — 크래시 복구

win.webContents.on('render-process-gone', (event, details) => {
  console.error('Renderer crashed:', details.reason);
  // reason: 'crashed' | 'killed' | 'oom' | 'launch-failed' | ...
  if (details.reason === 'oom') {
    dialog.showErrorBox('Out of memory', '메모리 부족으로 페이지가 종료되었습니다.');
  }
  win.webContents.reload(); // 자동 복구
});

Electron 9+: 기존 crashed 이벤트가 render-process-gone으로 변경 + reason 정보 추가.


What — webContents 이벤트 흐름 한 그림


What-if — 흔한 함정

함정증상해법
did-finish-load 안 기다리고 IPCrenderer가 아직 listen 안 함 → 메시지 유실wc.once('did-finish-load', ...)
will-navigate 핸들러 없음외부 링크 클릭에 앱 안에서 다른 사이트가 뜸 → XSS 통로app.on('web-contents-created', ...) 일괄 처리
setWindowOpenHandler 없음target="_blank" 클릭에 또 다른 Electron 창이 뜸모든 webContents에 등록
executeJavaScript로 페이지 데이터 추출preload bridge 우회 — 원래는 IPC로 해야 함IPC + contextBridge
render-process-gone 무시렌더러 죽으면 흰 창만 남음자동 reload 또는 안내
webRequest로 모든 요청 인터셉트 + 무거운 로직페이지 로딩이 느림URL 필터로 좁히기 ({ urls: [...] })
printToPDF 후 임시 파일 안 지움디스크 누수finally에서 unlink

How (advanced) — 자주 쓰진 않지만 알아두면 좋은

A. setWindowOpenHandler — 새 창을 허용하되 커스터마이즈

wc.setWindowOpenHandler(({ url, frameName, features }) => {
  if (url.startsWith('https://myapp.com/popup')) {
    return {
      action: 'allow',
      overrideBrowserWindowOptions: {
        width: 600, height: 400,
        webPreferences: { sandbox: true, contextIsolation: true },
      },
    };
  }
  require('electron').shell.openExternal(url);
  return { action: 'deny' };
});

B. findInPage — 페이지 내 검색

const requestId = wc.findInPage('keyword');
wc.on('found-in-page', (event, result) => {
  console.log(`${result.activeMatchOrdinal}/${result.matches}`);
});
wc.stopFindInPage('clearSelection');

C. Offscreen rendering — 창 없이 픽셀만

const win = new BrowserWindow({
  show: false,
  webPreferences: { offscreen: true },
});
win.loadURL('https://example.com');
win.webContents.on('paint', (event, dirty, image) => {
  fs.writeFile('frame.png', image.toPNG());
});
win.webContents.setFrameRate(30);
  • 스크린샷 자동화, 미리보기 생성, 동영상 캡처 등에 사용.

D. wc.session vs session.fromPartition()

  • wc.session — 그 webContents가 지금 쓰고 있는 세션. 읽기 권장.
  • session.fromPartition('persist:user-a')새 세션을 명시적으로 만들어 BrowserWindow 생성 시 주입.
const userSession = require('electron').session.fromPartition('persist:user-a');
const win = new BrowserWindow({
  webPreferences: { session: userSession, ... }
});

04에서 본격.


Insight — webContents가 진짜 단위인 이유

“창은 OS 단위, 콘텐츠는 Chromium 단위”

Chromium 내부에서 권한·격리·세션·플러그인 ID가 전부 WebContents를 키로 한다. BrowserWindow는 그것을 OS 창에 매핑하는 어댑터에 불과하다. 그래서 멀티 탭 브라우저를 Electron으로 만들 때는 BrowserWindow 1개 + WebContentsView N개 패턴이 맞다 — 탭마다 BrowserWindow를 만들면 OS 창 N개가 되어 dock/작업표시줄을 어지럽힌다.

이게 VS Code가 한 창 안에 Extension Host webContents + 메인 webContents + Output webContents를 끼우는 이유.


다음 문서

  • 콘텐츠가 가진 쿠키/캐시/스토리지04 session
  • 여러 콘텐츠를 어떻게 관리하는가05 다중 창
  • 콘텐츠가 외부와 연결되는 통로06 protocol

한 단락 요약

webContents창과 분리된 콘텐츠 실체다 — Chromium의 WebContents를 그대로 노출한 객체이고, 로드·네비게이션·세션·webRequest·print·devtools·크래시가 모두 여기 모인다. 보안의 두 줄 — will-navigate 차단 + setWindowOpenHandler deny — 은 모든 외부 콘텐츠를 로드하는 Electron 앱이 무조건 갖춰야 하는 방어선이다.