"커스텀 프로토콜은 file:// 대신 안전하게 로컬 리소스를 로드하는 방법" — CSP와 보안 정책을 지키면서 로컬 파일을 서빙할 수 있습니다.


왜 커스텀 프로토콜이 필요한가

file:// 프로토콜의 문제점:

  • CORS 정책이 엄격하게 적용됨
  • CSP(Content Security Policy) 설정이 제한적
  • 경로 조작 공격에 취약할 수 있음
  • fetch() API가 file://에서 제대로 동작하지 않는 경우가 있음

protocol.handle (Electron 25+)

JAVASCRIPT
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');
});

파일 서빙 패턴

JAVASCRIPT
// 정적 파일 서버처럼 동작하는 프로토콜
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
<!-- HTML에서 사용 -->
<link rel="stylesheet" href="static://./css/style.css" />
<img src="static://./images/logo.png" />
<script src="static://./js/app.js"></script>

동적 콘텐츠 응답

JAVASCRIPT
// 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 이전에는 이 방식을 사용했습니다.

JAVASCRIPT
// 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:// 대신 보안적으로 안전한 리소스 로딩 방법
  • registerSchemesAsPrivilegedapp.whenReady() 전에 호출해야 함
  • protocol.handle(Electron 25+)은 Response 객체를 반환하는 현대적 API
  • 경로 순회 공격 방지를 위한 경로 검증은 필수
  • standard: true로 설정해야 상대 경로, URL 파싱이 정상 동작

protocol 핸들러를 다뤘으면, 다음은 딥링크 구현을 알아봅시다.

댓글 로딩 중...