React 예제
사전 준비
index.html의 <head>에 SDK 스크립트를 추가합니다.
html
<script src="https://klleon.k1.klleon.io/{VERSION}/klleon-sdk.umd.js"></script>기본 예제
SDK Web Component를 사용하는 기본 React 예제입니다.
StrictMode 동작
SDK는 내부 Mutex로 init()/destroy() 호출을 직렬화합니다. React StrictMode의 이중 mount(mount → unmount → remount)에서도 cleanup의 destroy()와 두 번째 init()이 자동으로 직렬화되어 안전하게 재연결됩니다. destroy()는 미초기화 상태에서 호출해도 안전(no-op)하므로 cleanup에서 조건 없이 호출하세요.
tsx
import { useEffect, useState } from 'react';
const SDK_KEY = 'YOUR_SDK_KEY';
const AVATAR_ID = 'YOUR_AVATAR_ID';
function App() {
const [status, setStatus] = useState('IDLE');
const [speakText, setSpeakText] = useState('');
useEffect(() => {
const SDK = window.KlleonSDK;
SDK.onStatus((s) => setStatus(s));
SDK.onSignal((data) => {
console.log('시그널:', data.signal, data.payload);
});
SDK.onError((error) => {
console.error('에러:', error.code, error.message);
});
SDK.init({ sdk_key: SDK_KEY, avatar_id: AVATAR_ID })
.catch((e) => console.error('초기화 실패:', e.message));
return () => {
// destroy()는 미초기화 상태에서도 안전하며, 진행 중인 init 뒤로 자동 직렬화됩니다
SDK.destroy();
};
}, []);
const handleSpeak = () => {
if (speakText.trim()) {
window.KlleonSDK.speak(speakText);
setSpeakText('');
}
};
const isReady = status === 'CONNECTED_FINISH';
return (
<div style={{ display: 'flex', width: 800, height: 720, gap: 24 }}>
<avatar-container
style={{ flex: 1, borderRadius: 24, overflow: 'hidden' }}
/>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12 }}>
<p>상태: {status}</p>
{/* 대화 영역 — chat-container가 자체 입력 UI로 sendMessage()를 전송합니다 */}
<chat-container style={{ flex: 1, borderRadius: 24 }} />
{/* speak() 직접 호출 데모 */}
<p style={{ margin: 0, fontWeight: 'bold' }}>speak() 직접 호출 데모</p>
<p style={{ margin: 0, fontSize: 12, color: '#666' }}>
입력한 텍스트를 아바타가 그대로 읽습니다 (LLM 응답 없음).
대화 메시지는 위 chat-container 입력창을 사용하세요.
</p>
<input
value={speakText}
onChange={(e) => setSpeakText(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && !e.nativeEvent.isComposing && handleSpeak()}
placeholder="speak 내용 입력..."
disabled={!isReady}
/>
<button onClick={handleSpeak} disabled={!isReady}>speak 전송</button>
</div>
</div>
);
}
export default App;TypeScript 프로젝트
TypeScript에서 Web Component 사용 시 타입 에러가 발생하면 TypeScript 지원 페이지의 타입 정의 파일을 추가하세요.
커스텀 예제 (Web Component 없이)
SDK Web Component를 사용하지 않고 직접 UI를 구성하는 고급 예제입니다. SDK 이벤트와 메서드를 활용해 자유롭게 UI를 제어할 수 있습니다.
tsx
import { useEffect, useState } from 'react';
const SDK_KEY = 'YOUR_SDK_KEY';
const AVATAR_ID = 'YOUR_AVATAR_ID';
function CustomChat() {
const [status, setStatus] = useState('IDLE');
const [messages, setMessages] = useState<{ type: 'request' | 'response'; text: string }[]>([]);
const [text, setText] = useState('');
const [isSpeaking, setIsSpeaking] = useState(false);
useEffect(() => {
const SDK = window.KlleonSDK;
SDK.onStatus((s) => setStatus(s));
SDK.onSignal((data) => {
switch (data.signal) {
case 'RESPONSE_TEXT':
setMessages((prev) => [...prev, {
type: 'response', text: String(data.payload?.text ?? '')
}]);
setIsSpeaking(true);
break;
case 'STT_RESULT':
setMessages((prev) => [...prev, {
type: 'request', text: String(data.payload?.text ?? '')
}]);
break;
case 'RESPONSE_PREPARING':
setIsSpeaking(true);
break;
case 'RESPONSE_ENDED':
setIsSpeaking(false);
break;
}
});
SDK.onError((error) => {
console.error(error.code, error.message);
});
SDK.init({ sdk_key: SDK_KEY, avatar_id: AVATAR_ID })
.catch((e) => console.error(e.message));
return () => {
// destroy()는 미초기화 상태에서도 안전하며, 진행 중인 init 뒤로 자동 직렬화됩니다
SDK.destroy();
};
}, []);
const send = () => {
if (!text.trim() || isSpeaking) return;
window.KlleonSDK.sendMessage(text);
setMessages((prev) => [...prev, { type: 'request', text }]);
setText('');
};
const isReady = status === 'CONNECTED_FINISH';
return (
<div style={{ display: 'flex', gap: 24, height: 720 }}>
{/* 아바타 영역 */}
<avatar-container style={{ flex: 1, borderRadius: 24, overflow: 'hidden' }} />
{/* 채팅 영역 (직접 구현) */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12 }}>
<p>상태: {status}</p>
<div style={{
flex: 1, overflowY: 'auto', border: '1px solid #ccc',
borderRadius: 12, padding: 12
}}>
{messages.map((m, i) => (
<div key={i} style={{
textAlign: m.type === 'request' ? 'right' : 'left',
margin: '4px 0'
}}>
<span style={{
display: 'inline-block', padding: '8px 12px', borderRadius: 8,
background: m.type === 'request' ? '#3579CC' : '#f0f0f0',
color: m.type === 'request' ? '#fff' : '#333',
}}>
{m.text}
</span>
</div>
))}
</div>
{/* 컨트롤 */}
<div style={{ display: 'flex', gap: 8 }}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && !e.nativeEvent.isComposing && send()}
placeholder={isSpeaking ? '아바타 발화 중...' : '메시지 입력'}
disabled={!isReady || isSpeaking}
style={{ flex: 1, padding: 8 }}
/>
<button onClick={send} disabled={!isReady || isSpeaking}>전송</button>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={() => window.KlleonSDK.startListening()} disabled={!isReady}>STT 시작</button>
<button onClick={() => window.KlleonSDK.endListening()} disabled={!isReady}>STT 종료</button>
<button onClick={() => window.KlleonSDK.stopSpeaking()} disabled={!isSpeaking}>발화 중단</button>
</div>
</div>
</div>
);
}
export default CustomChat;