Skip to main content

HTML/JavaScript 사용 가이드

HTML/JavaScript를 사용하여 Klever One SDK를 통합하는 방법을 알아보세요.

준비사항

환경 요구사항

  • 보안 컨텍스트 필수: HTTPS 또는 localhost
  • 브라우저 지원: Chrome, Firefox, Safari (최신 버전)
  • 권한: 마이크 접근 권한

SDK 파일 준비

<!-- CDN 또는 로컬 파일 -->
<script
defer
src="https://unpkg.com/@klever-one/web-sdk@0.1.0-beta.19/dist/core/klever-one-core-v0.1.0-beta.19.umd.js"
></script>

빠른 시작

기본 HTML 구조

index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Klever One SDK Example</title>
<style>
#streaming-container {
width: 800px;
height: 600px;
background: #000;
margin: 20px auto;
}

.controls {
text-align: center;
margin: 20px;
}

.controls button {
margin: 5px;
padding: 10px 20px;
font-size: 16px;
}
</style>
</head>
<body>
<div id="streaming-container"></div>

<div class="controls">
<button id="connect-btn">연결</button>
<button
id="disconnect-btn"
disabled
>
연결 해제
</button>
<div id="status-display">연결 안됨</div>
</div>

<script
defer
src="https://unpkg.com/@klever-one/web-sdk@0.1.0-beta.19/dist/core/klever-one-core-v0.1.0-beta.19.umd.js"
></script>
<script
defer
src="./app.js"
></script>
</body>
</html>

기본 JavaScript 구현

app.js
// DOM 요소 참조
const elements = {
connectBtn: document.getElementById("connect-btn"),
disconnectBtn: document.getElementById("disconnect-btn"),
statusDisplay: document.getElementById("status-display"),
container: document.getElementById("streaming-container"),
};

let client = null;

// SDK 초기화 및 연결
async function initialize() {
try {
const { KleverOneClient } = window.KleverOneSdk;

client = new KleverOneClient({
apiKey: "your-api-key",
container: elements.container,
callbacks: {
onReady: () => updateStatus("준비 완료"),
onError: (error) => updateStatus(`오류: ${error.message}`),
onConnectionStatusChange: (status) => updateConnectionUI(status),
},
});

await client.connect();
} catch (error) {
updateStatus(`초기화 실패: ${error.message}`);
}
}

// 상태 표시 업데이트
function updateStatus(message) {
elements.statusDisplay.textContent = message;
}

// 연결 상태에 따른 UI 업데이트
function updateConnectionUI(status) {
const isConnected = status === "connected";
elements.connectBtn.disabled = isConnected;
elements.disconnectBtn.disabled = !isConnected;
updateStatus(isConnected ? "연결됨" : "연결 안됨");
}

// 이벤트 리스너 등록
elements.connectBtn.addEventListener("click", initialize);
elements.disconnectBtn.addEventListener("click", () => {
client?.disconnect();
updateStatus("연결 해제됨");
});

연결 관리

연결 상태 관리

class ConnectionManager {
constructor(containerId, apiKey) {
this.container = document.getElementById(containerId);
this.apiKey = apiKey;
this.client = null;
this.status = "disconnected";
}

async connect() {
if (this.status === "connected") return;

try {
const { KleverOneClient } = window.KleverOneSdk;

this.client = new KleverOneClient({
apiKey: this.apiKey,
container: this.container,
callbacks: {
onReady: () => this.onStatusChange("connected"),
onDisconnect: () => this.onStatusChange("disconnected"),
onError: (error) => this.onError(error),
},
});

await this.client.connect();
} catch (error) {
this.onError(error);
}
}

disconnect() {
if (this.client) {
this.client.disconnect();
this.client = null;
}
}

async reconnect() {
this.disconnect();
await new Promise((resolve) => setTimeout(resolve, 1000));
await this.connect();
}

onStatusChange(newStatus) {
this.status = newStatus;
console.log("연결 상태:", newStatus);
}

onError(error) {
console.error("연결 오류:", error.message);
this.status = "error";
}

isConnected() {
return this.status === "connected";
}
}

