Electron
Experimental
이 패키지는 실험 단계입니다. API가 예고 없이 변경될 수 있으며, 프로덕션 환경에서의 안정성이 보장되지 않습니다.
Electron용 Klleon AI 아바타 SDK (@klleon/sdk-electron)는 Web SDK와 동일한 API를 제공하며, 데스크톱 네이티브 기능(IPC, 클립보드, 트레이 등)과 통합됩니다.
SDK 요청
Electron SDK는 별도 요청을 통해 제공됩니다. 담당자에게 문의하여 SDK 패키지를 받으세요.
요구사항
| 항목 | 버전 |
|---|---|
| Electron | >= 33 |
| Node.js | >= 20 |
| agora-electron-sdk | >= 4.3.2 |
설치
1. SDK 패키지 설치
제공받은 .tgz 파일을 프로젝트 루트에 두고 설치합니다.
# SDK 설치
npm install ./klleon-sdk-electron-x.x.x.tgz
# Agora 네이티브 모듈 설치
npm install agora-electron-sdk설치 후 package.json에 다음과 같이 추가됩니다:
{
"dependencies": {
"@klleon/sdk-electron": "file:klleon-sdk-electron-x.x.x.tgz",
"agora-electron-sdk": "^4.3.2"
}
}2. SDK 업데이트
새 버전의 .tgz를 받으면 기존 파일을 교체하고 다시 설치합니다.
npm install ./klleon-sdk-electron-x.x.x.tgz3. Vite 설정
@klleon/sdk-electron은 순수 JavaScript 패키지(.node 바이너리 없음)이므로 Vite가 렌더러 번들에 포함시켜 tree shaking합니다. external에 넣으면 패키징된 앱에서 모듈을 찾지 못합니다.
C++ 바이너리가 포함된 네이티브 모듈(agora-electron-sdk)과 Electron 내장 모듈(electron)만 external로 설정합니다.
// vite.renderer.config.ts
export default defineConfig({
optimizeDeps: {
exclude: ["agora-electron-sdk", "electron"],
// ❌ "@klleon/sdk-electron"은 여기 넣지 않기
},
build: {
rollupOptions: {
external: ["agora-electron-sdk", "electron"],
// ❌ "@klleon/sdk-electron"은 여기 넣지 않기
},
},
});ESM Tree Shaking
@klleon/sdk-electron은 ESM으로 빌드됩니다. Vite가 번들링하면서 사용하지 않는 코드를 자동으로 제거(tree shaking)합니다.
패키징 (DMG / EXE)
Electron Forge로 배포용 패키지를 만들 수 있습니다.
알려진 이슈
Electron Forge Vite 플러그인 버그
@electron-forge/plugin-vite는 패키징 시 node_modules를 출력에 포함하지 않습니다. Vite가 모든 코드를 번들링했다고 판단하지만, external로 설정한 네이티브 모듈(agora-electron-sdk)과 그 의존성은 번들에 포함되지 않아 런타임에 로드 실패합니다.
이 문제는 Electron Forge에 공식 등록된 버그입니다:
해결: forge.config.ts 설정
Forge 공식 훅 packageAfterCopy로 node_modules를 패키징 결과에 복사합니다. 복사 후 Forge가 자동으로 devDependencies를 제거(prune)합니다.
import type { ForgeConfig } from "@electron-forge/shared-types";
import { VitePlugin } from "@electron-forge/plugin-vite";
import { AutoUnpackNativesPlugin } from "@electron-forge/plugin-auto-unpack-natives";
import path from "path";
import fs from "fs";
const config: ForgeConfig = {
packagerConfig: {
asar: {
// 네이티브 .node 파일은 asar 밖에서 로드해야 함
unpack: "**/node_modules/{agora-electron-sdk,@anthropic-ai}/**",
},
// assets 디렉토리를 Contents/Resources/에 복사
extraResource: ["./assets"],
},
hooks: {
packageAfterCopy: async (_config, buildPath) => {
// Forge Vite 플러그인 버그 (#3738) 워크어라운드
// 복사 후 Forge가 자동으로 devDependencies prune 처리
fs.cpSync(
path.join(__dirname, "node_modules"),
path.join(buildPath, "node_modules"),
{ recursive: true },
);
},
},
plugins: [
// 네이티브 .node 파일을 asar에서 자동 추출
new AutoUnpackNativesPlugin({}),
new VitePlugin({
build: [
{ entry: "electron/main.ts", config: "vite.main.config.ts", target: "main" },
{ entry: "electron/preload.ts", config: "vite.preload.config.ts", target: "preload" },
],
renderer: [
{ name: "main_window", config: "vite.renderer.config.ts" },
],
}),
],
makers: [
{ name: "@electron-forge/maker-dmg", config: {} },
{ name: "@electron-forge/maker-squirrel", config: {} },
{ name: "@electron-forge/maker-deb", config: {} },
],
};
export default config;빌드 명령어
# 개발
npm run dev # electron-forge start (HMR)
# 패키징
npm run make # DMG/EXE/DEB 생성API
KlleonSDK (싱글톤)
import { KlleonSDK } from "@klleon/sdk-electron";| 분류 | 메서드 | 설명 |
|---|---|---|
| 생명주기 | init(options) | SDK 초기화 및 연결 |
destroy() | SDK 종료 및 리소스 해제 | |
| 콜백 | onSignal(cb) | 서버 시그널 수신 |
onStatus(cb) | SDK 상태 변경 | |
onError(cb) | 에러 발생 | |
| 텍스트 | sendMessage(text) | 텍스트 전송 (LLM 응답 트리거) |
speak(text) | 텍스트 에코 (아바타가 그대로 읽음) | |
| 오디오 에코 | sendSpeakAudio(base64) | Base64 오디오 전송 |
endSpeakAudio() | 오디오 에코 종료 | |
| 음성 | startListening() | 음성 입력 시작 (Push-to-Talk) |
endListening() | 음성 입력 종료 | |
cancelListening() | 음성 입력 취소 | |
| 아바타 | stopSpeaking() | 아바타 발화 중지 |
setVolume(0~100) | 아바타 음량 조절 | |
| 비디오 | setContainer(container) | 비디오 렌더링 컨테이너 설정 (<avatar-container> 사용 시 자동) |
| 조회 | getVersion() | SDK 버전 |
getStatus() | 현재 상태 | |
clearMessages() | 메시지 초기화 |
비디오 렌더링
<avatar-container> (권장)
HTML에 <avatar-container>를 넣으면 SDK 연결 시 자동으로 비디오가 렌더링됩니다. 내부적으로 setContainer()를 호출하고, 원격 유저 합류 시 <agora-view>를 생성·삽입합니다.
<avatar-container></avatar-container>| Attribute | 기본값 | 설명 |
|---|---|---|
fit | "cover" | 비디오 fit 방식 (cover, contain, fill) |
<avatar-container fit="contain"></avatar-container>setContainer() (수동)
커스텀 엘리먼트 대신 직접 컨테이너를 지정할 수도 있습니다. init() 전에 호출해도 됩니다.
KlleonSDK.setContainer(document.getElementById("my-avatar")!);agora-electron-sdk 참고
Agora 공식 문서에서는 macOS 패키징 시 심볼 충돌 방지를 위해 import 대신 require를 권장합니다. @klleon/sdk-electron은 내부적으로 new Function("return require(module)")을 사용하여 Vite 번들링을 우회하고 런타임에 네이티브 모듈을 로드합니다.
sdkState (반응형 상태)
Valtio proxy로 상태를 구독할 수 있습니다.
import { sdkState } from "@klleon/sdk-electron";
import { subscribe } from "valtio/vanilla";
subscribe(sdkState, () => {
console.log("status:", sdkState.status);
console.log("messages:", sdkState.messageList);
});초기화 옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
sdk_key | string | - | SDK 인증 키 (필수) |
avatar_id | string | - | 아바타 ID (필수) |
language | LanguageCode | "ko_kr" | TTS 언어 (ko_kr, en_us, ja_jp, id_id) |
log_level | LogLevel | "debug" | 로그 레벨 |
enable_microphone | boolean | true | 마이크 활성화 |
auto_send | boolean | false | true: VAD 자동 응답, false: Push-to-Talk |
stt_only | boolean | false | true: STT 결과만, false: STT + 아바타 응답 |
상태 흐름
IDLE → CONNECTING → SOCKET_CONNECTED → STREAMING_CONNECTED → CONNECTED_FINISH| 상황 | 흐름 |
|---|---|
| 정상 종료 | destroy() → IDLE |
| 에러 발생 | 자동 cleanup → IDLE |
| 재연결 | onError + onStatus(IDLE) 수신 후 init() 재호출 |
에러 코드
| 코드 | 설명 |
|---|---|
SOCKET_FAILED | init 중 WebSocket 연결 실패 |
SOCKET_DISCONNECTED_UNEXPECTEDLY | 런타임 WebSocket 끊김 |
STREAMING_FAILED | init 중 Agora 연결 실패 |
STREAMING_DISCONNECTED_UNEXPECTEDLY | 런타임 Agora 끊김 |
SERVER_ERROR | 서버 에러 시그널 |
WORKER_DISCONNECTED | 워커 종료 시그널 |
BrowserWindow 설정
agora-electron-sdk는 렌더러 프로세스에서 Node.js API를 사용합니다.
보안 참고
nodeIntegration: true + contextIsolation: false는 Electron 보안 권장사항에 반하지만, agora-electron-sdk가 렌더러에서 require()를 직접 사용하기 때문에 현재로서는 불가피합니다. Agora SDK가 contextIsolation을 지원하면 즉시 마이그레이션하세요.
const mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: true, // 필수: agora-electron-sdk가 require() 사용
contextIsolation: false, // 필수: 렌더러에서 Node.js API 접근
},
});Web SDK와의 차이
| 항목 | Web (sdk-klleon) | Electron (sdk-electron) |
|---|---|---|
| Agora | agora-rtc-sdk-ng | agora-electron-sdk |
| 비디오 렌더링 | <video> track.play() | <agora-view> (내부) |
| 비디오 컨테이너 | <avatar-container> (Lit) | <avatar-container> (Web Component) |
| 모듈 로딩 | ESM import | require() (Vite 우회) |
| 디바이스 정보 | navigator.userAgent | process.platform / process.arch |
| UI | Lit 커스텀 엘리먼트 | HTML + Electron IPC |
| 배포 | CDN (UMD) | 앱 패키징 (DMG/EXE/DEB) |
| 패키징 | 불필요 | Electron Forge |