IPC 모듈화·세션 분리·m3u8 가로채기·크로스플랫폼 패키징을 한 레포에 다 담은 데스크탑 앱의 안쪽.
왜 웹 브라우저가 아니라 데스크탑 앱으로 만드는가.
VS Code, Slack, Discord, Figma 데스크탑 — 우리가 매일 쓰는 데스크탑 앱 상당수가 사실은 Chromium 브라우저 + Node.js로 만들어졌다. 이 조합을 묶어 데스크탑 앱처럼 보이게 해주는 프레임워크가 Electron이다. 웹 기술(HTML/CSS/JS)을 쓰면서도 파일 시스템 접근, OS API, 시스템 트레이, 알림 같은 네이티브 기능을 쓸 수 있다.
Streambert는 영화·드라마·애니메이션을 외부 스트리밍 제공자에서 끌어와 재생하는 데스크탑 앱이다. 핵심은 광고와 추적기를 네트워크 레벨에서 100% 차단하면서 m3u8 스트림만 통과시키는 미들웨어 구조다. 이 한 가지를 위해 Electron의 모든 기능 — Session, webRequest, BrowserWindow, IPC — 을 본격적으로 활용한다.
ipcMain, ipcRenderer 양쪽에서 채널 이름으로 메시지를 송수신한다..m3u8이라는 텍스트 플레이리스트에 그 조각들의 URL을 나열한다. 네트워크가 느려지면 자동으로 낮은 화질로 전환된다.session.webRequest.onBeforeRequest로 앱이 발생시키는 모든 네트워크 요청을 가로채고 취소·수정할 수 있다. 광고 차단기·VPN·프록시의 기반 기술.한 문장으로 줄이면.
콘텐츠 자체는 외부(VidSrc·videasy·2Embed·AllManga.to)에 있고, Streambert는 운영 레이어다. 핵심 기능은 세 가지 — (1) 메타데이터는 TMDB/AniList에서 가져와 UI 구성, (2) 영상은 외부 임베드를 webview로 띄우되 광고만 네트워크에서 차단, (3) 다섯 가지 OS 패키지로 한 번에 빌드.
"웹사이트의 광고 지옥을 데스크탑 앱이 어떻게 해결하는가"라는 질문에 대한 가장 구체적이고 완성도 높은 답안 중 하나다.
README에도 법적 주의문이 있다. 이 문서는 콘텐츠 사용 패턴을 권장하지 않는다 — 순수하게 기술 아키텍처(Electron 보안 모델, IPC 구조, 광고 차단 미들웨어, 메모리 관리, 크로스플랫폼 빌드)를 학습 자료로 다룬다.
차별점 + 경쟁 우위.
session.webRequest.onBeforeRequest로 네트워크 레벨 차단 + 통계 시각화까지 제공.electron-builder 하나로 빌드.storage·downloads·subtitles·allmanga·player·blockStats 6개 IPC 모듈로 분리.vid-dl-cli-only)에 넘김 (관심사 분리).일반 스트리밍 사이트(웹브라우저로 접속)는 여러 광고 손님과 같은 테이블에 앉는 카페테리아다. Streambert는 같은 음식(영상)을 가져오긴 하지만, 광고 손님을 입구에서 막고 사용자를 독립된 룸(BrowserWindow + persist:player 세션)에 안내한다. 음식의 출처는 같지만, 식사 경험은 다르다.
| 비교 항목 | 브라우저로 직접 시청 | Streambert |
|---|---|---|
| 광고 차단 | uBlock 등 확장에 의존 | 네트워크 레벨 강제 차단 |
| 추적기 | 잔존 가능 | 50+ 도메인 사전 차단 |
| UI 일관성 | 사이트마다 다름 | 통일된 React UI |
| 메타데이터 | 사이트 의존 | TMDB/AniList 통합 |
| 다운로드 | 대부분 차단 | 외부 CLI로 위임 |
| PiP | 브라우저 PiP API | 네이티브 alwaysOnTop 창 |
메인 · 렌더러 · 외부 API · 패키징.
ELECTRON_DIST=1)dist/로 빌드ani-cli 패턴 차용)asarUnpack으로 폰트만 예외)전체 구조도 → 핵심 패턴 하나하나.
┌────────────────────────────────────────────────────────────────────┐ │ 사용자 데스크탑 │ │ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ Electron 앱 (Streambert) │ │ │ │ │ │ │ │ ┌─────────────────────┐ ┌──────────────────────────┐ │ │ │ │ │ MAIN PROCESS │ IPC │ RENDERER (React) │ │ │ │ │ │ index.js │◄────►│ src/App.jsx │ │ │ │ │ │ │ │ + pages, components │ │ │ │ │ │ - BrowserWindow │ │ - HomePage │ │ │ │ │ │ - Session 제어 │ │ - MoviePage / TVPage │ │ │ │ │ │ - IPC 핸들러: │ │ - LibraryPage │ │ │ │ │ │ · storage │ │ - SettingsPage │ │ │ │ │ │ · downloads │ │ - DownloadsPage │ │ │ │ │ │ · subtitles │ │ │ │ │ │ │ │ · allmanga │ │ ┌────────────────────┐ │ │ │ │ │ │ · player │ │ │ <webview> │ │ │ │ │ │ │ · blockStats │ │ │ persist:player │ │ │ │ │ │ │ - 광고 차단 미들웨어 │ │ │ (VidSrc 임베드) │ │ │ │ │ │ └─────────┬───────────┘ │ └────────────────────┘ │ │ │ │ │ │ └──────────────────────────┘ │ │ │ │ ▼ │ │ │ │ preload.js (contextBridge) │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ ▲ ▲ │ │ │ HTTPS/메타데이터 │ HTTPS/m3u8 스트림 │ │ ▼ ▼ │ │ ┌─────────────────────┐ ┌──────────────────────────────┐ │ │ │ TMDB / AniList │ │ VidSrc / AllManga.to / │ │ │ │ (메타데이터) │ │ videasy.net / 2Embed │ │ │ └─────────────────────┘ └──────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 외부 CLI 의존성: vid-dl-cli-only + ffmpeg (다운로드 시) │ │ │ └─────────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────────┘
Electron은 한 앱 안에 세 가지 종류의 프로세스가 동시에 돈다. 각자 역할이 다르고, 메모리도 공유하지 않는다.
index.js). Renderer는 Chromium 환경, 창마다 하나씩(src/App.jsx 트리). Preload는 둘 사이의 안전한 다리, contextBridge.exposeInMainWorld로 화이트리스트한 API만 노출(preload.js).Main = 총무팀(예산 집행, 출입증 발급, 사무용품 구매). Renderer = 현장 직원(고객 응대 = UI 표시). Preload = 총무팀이 미리 정해둔 신청 양식. 현장 직원은 양식에 적힌 항목만 신청할 수 있고, 신청서를 받은 총무팀이 검토 후 실행한다. 직원이 직접 금고를 열 수는 없다.
Streambert는 한 앱 안에서 3개의 분리된 Electron 세션을 운영한다. 각 세션은 독립된 쿠키 저장소·캐시·권한을 가진다.
// index.js 발췌
const playerSession = session.fromPartition("persist:player"); // 영화/시리즈 재생용
const trailerSession = session.fromPartition("persist:trailer"); // 예고편 재생용
const redeemSession = session.fromPartition("partition:wyzie-redeem"); // 자막 키 (비영구)
왜 분리하나? 플레이어 세션엔 광고 차단 + m3u8 가로채기를 강하게 걸고, 트레일러 세션엔 가벼운 차단만 건다. 자막 발급 세션은 partition: 접두사(persist: 없음)로 영구 저장하지 않아 보안에 유리하다.
const BLOCKED_HOSTS = [
"*://www.google-analytics.com/*",
"*://googletagmanager.com/*",
"*://doubleclick.net/*",
// ... 50여 개의 광고/추적기 도메인
];
playerSession.webRequest.onBeforeRequest(
{ urls: [...BLOCKED_HOSTS, ...MEDIA_URLS] },
(details, callback) => {
const { url } = details;
const isMedia = url.includes(".m3u8") || url.includes(".vtt");
if (!isMedia) {
blockStats.recordBlockedRequest(url); // 통계 기록
callback({ cancel: true }); // 요청 취소!
return;
}
// 미디어 URL은 통과 + 렌더러에 알림
mainWindow.webContents.send("m3u8-found", url);
callback({}); // 요청 허용
}
);
① 광고 도메인 차단, ② 차단 통계 기록, ③ m3u8 URL을 가로채서 렌더러에 알림. 미들웨어 패턴(요청 가로채기)의 좋은 예다. Express.js의 app.use((req, res, next) => {...})와 거의 같은 개념을 데스크탑 앱 네트워크 레이어에 적용한 것.
index.js가 통째로 길어지지 않도록, IPC 핸들러를 도메인별 모듈로 쪼갰다. 각 모듈은 register() 함수를 export하고, index.js는 그냥 호출만 한다.
src/ipc/ ├── allmanga.js ← 애니 스크레이퍼 (AllManga.to) ├── blockStats.js ← 광고 차단 통계 저장/조회 ├── downloads.js ← 다운로드 큐, 외부 CLI 호출 ├── player.js ← 플레이어 제어 (mute, fullscreen) ├── storage.js ← TMDB 키, 설정, 시크릿 마이그레이션 └── subtitles.js ← 자막 다운로드 (Wyzie)
storageIpc.register();
downloadsIpc.register(getMainWindow);
subtitlesIpc.register({ getDownloads, saveDownloads });
allmangaIpc.register();
playerIpc.register(getMainWindow, { writeSecretMigration });
blockStats.init(getMainWindow);
영상을 작은 별도 창으로 띄우는 PiP 기능. 새 BrowserWindow를 만들고 alwaysOnTop: true로 설정하되, 같은 세션 파티션을 공유해 광고 차단 규칙이 자동 적용된다.
pipWindow = new BrowserWindow({
width: 640, height: 360,
minWidth: 320, minHeight: 180,
alwaysOnTop: true, // 다른 창 위에 항상 표시
webPreferences: {
partition: "persist:player", // 같은 세션 공유 → 광고 차단 그대로
contextIsolation: true,
nodeIntegration: false,
preload: "popout-preload.js"
}
});
장시간 재생 시 메모리가 쌓이지 않게 적극적으로 관리한다. Electron 앱이 부풀어 오르는 흔한 문제를 정면 대응.
--max-old-space-size=256으로 OldGen 힙을 256MB로 제한--renderer-process-limit=3으로 동시에 3개까지만--disk-cache-size=80MBwebContents.destroy() → 캐시 플러시 → 수동 GC 호출// player-stopped 이벤트 처리 (영상 페이지 unmount 시 발생)
ipcMain.on("player-stopped", () => {
// 1) 추적 중인 player webContents 모두 destroy
for (const id of playerWcIds) {
const wc = webContents.fromId(id);
if (wc && !wc.isDestroyed()) {
wc.setAudioMuted(true);
wc.destroy();
}
}
// 2) 플레이어 세션 캐시 플러시
const ps = session.fromPartition("persist:player");
ps.clearCache();
ps.clearStorageData({ storages: ["shadercache", "cachestorage"] });
// 3) GC 힌트 (메인 + 렌더러)
if (typeof global.gc === "function") global.gc();
});
Streambert는 임베드 사이트의 CSP와 X-Frame-Options 헤더를 제거해서 동작시킨다 (보안 트레이드오프). 본인이 만드는 앱에서 이 패턴을 그대로 따라하면 보안 위협이 될 수 있다 — 악성 스크립트가 들어올 통로가 생긴다. 학습 차원에서는 "왜 보통은 이걸 안 건드리는가"를 이해하는 게 더 중요하다.
어디에 뭐가 있는지.
streambert/ ├── index.js # 핵심 Main process 엔트리 (창 생성, 광고 차단, IPC 등록) ├── preload.js # 메인 창용 contextBridge (화이트리스트 API) ├── popout-preload.js # PiP 창용 별도 preload (타이틀바 컨트롤만 노출) ├── index.html # Vite 빌드 진입 HTML ├── vite.config.js # Vite 설정 (React 플러그인 등) ├── package.json # 의존성 + electron-builder 설정 ├── tmdb-tutorial.md # 사용자 가이드: TMDB API 키 발급 │ ├── public/ # 정적 자산 (아이콘, 설치 사이드바) │ ├── icon.png │ ├── installer-sidebar.bmp # NSIS 설치 마법사 좌측 이미지 │ ├── logo.svg │ └── sized/ # Linux용 다해상도 아이콘 │ ├── src/ # 렌더러 소스 │ ├── main.jsx # React 진입점 (createRoot) │ ├── App.jsx # 라우팅 + 전역 상태 │ │ │ ├── components/ # 재사용 컴포넌트 │ │ ├── Sidebar.jsx │ │ ├── MediaCard.jsx │ │ ├── SetupScreen.jsx # 최초 실행 시 TMDB 키 입력 │ │ ├── SearchModal.jsx │ │ ├── DownloadModal.jsx │ │ ├── SubtitleDownloaderModal.jsx │ │ ├── TrailerModal.jsx # YouTube 트레일러 (별도 세션) │ │ ├── TrendingCarousel.jsx │ │ ├── ErrorBoundary.jsx │ │ ├── KeyboardShortcutsModal.jsx │ │ ├── BlockedStatsModal.jsx # 광고 차단 통계 시각화 │ │ ├── UpdateModal.jsx │ │ ├── CloseConfirmModal.jsx # 다운로드 중 종료 확인 │ │ ├── WindowTitlebar.jsx # 커스텀 타이틀바 │ │ └── Icons.jsx │ │ │ ├── pages/ # 라우트 단위 페이지 │ │ ├── HomePage.jsx │ │ ├── MoviePage.jsx │ │ ├── TVPage.jsx │ │ ├── DownloadsPage.jsx │ │ ├── LibraryPage.jsx # 시청 기록, 위시리스트 │ │ └── SettingsPage.jsx │ │ │ ├── ipc/ # 메인 프로세스 IPC 모듈 │ │ ├── allmanga.js │ │ ├── blockStats.js │ │ ├── downloads.js │ │ ├── player.js │ │ ├── storage.js │ │ └── subtitles.js │ │ │ ├── styles/ │ │ ├── global.css │ │ └── fonts/ # Bebas Neue, DM Sans (로컬 호스팅, asarUnpack 예외) │ │ │ └── utils/ # 렌더러 유틸 │ ├── api.js # TMDB API 호출 래퍼 │ ├── storage.js # localStorage 추상화 │ ├── subtitles.js │ ├── updates.js # 앱 자동 업데이트 체크 │ ├── backup.js # 스케줄 백업 │ ├── ageRating.js │ ├── aniSkip.js # 애니 오프닝/엔딩 스킵 정보 │ ├── episodeMappings.js │ ├── homeLayout.js │ ├── appearance.js # 테마/UI 설정 │ ├── useBlockedStats.js # React hook │ └── useRatings.js # React hook │ ├── screenshots/ # README 이미지들 ├── .github/ # GitHub Actions, 이슈 템플릿 ├── .gitignore └── LICENSE # GPL-3.0
Main과 Renderer가 물리적으로 분리돼 있다 — Main 코드는 루트의 index.js, Renderer는 src/ 아래. IPC 모듈을 한 폴더(src/ipc/)에 모아 메인 프로세스 코드가 흩어지지 않는다. 재사용 가능한 것은 components/, 라우트 단위는 pages/로 React 표준 컨벤션. React hook은 use 접두사(useBlockedStats.js) 규약을 그대로 지킨다.
기술 영역마다 무엇을 가져갈지.
contextIsolation: true + nodeIntegration: false = 보안 권장 설정BrowserWindow 옵션 — 프레임 제거, 타이틀바 커스텀실습: 빈 Electron 앱에 "Hello World" 렌더러 띄우기
ipcMain.handle / ipcMain.on 차이 (응답 받을지 여부)contextBridge.exposeInMainWorld로 안전한 API 노출register())실습: 카운터 앱 — 버튼 누르면 메인에서 값을 +1 해서 응답
onBeforeRequest — 요청 전 미들웨어 (취소 가능)onHeadersReceived — 응답 헤더 수정 (CSP/X-Frame-Options 제거)실습: 특정 도메인의 이미지 요청을 모두 회색 placeholder로 바꾸기
persist:NAME — 디스크에 저장partition:NAME — 휘발성 (보안용)실습: 한 앱에서 두 계정 동시 로그인 (세션 분리)
dist/)를 loadFile로 로드vite build --watch로 구현useBlockedStats 패턴)실습: create-electron-vite 템플릿으로 React + Electron 시작
실습: 무료 샘플 m3u8을 ffmpeg로 mp4로 변환
electron-builder의 build 설정 구조실습: 본인 앱을 .exe, .deb 두 형식으로 빌드해보기
--max-old-space-size)--expose-gc + global.gc() 수동 호출session.clearCache, clearStorageDatawebContents.destroy()로 명시적 정리실습: Activity Monitor로 메모리 추적하며 GC 효과 확인
개발용 vs 사용자용.
| 항목 | 요구사항 | 비고 |
|---|---|---|
| OS | Windows 10/11, macOS 11+, Linux | 크로스플랫폼 |
| Node.js | ≥ 22.12.0 | 소스에서 빌드할 때만 |
| RAM | 최소 2GB, 권장 4GB | V8 힙이 256MB로 캡 |
| 저장공간 | 500MB+ 앱 + 다운로드용 추가 | 경로는 사용자 지정 |
| 외부 도구 | ffmpeg, vid-dl-cli-only | 다운로드 시 필요 |
| API 키 | TMDB Read Access Token (무료) | themoviedb.org에서 발급 |
| 인터넷 | 스트리밍 가능한 안정적 연결 | m3u8 = 가변 비트레이트 |
Windows에서 직접 빌드 시 NSIS와 일부 네이티브 모듈을 위해 Visual C++ Build Tools가 필요할 수 있다. 또 코드사이닝 인증서가 없으면 SmartScreen 경고가 뜬다 — 학습용이면 무시 가능하지만 배포 시엔 인증서 구매 권장.
난이도별 실습 — Easy → Hard.
1) npx create-electron-app my-first-app로 시작 2) BrowserWindow로 "Hello"를 띄워본다 3) app.whenReady(), window-all-closed 이벤트의 의미 확인
배우는 것: Main 프로세스의 라이프사이클
1) 렌더러에서 window.myAPI.getVersion()을 호출하면 Electron 버전을 돌려주는 다리 만들기 2) contextBridge.exposeInMainWorld 사용 3) nodeIntegration: false인데도 동작하는 이유 확인
배우는 것: contextBridge, 보안 격리 모델
1) git clone 2) npm install && npm run start로 dev 모드 실행 3) npm run dist:win으로 설치파일 만들기 4) dist/ 폴더의 결과물 확인
배우는 것: electron-builder 워크플로
1) Streambert의 BLOCKED_HOSTS 패턴 차용 2) 본인이 자주 보는 사이트의 광고만 차단하는 간단한 Electron 브라우저 작성 3) 차단한 요청 수를 통계 모달로 표시 4) onHeadersReceived로 CSP 헤더 분석
배우는 것: webRequest 미들웨어 패턴, 차단 통계 UX
1) Streambert의 vite.config.js, package.json 스크립트 참고 2) 본인만의 시작 템플릿 만들기 3) Hot reload가 되도록 vite build --watch 활용 4) 메인 프로세스 변경 시 재시작 자동화
배우는 것: Vite + Electron 통합, HMR 우회 패턴
1) 간단한 메모장 앱 만들기 2) src/ipc/notes.js를 분리해 register() 패턴으로 IPC 핸들러 등록 3) 파일 시스템에 JSON으로 저장 4) 렌더러는 window.notesAPI.save(...)만 호출
배우는 것: IPC 모듈화, 책임 분리
1) GitHub Actions로 푸시할 때마다 Windows/Linux/macOS 빌드를 자동 생성 2) 릴리스 태그에 첨부 3) Streambert의 .github/를 참고 4) 빌드 매트릭스로 OS별 러너 분리
배우는 것: CI/CD, electron-builder + GitHub Releases
1) vid-dl-cli-only를 외부에 두지 않고 앱 안에서 직접 구현 2) ffmpeg-static과 fluent-ffmpeg로 m3u8 → mp4 변환 3) 진행률 IPC로 렌더러에 스트리밍 4) 일시정지/취소 지원
배우는 것: 외부 프로세스 관리, 스트림 처리
1) electron-updater 통합 2) GitHub Releases를 업데이트 채널로 사용 3) Windows 코드사이닝 인증서 적용 4) SmartScreen 경고 사라지는지 확인 5) macOS notarization 도전
배우는 것: 프로덕션 배포, 코드사이닝 전체 흐름
주차별로 따라가면 전체 스택 체득.
공식 Tutorial 정독 + Security checklist → 실습 1, 2 완수. Main/Renderer/Preload 구조를 손으로 그려 설명할 수 있을 때까지.
Streambert index.js 정독 → 실습 4, 6 완수. 광고 차단 미들웨어와 세션 분리의 의미를 본인 앱에 적용.
vite-plugin-electron vs electron-vite 비교 → 실습 5 완수. 본인만의 보일러플레이트 완성, HMR 작동 확인.
electron-builder 문서 + GitHub Actions → 실습 7, 9 도전. 5개 OS 패키지 자동 빌드 파이프라인 구축.
webview 대신 native <video>로 처리하는 길.자주 마주칠 단어들.
ipcMain, ipcRenderer)가 필요.contextIsolation: true 필수.window를 공유하지 않게 격리. XSS 공격 면역이 커진다. Electron 12부터 기본값 true.<video> 태그의 <track>으로 직접 로드 가능. Streambert는 .vtt URL도 가로채서 자막으로 노출.electron-builder가 내부적으로 사용.asarUnpack으로 폰트 등 예외 처리.<iframe>보다 더 격리됨.app.requestSingleInstanceLock().onBeforeRequest, onHeadersReceived로 요청/응답 가로채기 가능. Electron에서 그대로 사용.--max-old-space-size로 캡 가능.공식 · Electron 학습 · 관련 라이브러리.
npm install && npm run start로 Streambert를 직접 띄워본다. TMDB 키 발급해서 실제로 검색·재생 흐름을 끝까지 경험. 광고 차단 통계 모달도 열어본다.index.js 정독. Main 프로세스 코드를 처음부터 끝까지 읽으며 — 창 생성, 세션 분리, webRequest 미들웨어, IPC 등록, 메모리 관리 — 각 단계를 손으로 메모.BLOCKED_HOSTS를 차용해 본인이 자주 가는 사이트의 광고만 차단하는 미니 Electron 브라우저 작성. 차단 통계 모달까지 구현.src/ipc/notes.js를 분리해 register() 패턴으로 IPC 핸들러를 등록. 렌더러는 window.notesAPI.save()만 호출하게.