자동 재연결 구현

class AutoReconnector {
constructor(connectionManager) {
this.connectionManager = connectionManager;
this.reconnectAttempts = 0;
this.maxRetries = 3;
this.retryInterval = 2000;
}

async startAutoReconnect() {
if (this.reconnectAttempts >= this.maxRetries) {
console.log("최대 재연결 시도 횟수 초과");
return;
}

this.reconnectAttempts++;
console.log(`재연결 시도 ${this.reconnectAttempts}/${this.maxRetries}`);

try {
await this.connectionManager.reconnect();
this.reconnectAttempts = 0; // 성공 시 카운터 리셋
} catch (error) {
console.error("재연결 실패:", error.message);
setTimeout(() => this.startAutoReconnect(), this.retryInterval);
}
}
}

메시지 전송

텍스트 메시지 전송

class MessageSender {
constructor(client) {
this.client = client;
}

async sendText(message) {
if (!this.client || !this.client.isReady()) {
throw new Error("클라이언트가 준비되지 않았습니다");
}

try {
await this.client.sendText(message);
console.log("메시지 전송 완료:", message);
return true;
} catch (error) {
console.error("메시지 전송 실패:", error.message);
return false;
}
}

speak(text) {
if (!this.client || !this.client.isReady()) {
console.warn("클라이언트가 준비되지 않았습니다");
return;
}

this.client.speak(text);
console.log("발화 요청:", text);
}

speakMultiple(texts) {
if (!this.client || !this.client.isReady()) {
console.warn("클라이언트가 준비되지 않았습니다");
return;
}

this.client.speak(texts);
console.log("순차 발화 요청:", texts.length + "개 문장");
}
}

입력 필드와 연동

function setupMessageInput() {
const messageInput = document.getElementById("message-input");
const sendBtn = document.getElementById("send-btn");
const speakBtn = document.getElementById("speak-btn");

const messageSender = new MessageSender(client);

sendBtn.addEventListener("click", async () => {
const message = messageInput.value.trim();
if (!message) return;

const success = await messageSender.sendText(message);
if (success) {
messageInput.value = "";
}
});

speakBtn.addEventListener("click", () => {
const message = messageInput.value.trim();
if (!message) return;

messageSender.speak(message);
messageInput.value = "";
});

// Enter 키로 전송
messageInput.addEventListener("keypress", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendBtn.click();
}
});
}

음성 녹음

녹음 관리

class RecordingManager {
constructor(client) {
this.client = client;
this.isRecording = false;
}

async startRecording() {
if (this.isRecording) return;

try {
await this.client.startRecording();
this.isRecording = true;
console.log("녹음 시작");
this.onRecordingStart();
} catch (error) {
console.error("녹음 시작 실패:", error.message);
}
}

async stopRecording() {
if (!this.isRecording) return;

try {
await this.client.stopRecording();
this.isRecording = false;
console.log("녹음 중지");
this.onRecordingStop();
} catch (error) {
console.error("녹음 중지 실패:", error.message);
}
}

async toggleRecording() {
if (this.isRecording) {
await this.stopRecording();
} else {
await this.startRecording();
}
}

onRecordingStart() {
const recordBtn = document.getElementById("record-btn");
if (recordBtn) {
recordBtn.textContent = " 녹음 중지";
recordBtn.classList.add("recording");
}
}

onRecordingStop() {
const recordBtn = document.getElementById("record-btn");
if (recordBtn) {
recordBtn.textContent = "녹음 시작";
recordBtn.classList.remove("recording");
}
}
}

녹음 버튼 구현

