Context Isolation
Context Isolation이란?
Context Isolation은 preload
스크립트와 Electron의 내부 로직이 webContents
에 로드된 웹사이트와 별도의 컨텍스트에서 실행되도록 보장하는 기능이다. 이는 보안을 위해 중요한데, 웹사이트가 Electron의 내부 로직이나 preload
스크립트가 접근할 수 있는 강력한 API에 접근하는 것을 방지하는 데 도움을 준다.
즉, preload
스크립트가 접근하는 window
객체는 웹사이트가 접근하는 window
객체와 다른 객체라는 의미이다. 예를 들어, preload
스크립트에서 window.hello = 'wave'
를 설정하고 Context Isolation이 활성화된 상태라면, 웹사이트가 window.hello
에 접근하려고 할 때 undefined
가 반환된다.
Context Isolation은 Electron 12부터 기본적으로 활성화되어 있으며, 모든 애플리케이션에 권장되는 보안 설정이다.
컨텍스트 격리 기능을 사용하지 않았을 때는,
window.X = apiObject
를 통해 preload 스크립트에서 API를 제공했다. 이제는 어떻게 해야 할까?
컨텍스트 격리 비활성화 시
렌더러 프로세스에서 로드된 웹사이트로 프리로드 스크립트의 API를 노출하는 것은 일반적인 사용 사례이다. 컨텍스트 격리를 비활성화하면 프리로드 스크립트가 렌더러와 공통의 전역 window
객체를 공유한다. 이 경우 프리로드 스크립트에 임의의 속성을 추가할 수 있다:
// 컨텍스트 격리 비활성화 상태의 프리로드
window.myAPI = {
doAThing: () => {}
}
이제 doAThing()
함수를 렌더러 프로세스에서 직접 사용할 수 있다:
// 렌더러에서 노출된 API 사용
window.myAPI.doAThing()
이후: 컨텍스트 격리 활성화
Electron에서는 이 작업을 쉽게 수행할 수 있도록 전용 모듈을 제공한다. contextBridge
모듈을 사용하면 사전 로드 스크립트의 격리된 컨텍스트에서 웹사이트가 실행 중인 컨텍스트로 API를 안전하게 노출할 수 있다. 이 API는 이전과 마찬가지로 웹사이트에서 window.myAPI
로 접근할 수 있다.
// 컨텍스트 격리가 활성화된 상태에서 사전 로드
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
doAThing: () => {}
})
// 렌더러에서 노출된 API 사용
window.myAPI.doAThing()
위에 링크된 contextBridge
문서를 꼭 읽어서 제한 사항을 완전히 이해하기 바란다. 예를 들어, 커스텀 프로토타입이나 심볼은 브릿지를 통해 전송할 수 없다.
보안 고려사항
contextIsolation
을 활성화하고 contextBridge
를 사용한다고 해서 모든 작업이 안전해지는 것은 아니다. 예를 들어, 아래 코드는 안전하지 않다.
// ❌ 잘못된 코드
contextBridge.exposeInMainWorld('myAPI', {
send: ipcRenderer.send
})
이 코드는 어떤 인자 필터링도 없이 강력한 API를 직접 노출한다. 이는 모든 웹사이트가 임의의 IPC 메시지를 보낼 수 있게 만드는데, 이는 바람직하지 않은 상황이다. IPC 기반 API를 노출하는 올바른 방법은 각 IPC 메시지마다 하나의 메서드를 제공하는 것이다.
// ✅ 올바른 코드
contextBridge.exposeInMainWorld('myAPI', {
loadPreferences: () => ipcRenderer.invoke('load-prefs')
})
TypeScript와 함께 사용하기
TypeScript로 Electron 앱을 개발한다면, context bridge를 통해 노출된 API에 타입을 추가해야 한다. 선언 파일을 사용해 타입을 확장하지 않으면, 렌더러의 window
객체에 올바른 타입 정보가 제공되지 않는다.
예를 들어, 다음과 같은 preload.ts
스크립트가 있다고 가정해 보자:
contextBridge.exposeInMainWorld('electronAPI', {
loadPreferences: () => ipcRenderer.invoke('load-prefs')
})
이제 interface.d.ts
선언 파일을 생성하고, 전역 Window
인터페이스를 확장할 수 있다:
export interface IElectronAPI {
loadPreferences: () => Promise<void>,
}
declare global {
interface Window {
electronAPI: IElectronAPI
}
}
이렇게 하면 TypeScript 컴파일러가 렌더러 프로세스에서 스크립트를 작성할 때, 전역 window
객체의 electronAPI
속성을 인식할 수 있다:
window.electronAPI.loadPreferences()