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+ 기능 사용
- 폴리필: 필요시 적절한 폴리필 추가
- 기능 감지: 브라우저 기능 지원 여부 확인