protocol 핸들러 — 커스텀 프로토콜 등록
"커스텀 프로토콜은 file:// 대신 안전하게 로컬 리소스를 로드하는 방법" — CSP와 보안 정책을 지키면서 로컬 파일을 서빙할 수 있습니다.
왜 커스텀 프로토콜이 필요한가
file:// 프로토콜의 문제점:
- CORS 정책이 엄격하게 적용됨
- CSP(Content Security Policy) 설정이 제한적
- 경로 조작 공격에 취약할 수 있음
fetch()API가file://에서 제대로 동작하지 않는 경우가 있음
protocol.handle (Electron 25+)
const { app, protocol, net } = require('electron');
const path = require('path');
const { pathToFileURL } = require('url');
// 앱이 준비되기 전에 프로토콜 등록
protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
privileges: {
standard: true, // 표준 URL 파싱 적용
secure: true, // HTTPS와 같은 보안 수준
supportFetchAPI: true, // fetch() 사용 가능
corsEnabled: true, // CORS 허용
},
},
]);
app.whenReady().then(() => {
// 커스텀 프로토콜 핸들러 등록
protocol.handle('app', (request) => {
const url = new URL(request.url);
// app://resources/style.css → /path/to/app/resources/style.css
const filePath = path.join(__dirname, url.pathname);
// 경로 순회 공격 방지
if (!filePath.startsWith(__dirname)) {
return new Response('Forbidden', { status: 403 });
}
return net.fetch(pathToFileURL(filePath).toString());
});
const win = new BrowserWindow({ /* ... */ });
// 커스텀 프로토콜로 HTML 로드
win.loadURL('app://./index.html');
});
파일 서빙 패턴
// 정적 파일 서버처럼 동작하는 프로토콜
protocol.handle('static', (request) => {
const url = new URL(request.url);
const resourcesPath = path.join(app.getAppPath(), 'resources');
const filePath = path.join(resourcesPath, url.pathname);
// 보안: 허용된 디렉토리 내인지 확인
const normalizedPath = path.normalize(filePath);
if (!normalizedPath.startsWith(resourcesPath)) {
return new Response('접근 거부', { status: 403 });
}
// MIME 타입 자동 감지
return net.fetch(pathToFileURL(normalizedPath).toString());
});
<!-- HTML에서 사용 -->
<link rel="stylesheet" href="static://./css/style.css" />
<img src="static://./images/logo.png" />
<script src="static://./js/app.js"></script>
동적 콘텐츠 응답
// API처럼 동적 응답을 반환하는 프로토콜
protocol.handle('api', async (request) => {
const url = new URL(request.url);
if (url.pathname === '/config') {
const config = await loadConfig();
return new Response(JSON.stringify(config), {
headers: { 'Content-Type': 'application/json' },
});
}
if (url.pathname === '/status') {
return new Response(JSON.stringify({
uptime: process.uptime(),
memory: process.memoryUsage(),
version: app.getVersion(),
}), {
headers: { 'Content-Type': 'application/json' },
});
}
return new Response('Not Found', { status: 404 });
});
레거시 API (protocol.registerFileProtocol)
Electron 25 이전에는 이 방식을 사용했습니다.
// Electron 24 이하에서 사용하던 방식
protocol.registerFileProtocol('app', (request, callback) => {
const url = request.url.replace('app://', '');
const filePath = path.join(__dirname, url);
callback({ path: filePath });
});
새 프로젝트에서는 protocol.handle을 사용하세요.
면접 포인트 정리
- 커스텀 프로토콜은
file://대신 보안적으로 안전한 리소스 로딩 방법 registerSchemesAsPrivileged는app.whenReady()전에 호출해야 함protocol.handle(Electron 25+)은Response객체를 반환하는 현대적 API- 경로 순회 공격 방지를 위한 경로 검증은 필수
standard: true로 설정해야 상대 경로, URL 파싱이 정상 동작
protocol 핸들러를 다뤘으면, 다음은 딥링크 구현을 알아봅시다.
댓글 로딩 중...