the-overlay는 Electron 앱에서 Windows/macOS 대상 프로세스/창을 추적하고, 해당 창 위에 투명 오버레이 창을 띄우는 런타임 라이브러리입니다.
주 사용자는 Electron main process에서 processTracking()만 호출하면 됩니다. 런타임은 컨트롤 창과 오버레이 창을 만들고, 플랫폼 추적 backend로 대상 창의 포커스/이동/블러 이벤트를 추적한 뒤 오버레이 위치와 표시 상태를 동기화합니다.
이 저장소는 두 역할을 함께 가집니다.
- 재사용 가능한 라이브러리 패키지:
release/npm/library-package - 라이브러리를 검증하는 Electron + React 예제 앱
현재 공개 API의 중심은 Electron main process에서 호출하는 processTracking()입니다. Renderer는 라이브러리가 주입하는 preload bridge인 window.overlayAPI만 사용하며, renderer에서 Node/Electron main API를 직접 호출하지 않습니다.
| 구분 | 요구 사항 |
|---|---|
| 실행 OS | Windows, macOS. 생성되는 라이브러리 패키지는 os: ["win32", "darwin"]를 선언합니다. |
| 저장소 개발 Node | package.json 기준 Node >=20 |
| 배포 라이브러리 Node | 생성된 패키지 기준 Node >=18 |
| Electron | 배포 라이브러리 peer dependency 기준 Electron >=28 |
| 패키지 매니저 | Yarn 1.22.22 이상, Yarn classic만 사용 |
| 네이티브 빌드 | Windows는 ch-overlay-node, dxgi-color-node N-API 바이너리 필요. macOS는 내장 Accessibility adapter와 런타임 컴파일되는 ScreenCaptureKit helper를 사용합니다. |
지원 플랫폼: Windows는
ch-overlay-nodeWinEvent hook으로 창 추적과 focus 복원을 처리하고,dxgi-color-node로 화면 색상 캡처를 제공합니다. macOS는 System Events/Accessibility 기반 window tracker로 창 추적과 focus 복원을 지원하고, ScreenCaptureKit helper로 화면 색상 캡처를 제공합니다. Linux는 지원하지 않습니다.
저장소 루트 패키지는 private: true입니다. npm에 올리거나 외부 앱에 설치하는 대상은 루트가 아니라 yarn build:library:package로 생성되는 release/npm/library-package입니다.
- 빠른 시작
- 설치와 로컬 검증
- 런타임 구조
- Public API
OverlayRuntimeOptions- Renderer API:
window.overlayAPI - 오버레이 UI 상호작용 패턴
- 주요 기능
- 예제 앱 명령어
- 패키지 exports
- 문제 해결
소비 앱에서는 Electron main process 진입점에서 processTracking()을 한 번 호출합니다.
기본 구성 요소:
renderer.devServerUrl: 개발 중 Vite/React dev server 주소입니다. 생략하면http://localhost:3000을 사용합니다.renderer.distIndexPath: 패키징된 앱에서 로드할 rendererindex.html경로입니다.renderer.controlRoute: 설정/대시보드 창 route입니다.renderer.overlayRoute: 대상 창 위에 올라갈 overlay 창 route입니다.targets: 추적할 창 제목 또는 앱 이름 목록입니다.
the-overlay는 route를 hash route로 로드합니다. 예를 들어 controlRoute: 'main', overlayRoute: 'overlay'면 개발 중에는 http://localhost:3000/#/main, http://localhost:3000/#/overlay가 로드됩니다.
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { processTracking } from 'the-overlay';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const envPath = path.join(__dirname, '..', '.env');
processTracking({
envPath,
renderer: {
devServerUrl: 'http://localhost:3000',
distIndexPath: path.join(__dirname, '..', 'dist', 'index.html'),
controlRoute: 'main',
overlayRoute: 'overlay',
},
targets: [
{ name: '메모장', mode: 'contains' },
{ name: '마비노기 모바일', mode: 'exact' },
{ name: 'MapleStory', mode: 'exact' },
{ name: 'Brood War', mode: 'exact' },
],
}).catch((error) => {
console.error('[CH-OVERLAY] Failed to start overlay runtime:', error);
process.exitCode = 1;
});const path = require('node:path');
const { processTracking } = require('the-overlay');
const envPath = path.join(__dirname, '..', '.env');
processTracking({
envPath,
renderer: {
devServerUrl: 'http://localhost:3000',
distIndexPath: path.join(__dirname, '..', 'dist', 'index.html'),
controlRoute: 'main',
overlayRoute: 'overlay',
},
targets: [
{ name: '메모장', mode: 'contains' },
{ name: '마비노기 모바일', mode: 'exact' },
],
}).catch((error) => {
console.error('[CH-OVERLAY] Failed to start overlay runtime:', error);
process.exitCode = 1;
});renderer 기본값을 그대로 쓸 수 있고, 대상 창 제목만 지정하면 다음처럼 줄일 수 있습니다.
import { processTracking } from 'the-overlay';
await processTracking({
targets: [{ name: 'MapleStory', mode: 'exact' }],
});이 경우 기본 renderer 설정은 다음과 같습니다.
| 항목 | 기본값 |
|---|---|
devServerUrl |
http://localhost:3000 |
distIndexPath |
path.join(process.cwd(), 'dist', 'index.html') |
controlRoute |
main |
overlayRoute |
overlay |
개발 중에는 dev server를 사용하고, 배포 빌드에서는 dist/index.html을 쓰도록 명시하는 구성이 가장 일반적입니다.
import { app } from 'electron';
const rendererDevServerUrl = app.isPackaged ? '' : 'http://localhost:3000';
await processTracking({
renderer: {
devServerUrl: rendererDevServerUrl,
distIndexPath: path.join(__dirname, '..', 'dist', 'index.html'),
controlRoute: 'main',
overlayRoute: 'overlay',
},
targets: [{ name: 'My Game', mode: 'contains' }],
});renderer.devServerUrl을 생략하거나 undefined로 두면 기본값인 http://localhost:3000이 사용됩니다. 패키징되지 않은 Electron 프로세스에서도 distIndexPath를 강제로 로드해야 한다면 devServerUrl: ''처럼 falsy string을 넘기세요. Electron이 packaged 상태이거나 devServerUrl이 falsy이면 런타임은 loadFile()로 distIndexPath를 로드합니다. React Router를 쓰는 renderer는 BrowserRouter보다 HashRouter를 권장합니다.
processTracking()은 같은 renderer 번들을 두 route로 엽니다. control route는 일반 설정 창이고, overlay route는 투명 overlay 창입니다.
import { HashRouter, Route, Routes } from 'react-router-dom';
function ControlPage() {
return (
<button
onClick={() => {
void window.overlayAPI.visibility.toggleUserIntent();
}}
>
Toggle overlay
</button>
);
}
function OverlayPage() {
return (
<div style={{ position: 'relative', width: '100vw', height: '100vh' }}>
<div
aria-hidden="true"
data-stratum-id="overlay-panel"
style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
/>
<button data-overlay-control="transient">Run action</button>
</div>
);
}
export function App() {
return (
<HashRouter>
<Routes>
<Route path="/main" element={<ControlPage />} />
<Route path="/overlay" element={<OverlayPage />} />
</Routes>
</HashRouter>
);
}data-stratum-id가 붙은 요소는 UI로 상호작용하지 않는 hit zone입니다. 버튼, input, select처럼 사용자가 직접 조작하는 요소에는 data-stratum-id를 붙이지 말고 data-overlay-control 또는 data-overlay-input 같은 별도 marker를 사용합니다.
공개 npm 패키지를 사용하는 경우:
yarn add the-overlay소비 앱에는 Electron이 별도로 있어야 합니다. 생성 패키지는 Windows/macOS용(os: ["win32", "darwin"])이며, electron >=28을 peer dependency로 요구합니다.
이 저장소에서 만든 tarball을 바로 시험하려면 먼저 라이브러리 패키지를 생성합니다.
yarn pack:library그 다음 소비 앱에서 tarball을 설치합니다.
yarn add file:C:\Users\cizz3\Downloads\examples2\the-overlay.tgzyarn install --frozen-lockfile
yarn build:native
yarn startyarn build:library:package
yarn validate:library-package
yarn pack:library생성 결과:
release/npm/library-package/index.cjsrelease/npm/library-package/index.mjsrelease/npm/library-package/index.d.tsrelease/npm/library-package/main/*release/npm/library-package/native/*the-overlay.tgz
release/npm/library-package는 실제 publish 대상입니다. README나 예제 앱 파일은 기본 publish surface가 아니며, 생성 패키지는 runtime, preload, native bridge, integrity/license 파일 중심으로 구성됩니다.
문서/API/런타임을 바꾼 뒤에는 최소한 다음 순서로 확인합니다.
yarn lint
yarn testpublish surface까지 확인해야 하는 변경이면 생성 패키지를 만든 뒤 source와 generated package 양쪽을 함께 검증합니다.
yarn test:generated가장 넓은 로컬 회귀 검증은 다음 명령입니다.
yarn test:allRust native 추적, 성능 벤치마크, 보안 진단을 별도로 확인할 때는 docs/RUST_QUALITY.md의 Rust 품질 게이트를 사용합니다.
yarn rust:clippy
yarn rust:bench
yarn rust:securityprocessTracking()은 Electron app.whenReady() 이후 다음 작업을 수행합니다.
.env파일을 로드합니다.- 패키지 무결성 검사를 수행합니다.
- 라이선스 옵션이 있으면 토큰 또는 서버 응답을 검증합니다.
- 플랫폼 추적 backend를 초기화해 대상 창 추적을 준비합니다.
- 현재 OS의 화면 색상 backend를 초기화해 색상 샘플링을 준비합니다.
- IPC handler를 등록합니다.
- 컨트롤 창과 오버레이 창을 생성합니다.
targets가 있으면 네이티브 추적을 시작합니다.
런타임은 한 Electron 프로세스에서 하나만 활성화할 수 있습니다. 두 번째 런타임을 동시에 시작하면 에러가 발생합니다.
패키지는 ESM과 CJS를 모두 지원합니다.
import {
DEFAULT_INTERACTION_GRACE_MS,
createOverlayRuntime,
processTracking,
resolvePreloadPath,
} from 'the-overlay';const {
DEFAULT_INTERACTION_GRACE_MS,
createOverlayRuntime,
processTracking,
resolvePreloadPath,
} = require('the-overlay');가장 단순한 진입점입니다.
function processTracking(options?: OverlayRuntimeOptions): Promise<OverlayRuntimeStartResult>;createOverlayRuntime(options).start()를 한 번에 실행합니다. 일반적인 앱에서는 이 함수만 사용하면 됩니다.
반환 Promise는 런타임 시작 후 다음 값을 resolve합니다.
interface OverlayRuntimeStartResult {
controlWindow: unknown;
overlayWindow: unknown;
trackingReady: boolean;
dxgiColorReady: boolean;
preloadPath: string;
getOverlayState(): OverlayVisibilityState;
setOverlayVisibilityIntent(visible: boolean): OverlayVisibilityState;
toggleOverlayVisibility(): OverlayVisibilityState;
forceFocusOverlay(): boolean;
}dxgiColorReady는 기존 API 호환을 위해 유지되는 이름입니다. Windows에서는 DXGI 준비 여부, macOS에서는 ScreenCaptureKit helper 준비 여부를 의미합니다.
사용 예:
const rendererDevServerUrl =
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : '';
const runtime = await processTracking({
renderer: {
devServerUrl: rendererDevServerUrl,
distIndexPath: path.join(__dirname, '../dist/index.html'),
controlRoute: 'main',
overlayRoute: 'overlay',
},
targets: [{ name: 'My Game', mode: 'exact' }],
});
console.log(runtime.trackingReady, runtime.dxgiColorReady);수동으로 시작 시점과 runtime handle을 관리하고 싶을 때 사용합니다.
function createOverlayRuntime(options?: OverlayRuntimeOptions): OverlayRuntimeHandle;반환 handle:
interface OverlayRuntimeHandle {
start(): Promise<OverlayRuntimeStartResult>;
getWindows(): {
controlWindow: unknown;
overlayWindow: unknown;
};
getPreloadPath(): string;
getOverlayState(): OverlayVisibilityState;
setOverlayVisibilityIntent(visible: boolean): OverlayVisibilityState;
toggleOverlayVisibility(): OverlayVisibilityState;
forceFocusOverlay(): boolean;
}사용 예:
const overlayRuntime = createOverlayRuntime({
targets: [{ name: 'MapleStory', mode: 'exact' }],
visibility: {
mode: 'controlled',
initialUserIntent: true,
},
});
const result = await overlayRuntime.start();
if (!result.trackingReady) {
console.warn('Overlay window tracking is not available.');
}라이브러리가 제공하는 preload 파일 경로를 반환합니다.
function resolvePreloadPath(): string;일반적으로는 런타임이 직접 사용하므로 호출할 필요가 없습니다. 별도 BrowserWindow를 직접 만들면서 동일한 window.overlayAPI 브리지를 쓰고 싶을 때 사용할 수 있습니다.
const preload = resolvePreloadPath();
const debugWindow = new BrowserWindow({
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false,
},
});기본 상호작용 grace time입니다.
const DEFAULT_INTERACTION_GRACE_MS: number; // 400마우스 클릭, 드래그, 입력 모드 전환 직후 네이티브 blur가 들어와도 오버레이가 즉시 숨겨지지 않도록 보호하는 짧은 시간입니다. interactionGraceMs 옵션으로 바꿀 수 있습니다.
interface OverlayRuntimeOptions {
envPath?: string;
targets?: OverlayTarget[];
renderer?: OverlayRendererOptions;
visibility?: OverlayVisibilityOptions;
interactionGraceMs?: number;
controlWindowTitle?: string;
controlWindowOptions?: Record<string, unknown>;
overlayWindowOptions?: Record<string, unknown>;
quitOnAllWindowsClosed?: boolean;
logNativeStatus?: boolean;
logLicenseStatus?: boolean;
verifyIntegrity?: boolean;
requireSignedIntegrity?: boolean;
integrityPublicKey?: string;
integrityManifestPath?: string;
license?: LicenseOptions;
}
interface LicenseOptions {
enabled?: boolean;
required?: boolean;
endpoint?: string;
method?: string;
apiKey?: string;
headers?: Record<string, string>;
token?: string | Record<string, unknown>;
publicKey?: string;
appId?: string;
appVersion?: string;
machineId?: string;
metadata?: Record<string, unknown>;
timeoutMs?: number;
}.env 파일 경로입니다. 값이 있고 파일이 존재하면 런타임 시작 시 dotenv.config({ path: envPath })로 로드합니다.
processTracking({
envPath: path.join(__dirname, '../.env'),
});추적할 Windows 창 목록입니다.
type OverlayTargetMode = 'exact' | 'contains';
interface OverlayTarget {
name: string;
mode?: OverlayTargetMode;
}사용법:
targets: [
{ name: '메모장', mode: 'contains' },
{ name: '마비노기 모바일', mode: 'exact' },
]동작:
exact: 창 제목이name과 정확히 같아야 합니다.contains: 창 제목에name이 포함되면 매칭됩니다.mode를 생략하거나 알 수 없는 값이면 내부 정규화 과정에서contains로 처리됩니다.targets가 비어 있으면 창은 생성되지만 네이티브 추적은 시작하지 않습니다.
컨트롤 창과 오버레이 창이 로드할 renderer 경로를 지정합니다.
interface OverlayRendererOptions {
devServerUrl?: string;
distIndexPath?: string;
controlRoute?: string;
overlayRoute?: string;
}기본값:
| 옵션 | 기본값 |
|---|---|
devServerUrl |
http://localhost:3000 |
distIndexPath |
path.join(process.cwd(), 'dist', 'index.html') |
controlRoute |
main |
overlayRoute |
overlay |
renderer.devServerUrl을 생략하거나 undefined로 두면 기본값인 http://localhost:3000이 적용됩니다.
개발 중에는 devServerUrl의 hash route를 로드합니다.
http://localhost:3000/#/main
http://localhost:3000/#/overlay
패키징된 앱이거나 devServerUrl이 falsy이면 distIndexPath 파일을 hash route와 함께 로드합니다. 패키징되지 않은 Electron 프로세스에서 파일 로드를 테스트하려면 devServerUrl: ''을 넘기세요.
dist/index.html#/main
dist/index.html#/overlay
React Router를 쓴다면 HashRouter를 사용해야 Electron loadFile()과 잘 맞습니다.
import { HashRouter, Route, Routes } from 'react-router-dom';
export function App() {
return (
<HashRouter>
<Routes>
<Route path="/main" element={<ControlPage />} />
<Route path="/overlay" element={<OverlayPage />} />
</Routes>
</HashRouter>
);
}오버레이 표시 정책을 제어합니다.
type OverlayVisibilityMode = 'auto' | 'controlled';
type OverlayInactiveWindowBehavior = 'hide-window' | 'keep-window-visible';
interface OverlayVisibilityOptions {
mode?: OverlayVisibilityMode;
initialUserIntent?: boolean;
inactiveWindowBehavior?: OverlayInactiveWindowBehavior;
canShow?: (
state: OverlayVisibilityState,
reason?: string,
event?: unknown,
) => boolean;
canHide?: (
state: OverlayVisibilityState,
reason?: string,
event?: unknown,
) => boolean;
}auto 모드:
- 기본 모드입니다.
- 대상 창의
focus또는move이벤트가 들어오면 오버레이가 표시됩니다. - renderer나 main에서 숨김 intent를 설정해도 이후 추적 이벤트가 다시 오면 표시될 수 있습니다.
- 단순한 "대상 창 위에 항상 따라다니는 오버레이"에 적합합니다.
await processTracking({
visibility: { mode: 'auto' },
targets: [{ name: 'My Game', mode: 'contains' }],
});controlled 모드:
userIntent === true이고 대상 창 또는 런타임 창이 활성 상태일 때만 표시됩니다.visibility.setUserIntent(),toggleUserIntent(),notifyGameState()같은 renderer API와 함께 쓰기 좋습니다.- 앱 내부 토글, 단축키, 설정값으로 오버레이 표시를 엄격하게 제어할 때 사용합니다.
initialUserIntent가false이면 앱이 시작되어도 사용자가 켜기 전까지 overlay content가 표시되지 않습니다.
await processTracking({
visibility: {
mode: 'controlled',
initialUserIntent: false,
},
targets: [{ name: 'My Game', mode: 'contains' }],
});renderer에서 사용자가 overlay를 켜는 버튼은 다음처럼 구현합니다.
async function toggleOverlay() {
const state = await window.overlayAPI.visibility.toggleUserIntent();
console.log('overlay user intent:', state.userIntent);
}inactiveWindowBehavior:
- 기본값은
'hide-window'입니다. 기존처럼 logical visibility가 false가 되면 실제overlayWindow.hide()를 호출합니다. 'keep-window-visible'은controlled모드에서userIntent === true이고 대상/런타임 창이 비활성인 경우에도 실제overlayWindow를 유지합니다.- 같은 overlay window 안에서 Prism은 숨기고 Beam webview는 유지해야 하는 앱은 이 옵션을 켜고 renderer에서
contentVisible,targetActive,runtimeWindowActive를 기준으로 Prism DOM만 soft-hide하면 됩니다.
사용 예:
processTracking({
visibility: {
mode: 'controlled',
initialUserIntent: false,
inactiveWindowBehavior: 'keep-window-visible',
canShow(state, reason) {
if (reason === 'maintenance-mode') {
return false;
}
return state.userIntent && (state.targetActive || state.runtimeWindowActive);
},
},
});renderer에서 keep-window-visible을 사용할 때는 실제 window visibility와 content visibility를 분리해서 봅니다.
useEffect(() => {
const unsubscribe = window.overlayAPI.visibility.onChange((state) => {
document.documentElement.dataset.overlayContentVisible = String(state.contentVisible);
document.documentElement.dataset.overlayWindowVisible = String(state.windowVisible);
});
return () => unsubscribe();
}, []);canShow와 canHide:
- 두 callback은 main process에서 실행됩니다.
- callback이
false를 반환하면 해당 show/hide 동작을 막습니다. - callback 내부에서 에러가 나면 런타임은 warning을 출력하고 기본 판단을 사용합니다.
reason은tracking-focus,tracking-blur,renderer-set-user-intent,set-input-mode=true처럼 런타임 전환 이유를 담습니다.
예를 들어 overlay route가 아직 준비되지 않았거나 앱 자체 pause 상태이면 표시를 막을 수 있습니다.
let overlayPaused = false;
await processTracking({
visibility: {
mode: 'controlled',
initialUserIntent: true,
canShow(state, reason) {
if (overlayPaused) return false;
if (reason === 'tracking-blur') return false;
return state.userIntent && (state.targetActive || state.runtimeWindowActive);
},
},
});컨트롤 창 제목입니다. 기본값은 마비노기 모바일 오버레이입니다.
processTracking({
controlWindowTitle: 'My Game Overlay',
});컨트롤 창에 추가로 전달할 Electron BrowserWindow 옵션입니다.
processTracking({
controlWindowOptions: {
width: 900,
height: 640,
title: 'Overlay Control',
},
});런타임은 기본적으로 다음 보안 옵션을 설정합니다.
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false,
}controlWindowOptions.webPreferences로 덮어쓸 수는 있지만, renderer 보안을 위해 contextIsolation: true, nodeIntegration: false를 유지하는 편이 좋습니다.
오버레이 창에 추가로 전달할 Electron BrowserWindow 옵션입니다.
기본 오버레이 창은 다음 성격을 가집니다.
- 투명
- 프레임 없음
- 항상 위
- 작업 표시줄에서 숨김
- 전체 워크스페이스 표시
- 최초에는 click-through
- 최초 크기는
10x10, 대상 창 이벤트를 받으면 대상 창 bounds로 이동/리사이즈
사용 예:
processTracking({
overlayWindowOptions: {
backgroundColor: '#00000000',
hasShadow: false,
},
});오버레이가 입력/드래그/클릭 상태로 전환되는 짧은 순간에 blur 이벤트로 바로 숨겨지는 일을 막는 시간입니다.
processTracking({
interactionGraceMs: 600,
});기본값은 DEFAULT_INTERACTION_GRACE_MS, 현재 400ms입니다.
모든 창이 닫힐 때 Electron 앱을 종료할지 결정합니다.
processTracking({
quitOnAllWindowsClosed: false,
});기본값은 true입니다.
네이티브 모듈과 라이선스 상태 로그를 출력할지 결정합니다.
processTracking({
logNativeStatus: false,
});logLicenseStatus는 호환용 옵션입니다. 현재 런타임은 logNativeStatus !== false && logLicenseStatus !== false일 때 상태 로그를 출력합니다.
배포 패키지는 INTEGRITY.json과 SHA256SUMS.txt를 포함합니다. 런타임 시작 시 기본적으로 패키지 파일 hash를 검증합니다.
processTracking({
verifyIntegrity: true,
requireSignedIntegrity: true,
integrityPublicKey: process.env.STRATUM_INTEGRITY_PUBLIC_KEY_PEM,
});옵션:
| 옵션 | 설명 |
|---|---|
verifyIntegrity |
기본값 true. false면 무결성 검사를 건너뜁니다. |
requireSignedIntegrity |
signature가 반드시 검증되어야 하는지 지정합니다. |
integrityPublicKey |
서명 검증용 PEM public key입니다. |
integrityManifestPath |
기본 INTEGRITY.json 대신 사용할 manifest 경로입니다. |
환경 변수:
| 환경 변수 | 설명 |
|---|---|
STRATUM_INTEGRITY_PUBLIC_KEY_PEM |
런타임 서명 검증용 public key |
STRATUM_INTEGRITY_REQUIRE_SIGNED |
truthy면 서명 검증 필수 |
STRATUM_INTEGRITY_PRIVATE_KEY_PEM |
패키지 빌드 시 manifest 서명용 private key |
STRATUM_INTEGRITY_PRIVATE_KEY_PATH |
패키지 빌드 시 private key 파일 경로 |
Electron packager가 라이브러리 파일을 이동하거나 일부 파일을 제거해 manifest와 실제 파일 구성이 달라지는 경우에만 verifyIntegrity: false를 검토하세요.
라이선스 검증은 선택 기능입니다.
processTracking({
license: {
required: true,
endpoint: 'https://license.example.com/issue',
publicKey: process.env.STRATUM_LICENSE_PUBLIC_KEY_PEM,
appId: 'my-overlay-app',
appVersion: '1.0.0',
metadata: {
channel: 'stable',
},
},
});지원 방식:
license.token: 이미 발급된{ payload, signature }envelope를 로컬에서 검증합니다.license.endpoint: 서버에 machine/app 정보를 보내고, 서버가 반환한 signed envelope를 검증합니다.license.required: true: token 또는 endpoint 검증이 실패하면 시작을 실패 처리합니다.payload.licenseKey가 있으면 네이티브 모듈 초기화에 전달됩니다.
token 또는 endpoint 검증에는 public key가 필요합니다. public key는 license.publicKey, STRATUM_LICENSE_PUBLIC_KEY_PEM, 또는 패키지에 포함된 LICENSE.pub.pem에서 읽습니다.
license.enabled === false이면 라이선스 검증을 명시적으로 끕니다.
런타임 preload는 renderer에 window.overlayAPI를 노출합니다. renderer는 Node API에 직접 접근하지 않고 이 브리지를 통해 main process와 통신합니다.
사용 위치:
#/main같은 control route: 설정 UI, 색상 샘플링 대시보드, overlay on/off 토글을 구현합니다.#/overlay같은 overlay route: 대상 창 위에 표시할 투명 UI, drag panel, screen color scan, hot zone 계산을 구현합니다.
preload bridge가 있는 창에서만 window.overlayAPI가 존재합니다. 런타임이 만든 controlWindow와 overlayWindow에는 자동으로 주입되고, 직접 만든 BrowserWindow에서 쓰려면 main process에서 resolvePreloadPath()를 지정해야 합니다.
패키지의 index.d.ts는 main process export(processTracking, createOverlayRuntime 등)를 타입으로 제공합니다. window.overlayAPI는 preload가 런타임에 주입하는 renderer bridge이므로 TypeScript renderer 앱에서는 아래 구조를 참고해 앱 쪽 ambient declaration을 따로 두세요.
interface OverlayApi {
onEvent(cb: (payload: OverlayEvent | RuntimeEvent) => void): () => void;
setIgnore(ignore: boolean): Promise<boolean>;
setInputMode(enabled: boolean): Promise<boolean>;
focusLastTarget(): Promise<boolean>;
notifyGameState(active: boolean): void;
getOverlayState(): Promise<OverlayVisibilityCompatState>;
toggleOverlayVisibility(): Promise<boolean>;
forceFocusOverlay(): Promise<boolean>;
visibility: {
getState(): Promise<OverlayVisibilityState>;
setUserIntent(visible: boolean): Promise<OverlayVisibilityState>;
toggleUserIntent(): Promise<OverlayVisibilityState>;
forceFocusOverlay(): Promise<boolean>;
onChange(cb: (state: OverlayVisibilityState, event: RuntimeEvent) => void): () => void;
};
window: {
setContentSize(size: { width: number; height: number }): Promise<Bounds>;
};
screenColor: {
isReady(): Promise<boolean>;
listDisplays(): Promise<DisplayInfo[]>;
sampleClientPoint(point: Point): Promise<{ screenPoint: Point; color: RgbaColor }>;
samplePoint(point: Point): Promise<RgbaColor>;
sampleClientAverageColor(region: Region): Promise<{ screenRegion: Region; color: RgbaColor }>;
sampleAverageColor(region: Region): Promise<RgbaColor>;
captureClientRegionRaw(region: Region): Promise<{
clientRegion: Region;
screenRegion: Region;
capture: RawRegionCapture;
}>;
captureRegionRaw(region: Region): Promise<RawRegionCapture>;
};
debugLog(payload: Record<string, unknown>): void;
}
type OverlayVisibilityCompatState = OverlayVisibilityState & {
visible: boolean; // legacy: userIntent
effectiveVisible: boolean; // actual BrowserWindow visibility
};
interface Bounds {
x: number;
y: number;
width: number;
height: number;
}main process가 전달하는 overlay/runtime 이벤트를 구독합니다. 반환값은 unsubscribe 함수입니다.
useEffect(() => {
const unsubscribe = window.overlayAPI?.onEvent((event) => {
if (event.eventType === 'focus') {
console.log('target focused:', event.targetName);
}
});
return () => {
unsubscribe?.();
};
}, []);네이티브 추적 이벤트 shape:
interface OverlayEvent {
eventType: 'focus' | 'move' | 'blur' | 'cursor' | 'cursor-leave' | string;
targetName: string;
x: number;
y: number;
width: number;
height: number;
}좌표 단위:
| 이벤트 | x, y |
width, height |
|---|---|---|
focus |
절대 physical screen pixel | physical pixel 크기 |
move |
절대 physical screen pixel | physical pixel 크기 |
cursor |
활성 대상 창 좌상단 기준 physical pixel | 보통 0 |
cursor-leave |
0 |
0 |
blur |
0 |
0 |
런타임 상태 이벤트:
{
type: 'overlay:visibility-state';
reason: string;
state: OverlayVisibilityState;
}호환 이벤트:
{ type: 'iride:overlay-visibility'; visible: boolean }
{ type: 'iride:game-active'; active: boolean }오버레이 창의 click-through 상태를 바꿉니다.
await window.overlayAPI.setIgnore(true); // 마우스 이벤트를 게임/대상 창으로 통과
await window.overlayAPI.setIgnore(false); // 오버레이 UI가 마우스 이벤트를 받음사용 패턴:
- 커서가 오버레이 UI hot zone 위에 있으면
false - 커서가 오버레이 UI 밖으로 나가면
true - 드래그/버튼 클릭 중에는 잠시
false - 작업이 끝나면 다시
true로 돌리고focusLastTarget()호출
오버레이 창의 focus 가능 여부를 바꿉니다.
await window.overlayAPI.setInputMode(true); // input/select/textarea 편집 전
await window.overlayAPI.setInputMode(false); // 편집 종료 후true일 때:
- 오버레이 창이 focusable 상태가 됩니다.
- 가능하면 오버레이가 focus를 얻습니다.
- grace time이 설정되어 바로 숨겨지는 것을 막습니다.
false일 때:
- 오버레이 창을 다시 focus 불가능한 상태로 되돌립니다.
- 일반적으로
setIgnore(true)와focusLastTarget()를 함께 호출합니다.
네이티브 모듈이 기억하는 마지막 대상 창으로 focus를 되돌립니다.
await window.overlayAPI.focusLastTarget();드래그, 버튼 클릭, 입력 종료 후 게임이나 대상 앱으로 focus를 돌려줄 때 사용합니다. 네이티브 모듈이 준비되지 않았거나 대상이 없으면 false를 반환할 수 있습니다. 마지막 대상 창이 이미 종료되었으면 내부 상태를 정리하고 false를 반환합니다. focus 복원은 Windows 포그라운드 락을 우회하기 위해 입력 큐 attach(AttachThreadInput)를 사용합니다.
renderer가 대상 활성 상태를 main process에 알려줍니다.
window.overlayAPI.notifyGameState(true);
window.overlayAPI.notifyGameState(false);주로 visibility.mode: 'controlled'에서 앱 자체 판단으로 오버레이 표시 가능 상태를 갱신할 때 사용합니다.
호환용 visibility state를 반환합니다.
const state = await window.overlayAPI.getOverlayState();이 API는 legacy 호환을 위해 visible을 실제 창 표시 여부가 아니라 userIntent 기준으로 반환합니다. 실제 BrowserWindow 표시 여부는 effectiveVisible을 보세요.
새 코드에서는 window.overlayAPI.visibility.getState()를 권장합니다.
top-level 호환 API입니다.
const userIntent = await window.overlayAPI.toggleOverlayVisibility();내부적으로 overlay:visibility:toggle-user-intent를 호출하고, 결과 state의 userIntent boolean만 반환합니다. 전체 state가 필요하면 visibility.toggleUserIntent()를 사용하세요.
오버레이를 표시하고 강제로 focus를 가져옵니다.
const focused = await window.overlayAPI.forceFocusOverlay();입력 UI를 즉시 열어야 하는 단축키/명령 팔레트 같은 흐름에서 사용할 수 있습니다. 오버레이 창이 없거나 표시 불가능하면 false를 반환합니다.
현재 visibility state를 반환합니다.
const state = await window.overlayAPI.visibility.getState();state shape:
interface OverlayVisibilityState {
mode: 'auto' | 'controlled';
inactiveWindowBehavior: 'hide-window' | 'keep-window-visible';
visible: boolean;
windowVisible: boolean;
contentVisible: boolean;
userIntent: boolean;
targetActive: boolean;
gameActive: boolean;
runtimeWindowActive: boolean;
effectiveVisible: boolean;
exists: boolean;
overlayReady: boolean;
inputMode: boolean;
overlayMouseIgnored: boolean;
}필드 의미:
| 필드 | 의미 |
|---|---|
mode |
현재 visibility 정책 |
inactiveWindowBehavior |
비활성 상태에서 실제 window를 숨길지 유지할지에 대한 정책 |
visible |
실제 오버레이 BrowserWindow 표시 여부 |
windowVisible |
visible과 같은 실제 BrowserWindow 표시 여부 |
contentVisible |
visibility 정책상 Prism/overlay content가 표시되어야 하는지 여부 |
userIntent |
사용자가 오버레이를 켜고 싶어 하는 상태 |
targetActive, gameActive |
추적 대상 활성 여부. gameActive는 호환 alias입니다. |
runtimeWindowActive |
컨트롤 창 또는 오버레이 창 focus 여부 |
effectiveVisible |
실제 표시 여부. 현재 visible과 같습니다. |
exists |
오버레이 BrowserWindow가 살아 있는지 여부 |
overlayReady |
overlay renderer load 완료 여부 |
inputMode |
오버레이가 입력 focus를 받을 수 있는 상태인지 여부 |
overlayMouseIgnored |
click-through 상태인지 여부 |
사용자 intent를 명시적으로 설정합니다.
await window.overlayAPI.visibility.setUserIntent(true);
await window.overlayAPI.visibility.setUserIntent(false);controlled 모드에서는 이 값이 오버레이 표시 조건의 핵심입니다.
사용자 intent를 토글하고 전체 visibility state를 반환합니다.
const state = await window.overlayAPI.visibility.toggleUserIntent();overlay:visibility-state 이벤트만 골라 구독합니다.
useEffect(() => {
const unsubscribe = window.overlayAPI?.visibility?.onChange((state, event) => {
console.log(event.reason, state.effectiveVisible);
});
return () => {
unsubscribe?.();
};
}, []);현재 renderer를 호스팅하는 BrowserWindow의 content size를 바꿉니다.
await window.overlayAPI.window.setContentSize({
width: 1220,
height: 980,
});컨트롤 창에서 검증 UI나 대시보드 크기를 고정하고 싶을 때 사용합니다. 최소 크기는 내부에서 100x100으로 보정됩니다.
현재 OS의 화면 색상 backend 준비 여부를 반환합니다. Windows는 dxgi-color-node, macOS는 ScreenCaptureKit helper를 사용합니다.
const ready = await window.overlayAPI.screenColor.isReady();false인 경우:
- 지원 OS가 아닐 수 있습니다.
- Windows에서는
dxgi-color-node바이너리가 없거나 로드에 실패했을 수 있습니다. - macOS에서는 Screen Recording 권한이 없거나 Xcode Command Line Tools의
swiftc를 사용할 수 없을 수 있습니다. - Windows native 빌드가 필요한 경우
yarn build:dxgi-color또는yarn build:native를 실행합니다.
현재 OS의 화면 색상 backend가 인식한 display 목록을 반환합니다.
const displays = await window.overlayAPI.screenColor.listDisplays();반환 shape:
interface DisplayInfo {
id: string;
name: string;
left: number;
top: number;
width: number;
height: number;
rotation: number;
isPrimary: boolean;
}좌표는 physical screen pixel 기준입니다.
renderer client 좌표를 physical screen 좌표로 변환한 뒤 해당 pixel 색상을 샘플링합니다.
const result = await window.overlayAPI.screenColor.sampleClientPoint({
x: event.clientX,
y: event.clientY,
});
console.log(result.screenPoint, result.color.hex);반환:
{
screenPoint: { x: number; y: number };
color: { r: number; g: number; b: number; a: number; hex: string };
}Electron/Chromium client 좌표를 넘기면 런타임이 screen.dipToScreenPoint()로 변환합니다. renderer에서 보이는 UI 위치 기준으로 색상을 찍을 때는 이 API를 권장합니다.
physical screen 좌표를 직접 샘플링합니다.
const color = await window.overlayAPI.screenColor.samplePoint({
x: 1920,
y: 540,
});이미 physical screen pixel 좌표를 알고 있을 때만 사용하세요.
renderer client 영역을 physical screen 영역으로 변환한 뒤 평균 색상을 계산합니다.
const result = await window.overlayAPI.screenColor.sampleClientAverageColor({
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
});
console.log(result.screenRegion, result.color.hex);physical screen 영역의 평균 색상을 계산합니다.
const color = await window.overlayAPI.screenColor.sampleAverageColor({
x: 100,
y: 100,
width: 24,
height: 24,
});renderer client 영역을 physical screen 영역으로 변환한 뒤 raw RGBA capture를 반환합니다.
const result = await window.overlayAPI.screenColor.captureClientRegionRaw({
x: 0,
y: 0,
width: window.innerWidth,
height: window.innerHeight,
});
const { width, height, stride, pixelFormat, data } = result.capture;반환:
interface RawRegionCapture {
width: number;
height: number;
stride: number;
pixelFormat: 'rgba' | string;
data: Buffer | Uint8Array | { type: 'Buffer'; data: number[] };
}renderer에서는 IPC 직렬화 결과에 따라 Buffer가 plain object로 올 수 있으므로 Uint8Array로 정규화해서 사용하는 편이 안전합니다.
function asBytes(value: unknown): Uint8Array | null {
if (value instanceof Uint8Array) return value;
if (value instanceof ArrayBuffer) return new Uint8Array(value);
if (value && typeof value === 'object' && (value as any).type === 'Buffer') {
return Uint8Array.from((value as any).data);
}
return null;
}physical screen 영역을 직접 capture합니다.
const capture = await window.overlayAPI.screenColor.captureRegionRaw({
x: 0,
y: 0,
width: 320,
height: 180,
});이미 physical screen 좌표를 알고 있는 저수준 도구에서 사용합니다.
renderer에서 main process 콘솔로 debug payload를 보냅니다.
window.overlayAPI.debugLog({
channel: 'overlay-ui',
message: 'hot zone changed',
hot: true,
});main process는 다음 형태로 출력합니다.
[OVERLAY:overlay-ui] {"message":"hot zone changed","hot":true}
오버레이 창은 기본적으로 click-through입니다. 사용자가 오버레이 UI를 조작해야 하는 순간에만 click-through를 끄고, 조작이 끝나면 다시 대상 창으로 focus를 돌려야 합니다.
이 프로젝트의 규칙:
data-stratum-id는 네이티브/renderer hit-test용 마커입니다.data-stratum-id가 붙은 요소는 UI로 상호작용하지 않습니다.- 실제 버튼, input, select 같은 UI 제어 요소에
data-stratum-id를 직접 붙이지 않습니다. data-stratum-id가 붙은 요소는 별도의 비상호작용 hit zone으로 둡니다.- hit zone은 보통
pointer-events: none으로 두고, 실제 UI는 그 위에 별도 요소로 배치합니다.
금지 예:
// button 자체가 hit zone이 되어 UI 상호작용 계약이 깨집니다.
<button data-stratum-id="scan-button">Scan</button>권장 구조:
function OverlayPanel() {
return (
<section style={{ position: 'relative' }}>
<div
aria-hidden="true"
data-stratum-id="inventory-panel"
style={{
position: 'absolute',
inset: 0,
pointerEvents: 'none',
}}
/>
<button data-overlay-control="transient">Scan</button>
<input data-overlay-input="true" />
</section>
);
}data-stratum-id는 "이 영역을 overlay hit zone으로 계산한다"는 신호입니다. UI 클릭, focus, 입력은 실제 button/input/select 요소가 담당해야 합니다.
텍스트 입력처럼 focus가 필요한 요소에는 data-overlay-input="true"를 붙이고, focus 시 setInputMode(true)를 호출합니다.
<input
data-overlay-input="true"
onFocus={() => {
void window.overlayAPI.setInputMode(true);
void window.overlayAPI.setIgnore(false);
}}
onBlur={async () => {
await window.overlayAPI.setInputMode(false);
await window.overlayAPI.setIgnore(true);
await window.overlayAPI.focusLastTarget();
}}
/>버튼, checkbox, color picker처럼 짧게 조작하는 요소는 transient control로 다루는 편이 좋습니다.
async function beginTransientControlInteraction() {
await window.overlayAPI.setInputMode(true);
await window.overlayAPI.setIgnore(false);
}
async function endTransientControlInteraction() {
await window.overlayAPI.setInputMode(false);
await window.overlayAPI.setIgnore(true);
await window.overlayAPI.focusLastTarget();
}
<button
data-overlay-control="transient"
onPointerDown={() => {
void beginTransientControlInteraction();
}}
onClick={async () => {
try {
await doSomething();
} finally {
await endTransientControlInteraction();
}
}}
>
Run
</button>드래그 시작 시:
setIgnore(false)- 필요하면
setInputMode(false) - 드래그 상태 ref를 true로 설정
드래그 종료 시:
- hit-test rect 재계산
setIgnore(true)focusLastTarget()
예제 앱은 react-draggable을 사용하며, draggable panel 내부에 별도 data-stratum-id hit zone을 둡니다.
ch-overlay-node는 Windows WinEvent hook 기반으로 대상 창의 이벤트를 추적합니다. 대상 프로세스에 코드를 주입하지 않습니다.
처리하는 주요 이벤트:
focus: 대상 창이 활성화됨move: 대상 창 위치 또는 크기 변경blur: 대상 창 비활성화cursor: 커서가 대상 창 안에서 이동cursor-leave: 커서가 대상 창 밖으로 나감
focus와 move가 들어오면 런타임은 physical screen rect를 Electron DIP rect로 변환한 뒤 overlay BrowserWindow.setBounds()를 호출합니다.
네이티브 추적과 화면 색상 capture backend는 physical screen pixel을 사용합니다.
Electron BrowserWindow와 renderer DOM은 DIP/CSS pixel을 사용합니다.
런타임의 기본 원칙:
Native focus/move physical rect
-> Electron main screen.screenToDipRect()
-> BrowserWindow.setBounds(DIP rect)
Renderer client point/region
-> Electron main screen.dipToScreenPoint()
-> screen color backend physical point/region
DPI, 다중 모니터, 배율이 다른 모니터 이동을 다룰 때 이 좌표계를 섞지 않는 것이 중요합니다.
런타임은 두 개의 BrowserWindow를 만듭니다.
| 창 | 역할 |
|---|---|
controlWindow |
설정, 검증, 대시보드 UI |
overlayWindow |
대상 창 위에 표시되는 투명 오버레이 UI |
현재 예제 route:
| route | 컴포넌트 |
|---|---|
#/main |
renderer/src/pages/main/UserDashboard.jsx |
#/overlay |
renderer/src/pages/GameOverlay/index.jsx |
색상 backend가 준비되면 renderer에서 화면 색상을 읽을 수 있습니다. Windows는 dxgi-color-node, macOS는 ScreenCaptureKit helper를 사용합니다.
대표 사용처:
- 현재 커서 위치 색상 추출
- 특정 UI 영역 평균 색상 측정
- 화면 영역 raw capture
- 목표 색상과 유사한 pixel scan
- 감지된 color blob을 marker로 표시
예제 overlay는 captureClientRegionRaw()로 화면을 tile capture하고, 목표 HEX/RGB 색상과 tolerance를 기준으로 matching blob을 찾습니다.
yarn build:library:package는 패키지 파일 hash를 모아 INTEGRITY.json과 SHA256SUMS.txt를 생성합니다.
선택적으로 private key를 제공하면 manifest signature도 생성합니다.
yarn security:generate-integrity-keypair
$env:STRATUM_INTEGRITY_PRIVATE_KEY_PATH="release\keys\integrity-private.pem"
yarn build:library:package런타임은 시작 시 manifest를 읽고 각 파일 hash를 검증합니다. signature public key가 포함되어 있거나 requireSignedIntegrity가 켜져 있으면 signature까지 검증합니다.
라이선스 검증은 클라이언트 단독 보안 기능이 아니라 서버와 함께 사용할 때 의미가 있습니다.
권장 흐름:
- 앱이
license.endpoint로appId,appVersion,machineId,metadata를 보냅니다. - 서버가
{ payload, signature }envelope를 반환합니다. - 런타임이 public key로 signature를 검증합니다.
payload.expiresAt이 만료되었으면 실패합니다.payload.licenseKey가 있으면 native init에 전달합니다.
| 명령어 | 설명 |
|---|---|
yarn dev |
Vite dev server만 실행 |
yarn electron |
Electron main process만 실행 |
yarn start |
Vite :3000 실행 후 Electron 실행 |
yarn build |
renderer를 dist/로 빌드 |
yarn build:ch-overlay |
창 추적 native module 빌드 |
yarn build:dxgi-color |
Windows DXGI 색상 native module 빌드 |
yarn build:native |
Windows native module 모두 빌드 |
yarn build:library:package |
배포용 the-overlay 패키지 생성 |
yarn validate:library-package |
생성 패키지 구조/exports/무결성 검증 |
yarn pack:library |
the-overlay.tgz 생성 |
yarn pack:win |
unpacked Windows 앱 빌드 |
yarn build:win |
Windows installer와 portable EXE 빌드 |
yarn lint |
JS syntax와 release config 검증 |
yarn test:renderer |
Vitest/jsdom으로 renderer React 테스트 실행 |
yarn test:renderer:watch |
renderer 테스트 watch mode 실행 |
yarn test |
runtime, preload, renderer, native bridge, visibility, JS benchmark, Rust correctness 검증 |
yarn test:generated |
라이브러리 패키지를 생성한 뒤 source/generated package 양쪽 회귀 검증 |
yarn test:all |
yarn test와 yarn test:generated를 연속 실행 |
yarn rust:quality |
Rust check/test, clippy, Criterion benchmark 실행 |
yarn rust:security |
cargo-audit, cargo-deny, cargo-geiger 기반 보안/unsafe 진단 |
생성된 라이브러리 패키지는 다음 entry를 제공합니다.
{
"type": "commonjs",
"main": "./index.cjs",
"module": "./index.mjs",
"types": "./index.d.ts",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.mjs",
"require": "./index.cjs"
},
"./preload": "./main/preload.js",
"./package.json": "./package.json"
}
}일반 소비자는 root import만 사용하세요.
import { processTracking } from 'the-overlay';const { processTracking } = require('the-overlay');생성 패키지의 설치 조건은 Windows/macOS, Node >=18, Electron >=28입니다. 런타임 패키지에는 dotenv, main/, native/, 무결성/라이선스 공개 파일만 포함되며 README와 예제 renderer는 publish surface에 포함되지 않습니다.
확인할 것:
- Windows 또는 macOS에서 실행 중인지 확인합니다.
- Windows에서는
yarn build:ch-overlay또는yarn build:native를 실행했는지 확인합니다. - macOS에서는 시스템 설정에서 호스트 앱의 Accessibility 권한이 허용되어 있는지 확인합니다.
targets의 창 제목/앱 이름과 실제 대상이 맞는지 확인합니다.mode: 'exact'가 너무 엄격하면mode: 'contains'로 바꿔 봅니다.STRATUM_COMPAT_DEBUG=1을 켜서 native bridge load 실패 로그를 확인합니다.
확인할 것:
- 해당 창이 런타임이 생성한
controlWindow또는overlayWindow인지 확인합니다. - 직접 만든 창이라면
resolvePreloadPath()로 preload를 지정했는지 확인합니다. contextIsolation: true상태에서main/preload.js가 로드되는지 확인합니다.
확인할 것:
- 기본 상태는 click-through입니다. hot zone 진입 시
setIgnore(false)가 호출되어야 합니다. - 실제 버튼/input에
data-stratum-id를 직접 붙이지 말고 별도 hit zone 요소를 사용합니다. - hit zone 요소는
pointer-events: none이어야 실제 UI가 이벤트를 받을 수 있습니다. - 입력 요소는 focus 중
setInputMode(true)를 호출해야 합니다. - 조작 종료 후
setIgnore(true)와focusLastTarget()를 호출해야 대상 앱 focus가 복구됩니다.
확인할 것:
await window.overlayAPI.screenColor.isReady()가true인지 확인합니다.- Windows에서는
yarn build:dxgi-color또는yarn build:native를 실행했는지 확인합니다. - macOS에서는 System Settings > Privacy & Security > Screen Recording에서 실행 중인 Electron/터미널 앱에 권한을 부여한 뒤 재시작합니다.
- client 좌표를 넘기는 경우
sampleClientPoint()또는captureClientRegionRaw()를 사용합니다. - physical screen 좌표를 직접 넘기는 경우에만
samplePoint()또는captureRegionRaw()를 사용합니다.
확인할 것:
release/npm/library-package를 직접 수정하지 않았는지 확인합니다.yarn build:library:package를 다시 실행합니다.- Electron packaging 과정에서 라이브러리 파일이 이동/삭제되는지 확인합니다.
- packager가 파일 구성을 바꾸는 것이 의도된 경우에만
verifyIntegrity: false를 사용합니다.
docs/LIBRARY_PACKAGING.md: 라이브러리 패키징과 release 정책docs/DPI_COORDINATE_MODEL.md: DPI/좌표계 계약docs/RUST_QUALITY.md: Rust 테스트, clippy, benchmark, 보안 진단 명령docs/2026_RUST_REPORT.md: Rust 성능 회귀 테스트와 벤치마크 운영 보고서docs/RELEASE_CHECKLIST.md: 배포 전 검증 순서docs/WINDOWS_CODESIGNING.md: Windows code signingdocs/UNSIGNED_WINDOWS_RELEASE.md: unsigned Windows 배포 주의 사항.rust-skills/AGENTS.md: Rust/native 개발 가이드