function setupRecordingButton() {
const recordBtn = document.createElement("button");
recordBtn.id = "record-btn";
recordBtn.textContent = "녹음 시작";
recordBtn.className = "record-button";

// 버튼 스타일
recordBtn.style.cssText = `
padding: 10px 20px;
font-size: 16px;
border: none;
border-radius: 5px;
background: #007bff;
color: white;
cursor: pointer;
transition: all 0.3s ease;
`;

const recordingManager = new RecordingManager(client);
recordBtn.addEventListener("click", () => recordingManager.toggleRecording());

document.querySelector(".controls").appendChild(recordBtn);

// 녹음 중일 때 스타일 변경
const style = document.createElement("style");
style.textContent = `
.record-button.recording {
background: #dc3545 !important;
animation: pulse 1s infinite;
}

@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
`;
document.head.appendChild(style);
}

️고급 기능

이벤트 처리 시스템

class EventManager {
constructor() {
this.listeners = new Map();
}

on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}

off(event, callback) {
if (!this.listeners.has(event)) return;

const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}

emit(event, data) {
if (!this.listeners.has(event)) return;

this.listeners.get(event).forEach((callback) => {
try {
callback(data);
} catch (error) {
console.error(`이벤트 핸들러 오류 (${event}):`, error);
}
});
}

setupClientEvents(client) {
const callbacks = {
onReady: () => this.emit("ready"),
onDisconnect: () => this.emit("disconnect"),
onMessageReceived: (message) => this.emit("message", message),
onError: (error) => this.emit("error", error),
};

Object.entries(callbacks).forEach(([key, callback]) => {
if (client.callbacks) {
const originalCallback = client.callbacks[key];
client.callbacks[key] = (...args) => {
originalCallback?.(...args);
callback(...args);
};
}
});
}
}

상태 모니터링 대시보드

