Electron을 사용하면서 preload.js
를 제대로 이해하고 써야만
보안 강화, 안정성 향상, 성능 최적화를 모두 챙길 수 있습니다.
하지만 많은 초심자들이 preload를
- "그냥 있나보다"
- "대충 쓰면 되지"
정도로 넘깁니다.
그러면 앱이 위험해지고, 유지보수가 어려워지고, 퍼포먼스가 떨어집니다.
오늘 이 글에서
preload의 역할, 작성법, 보안 설정, 고급 패턴까지
처음부터 끝까지 꼼꼼하게 정리해봅시다.
Preload란 무엇인가?
Preload 스크립트는 렌더러 프로세스가 시작되기 전에 실행되는
Node.js 환경의 JavaScript 코드입니다.
쉽게 말하면?
- 메인 프로세스가 브라우저 창을 열 때
- 웹 페이지(렌더러)가 로드되기 전에
- 미리 필요한 기능을 준비하는 중간 다리 역할입니다.
Electron 공식 정의
Preload scripts run in an isolated context before the renderer process is loaded.
Preload를 왜 써야 할까?
Node.js 기능을 UI에 연결하기 위해
렌더러는 원래 Node.js 기능에 직접 접근하지 못합니다. (특히 contextIsolation: true
설정 시)
그래서 preload를 통해 Node.js API (fs
, os
, ipcRenderer
, ...)를 안전하게 노출시켜야 합니다.
보안 강화
Electron의 가장 위험한 지점은
렌더러 프로세스 = 외부 HTML/JS
을 열 수 있다는 점입니다.
렌더러가 Node.js 전체를 쥐고 있으면?
- XSS 한 방에 시스템 파일 삭제 가능
- 백도어 삽입 위험
Preload + contextBridge를 사용하면,
렌더러에 최소한의 기능만 노출할 수 있습니다.
성능 최적화
필요한 라이브러리만 preload에 적재하면
초기 로딩 속도를 높이고, 불필요한 메모리 사용을 줄일 수 있습니다.
기본 Preload 작성하기
Preload 파일 생성
프로젝트에 preload.js
파일을 만듭니다.
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
// expose API to renderer
contextBridge.exposeInMainWorld('electronAPI', {
sendLog: (message) => ipcRenderer.send('log-message', message),
fetchData: (key) => ipcRenderer.invoke('get-data', key)
});
BrowserWindow에 preload 연결
메인 프로세스에서 BrowserWindow
를 만들 때 preload를 지정합니다.
// main.js
const { BrowserWindow } = require('electron');
const path = require('path');
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
enableRemoteModule: false
}
});
주의:
contextIsolation: true
,nodeIntegration: false
는 반드시 설정해야 안전합니다.
Preload 스크립트와 contextBridge
contextBridge
는 렌더러 프로세스에
선별된 안전한 API만 노출하는 모듈입니다.
기본 구조
contextBridge.exposeInMainWorld('key', {
methodName: () => { /* logic */ }
});
key
: 렌더러에서 접근할 때 사용할 전역 객체 이름methodName
: 렌더러가 호출 가능한 함수
렌더러에서는 이렇게 사용
// renderer.js
electronAPI.sendLog('User clicked!');
- Node.js, ipcRenderer 같은 내부 모듈은 직접 접근 불가
- 오직
electronAPI
를 통해서만 접근 가능
보안 관점에서 본 preload
반드시 지켜야 할 설정
옵션 | 설명 |
contextIsolation: true | 렌더러를 분리된 context에서 실행 |
nodeIntegration: false | 렌더러에서 Node.js 기능 차단 |
enableRemoteModule: false | remote 모듈 비활성화 |
(※ Electron 12 이상은 기본적으로
contextIsolation: true
입니다.)
위험한 패턴
아래처럼 preload에서 전역 오염시키면 절대 안됩니다.
// preload.js (BAD)
window.ipcRenderer = require('electron').ipcRenderer;
- 이렇게 하면 렌더러가 ipcRenderer를 직접 조작할 수 있습니다.
- 공격자가 렌더러 스크립트를 탈취하면 시스템 접근 가능.
활용: 다중 preload 관리
프로젝트가 커지면 여러 기능을 preload에 나눌 수 있습니다.
구조 예시
/preload
/modules
- ipc.js
- fileSystem.js
- auth.js
preload.js
preload.js
const { contextBridge } = require('electron');
const ipc = require('./modules/ipc');
const fileSystem = require('./modules/fileSystem');
const auth = require('./modules/auth');
contextBridge.exposeInMainWorld('electronAPI', {
...ipc,
...fileSystem,
...auth
});
모듈별로 관리하면 유지보수성, 확장성이 뛰어나집니다.
실전 예제: 파일 읽기 기능 추가
preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
readFile: (path) => ipcRenderer.invoke('read-file', path)
});
main.js
const fs = require('fs').promises;
ipcMain.handle('read-file', async (event, path) => {
return await fs.readFile(path, 'utf8');
});
renderer.js
async function loadFile(path) {
const content = await window.electronAPI.readFile(path);
console.log(content);
}
정리
- Preload는 렌더러가 시작되기 전 Node.js 코드 실행용
- contextBridge로 안전하게 필요한 기능만 노출
- nodeIntegration: false, contextIsolation: true는 필수
- 보안, 유지보수, 성능을 모두 고려해야 함
- 모듈화하여 구조화하는 습관을 들이자