03 · webContents
이 문서가 답하는 질문:
BrowserWindow.webContents는 정확히 무엇이고, 페이지 로드·네비게이션·세션·인쇄·devtools가 왜 모두 거기 붙어 있는가?
한 줄 답 (Pyramid Top)
webContents는 창의 콘텐츠 영역 그 자체를 표현하는 Chromium 객체다 — 창 껍데기 인BrowserWindow와 분리된, 실제로 페이지를 로드하고 렌더하고 이벤트를 발생시키는 주체. 그래서loadURL·reload·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') }],
});
loadFilevsloadURL('file://...'): 같아 보이지만loadFile은app.getAppPath()기준 상대 경로 처리를 알아서 한다. asar 패키징 안의 파일도 그대로 동작. 직접file://URL 짜는 것보다 안전.
② 네비게이션 이벤트
| 이벤트 | 의미 | preventDefault? |
|---|---|---|
did-start-loading | 로딩 시작 | ❌ |
did-finish-load | 로드 완료 (성공) | ❌ |
did-fail-load | 로드 실패 (4xx/5xx, 네트워크 오류) | ❌ |
dom-ready | DOM 파싱 완료, 리소스는 아직일 수 있음 | ❌ |
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 확장의
webRequestAPI와 같은 모델. - 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 안 기다리고 IPC | renderer가 아직 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으로 만들 때는BrowserWindow1개 +WebContentsViewN개 패턴이 맞다 — 탭마다 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차단 +setWindowOpenHandlerdeny — 은 모든 외부 콘텐츠를 로드하는 Electron 앱이 무조건 갖춰야 하는 방어선이다.