class StatusDashboard {
constructor(client) {
this.client = client;
this.dashboardElement = null;
this.createDashboard();
this.startMonitoring();
}

createDashboard() {
this.dashboardElement = document.createElement("div");
this.dashboardElement.id = "status-dashboard";
this.dashboardElement.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.8);
color: white;
padding: 15px;
border-radius: 5px;
font-family: monospace;
font-size: 12px;
z-index: 1000;
`;
document.body.appendChild(this.dashboardElement);
}

updateDashboard() {
if (!this.client) return;

const state = this.client.getState();
const metrics = this.client.getMetrics();

const statusHTML = `
<div><strong>상태 모니터</strong></div>
<div>연결: ${state.connection}</div>
<div>녹음: ${state.recording}</div>
<div>전송: ${metrics.messagesSent}개</div>
<div>수신: ${metrics.messagesReceived}개</div>
<div>오류: ${metrics.errorCount}개</div>
`;

this.dashboardElement.innerHTML = statusHTML;
}

startMonitoring() {
setInterval(() => this.updateDashboard(), 1000);
}

destroy() {
if (this.dashboardElement) {
document.body.removeChild(this.dashboardElement);
}
}
}

실제 사용 예제

완전한 대화형 애플리케이션

class DigitalHumanApp {
constructor() {
this.client = null;
this.eventManager = new EventManager();
this.connectionManager = null;
this.messageSender = null;
this.recordingManager = null;
this.statusDashboard = null;

this.initialize();
}

async initialize() {
try {
// UI 설정
this.setupUI();

// 연결 관리자 초기화
this.connectionManager = new ConnectionManager(
"streaming-container",
"your-api-key",
);

// 이벤트 리스너 설정
this.setupEventListeners();

console.log("애플리케이션 초기화 완료");
} catch (error) {
console.error("초기화 실패:", error);
}
}

setupUI() {
// 메시지 입력 필드 추가
const controlsDiv = document.querySelector(".controls");
controlsDiv.innerHTML += `
<div style="margin: 20px 0;">
<input type="text" id="message-input" placeholder="메시지를 입력하세요..."
style="width: 300px; padding: 10px; margin-right: 10px;">
<button id="send-btn">전송</button>
<button id="speak-btn">말하기</button>
</div>
`;
}

setupEventListeners() {
// 연결 버튼
document
.getElementById("connect-btn")
.addEventListener("click", () => this.connect());

// 연결 해제 버튼
document
.getElementById("disconnect-btn")
.addEventListener("click", () => this.disconnect());

// 이벤트 관리자 이벤트
this.eventManager.on("ready", () => {
console.log("디지털 휴먼 준비 완료!");
this.onReady();
});

this.eventManager.on("message", (message) => {
console.log("새 메시지:", message.content);
});

this.eventManager.on("error", (error) => {
console.error("오류:", error.message);
});
}

async connect() {
try {
await this.connectionManager.connect();
this.client = this.connectionManager.client;

// 매니저들 초기화
this.messageSender = new MessageSender(this.client);
this.recordingManager = new RecordingManager(this.client);
this.statusDashboard = new StatusDashboard(this.client);

// 이벤트 설정
this.eventManager.setupClientEvents(this.client);
} catch (error) {
console.error("연결 실패:", error);
}
}

disconnect() {
this.connectionManager?.disconnect();
this.statusDashboard?.destroy();
this.client = null;
this.messageSender = null;
this.recordingManager = null;
this.statusDashboard = null;
}

onReady() {
// 환영 메시지
this.messageSender?.speak("안녕하세요! 디지털 휴먼이 준비되었습니다.");

// 메시지 입력 활성화
this.setupMessageInput();
this.setupRecordingButton();
}

setupMessageInput() {
const messageInput = document.getElementById("message-input");
const sendBtn = document.getElementById("send-btn");
const speakBtn = document.getElementById("speak-btn");

sendBtn.addEventListener("click", () => {
const message = messageInput.value.trim();
if (message) {
this.messageSender.sendText(message);
messageInput.value = "";
}
});

speakBtn.addEventListener("click", () => {
const message = messageInput.value.trim();
if (message) {
this.messageSender.speak(message);
messageInput.value = "";
}
});
}

setupRecordingButton() {
setupRecordingButton(); // 위에서 정의한 함수 사용
}
}

// 애플리케이션 시작
window.addEventListener("DOMContentLoaded", () => {
window.app = new DigitalHumanApp();
});

에러 처리

글로벌 에러 핸들러

class ErrorHandler {
constructor() {
this.setupGlobalHandlers();
}

setupGlobalHandlers() {
window.addEventListener("error", (event) => {
this.handleError("스크립트 오류", event.error);
});

window.addEventListener("unhandledrejection", (event) => {
this.handleError("Promise 오류", event.reason);
});
}

handleError(type, error) {
const errorInfo = {
type,
message: error?.message || "알 수 없는 오류",
stack: error?.stack,
timestamp: new Date().toISOString(),
};

console.error("오류 발생:", errorInfo);

// 사용자에게 알림 (선택적)
this.showUserNotification(errorInfo);
}

showUserNotification(errorInfo) {
const notification = document.createElement("div");
notification.style.cssText = `
position: fixed;
bottom: 20px;
left: 20px;
background: #ff4444;
color: white;
padding: 15px;
border-radius: 5px;
z-index: 10000;
`;
notification.textContent = `오류: ${errorInfo.message}`;

document.body.appendChild(notification);

setTimeout(() => {
document.body.removeChild(notification);
}, 5000);
}
}

// 글로벌 에러 핸들러 활성화
new ErrorHandler();

주요 고려사항

성능 최적화

  • lazy loading: SDK 스크립트를 필요할 때만 로드
  • 이벤트 정리: 페이지 언로드 시 리스너 제거
  • 메모리 관리: 사용하지 않는 객체 참조 해제

보안 고려사항

  • API 키 보호: 환경 변수나 서버에서 관리
  • HTTPS 사용: 프로덕션에서는 반드시 HTTPS
  • 권한 요청: 마이크 권한을 명시적으로 요청

브라우저 호환성

  • 모던 브라우저: ES6+ 기능 사용
  • 폴리필: 필요시 적절한 폴리필 추가
  • 기능 감지: 브라우저 기능 지원 여부 확인

관련 문서