# Klleon SDK — Complete Reference for LLMs > AI 아바타와 실시간 대화를 구현하는 브라우저용 TypeScript SDK. > CDN UMD 번들로 배포. window.KlleonSDK 글로벌 변수로 접근. --- # Klleon SDK Klleon SDK는 웹 애플리케이션에 클레온의 실시간 인터랙티브 아바타를 통합하는 JavaScript SDK입니다. WebRTC 연결과 시그널링을 추상화하여 API 호출만으로 아바타 대화를 구현합니다. ## 주요 기능 - ` ``` `{VERSION}`을 실제 버전(예: `2.0.0`)으로 교체하세요. 로드 후 `window.KlleonSDK`로 SDK에 접근합니다. ## 기본 사용법 ### 1. HTML에 컴포넌트 추가 ```html ``` ### 2. 이벤트 구독 `onStatus`는 `init()` 중 상태 변경을 수신하려면 `init()` 전에 등록하세요. ```js const SDK = window.KlleonSDK; SDK.onStatus((status) => { console.log('상태 변경:', status); }); SDK.onSignal((data) => { console.log('시그널:', data.signal, data.payload); }); SDK.onError((error) => { console.error('에러:', error.code, error.message); }); ``` ```jsx useEffect(() => { const SDK = window.KlleonSDK; SDK.onStatus((status) => console.log('상태:', status)); SDK.onSignal((data) => console.log(data.signal, data.payload)); SDK.onError((error) => console.error(error.code, error.message)); }, []); ``` ### 3. SDK 초기화 ```js SDK.init({ sdk_key: 'YOUR_SDK_KEY', avatar_id: 'YOUR_AVATAR_ID', }).catch((e) => console.error('초기화 실패:', e.message)); ``` ```jsx import { useEffect, useRef } from 'react'; function App() { const initialized = useRef(false); useEffect(() => { const SDK = window.KlleonSDK; SDK.init({ sdk_key: 'YOUR_SDK_KEY', avatar_id: 'YOUR_AVATAR_ID', }) .then(() => { initialized.current = true; }) .catch((e) => console.error('초기화 실패:', e.message)); return () => { if (initialized.current) SDK.destroy(); }; }, []); } ``` ### 4. 메시지 전송 ```js window.KlleonSDK.sendMessage('안녕하세요'); ``` ### 5. SDK 종료 ```js window.KlleonSDK.destroy(); ``` ## 음성 입력 ```js window.KlleonSDK.startListening(); // STT 시작 window.KlleonSDK.endListening(); // STT 종료 ``` ## 전체 예제 ```html ``` ```jsx import { useEffect, useRef } from 'react'; function App() { const initialized = useRef(false); useEffect(() => { const SDK = window.KlleonSDK; SDK.onStatus((status) => console.log('상태:', status)); SDK.onSignal((data) => console.log(data.signal, data.payload)); SDK.onError((error) => console.error(error.code, error.message)); SDK.init({ sdk_key: 'YOUR_SDK_KEY', avatar_id: 'YOUR_AVATAR_ID', }) .then(() => { initialized.current = true; }) .catch((e) => console.error(e.message)); return () => { if (initialized.current) SDK.destroy(); }; }, []); return (
); } export default App; ``` --- # init() SDK를 초기화하고 아바타 연결을 시작합니다. ## 사용법 ```js await SDK.init(options); ``` ## InitOption | 파라미터 | 타입 | 필수 | 기본값 | 설명 | |---------|------|------|--------|------| | `sdk_key` | `string` | O | - | SDK 인증 키 | | `avatar_id` | `string` | O | - | 아바타 ID | | `log_level` | `LogLevel` | X | `"debug"` | 로그 레벨 | | `language` | `LanguageCode` | X | `"ko_kr"` | 언어 코드 (TTS 음성 + 자막) | | `enable_microphone` | `boolean` | X | `true` | 마이크 활성화 (STT용) | | `auto_send` | `boolean` | X | `false` | `false`(Push to Send): `startListening`/`endListening`으로 수동 제어. `true`(Auto Send): STT 상시 활성, 서버 VAD가 발화를 감지하여 자동 응답. Auto Send는 아바타가 지원하는 경우에만 동작하므로, 사전에 해당 아바타의 지원 여부를 확인하세요. | | `stt_only` | `boolean` | X | `false` | STT 전용 모드. `true`이면 아바타가 응답하지 않고 STT 결과만 반환합니다. `false`이면 STT 결과를 반환하고, 해당 결과를 기반으로 아바타가 응답합니다. | ### LogLevel 브라우저 콘솔에 출력되는 로그 수준을 제어합니다. 설정한 레벨 이상만 출력됩니다. | 값 | 출력 범위 | 용도 | |----|----------|------| | `"debug"` | debug, info, warn, error | 개발 중 상세 디버깅 (기본값) | | `"info"` | info, warn, error | 주요 이벤트만 확인 (연결 완료, 초기화 등) | | `"warn"` | warn, error | 잠재적 문제만 확인 | | `"error"` | error | 에러만 출력 | | `"silent"` | 없음 | 콘솔 출력 완전 비활성화 | ### LanguageCode 아바타의 TTS 음성 및 자막 언어를 지정합니다. | 값 | 언어 | |----|------| | `"ko_kr"` | 한국어 (기본값) | | `"en_us"` | 영어 | | `"ja_jp"` | 일본어 | | `"id_id"` | 인도네시아어 | ## 연결 흐름 ```mermaid sequenceDiagram participant App participant SDK participant API participant WS as WebSocket participant Streaming App->>SDK: init(options) SDK->>API: checkStatus (SDK 키 검증) API-->>SDK: OK SDK->>WS: connect WS-->>SDK: MATCHED (channel, token, uid) SDK->>Streaming: join(channel, token, uid) Streaming-->>SDK: video + audio track 수신 SDK-->>App: status: CONNECTED_FINISH ``` ## 에러 처리 실패 시 내부 cleanup 후 `IDLE`로 리셋하고 `Error`를 throw합니다. ```js try { await SDK.init(options); } catch (error) { // error.message에 실패 원인 포함 // "Failed to connect: ..." → SDK 키 검증 또는 WebSocket 연결 실패 // "Failed to connect streaming: ..." → 스트리밍 연결 실패 console.error(error.message); } ``` - init/destroy 진행 중 재호출 시 이전 작업 완료를 기다린 후 실행됩니다 (Mutex 직렬화). --- # 라이프사이클 ## destroy() SDK를 종료하고 모든 연결을 해제합니다. ```js await SDK.destroy(); ``` - WebSocket, 스트리밍 연결 종료 - 상태를 `IDLE`로 초기화 - 메시지 목록, 시그널 상태 초기화 - 원격 로거 분리 > **WARNING** `destroy()`는 비동기 함수입니다. 반드시 `await`를 사용해야 합니다. 비활성 상태(`IDLE`)에서 호출하면 무시됩니다. `destroy()` 후에도 이벤트 콜백은 유지되므로, 재초기화 시 다시 등록할 필요가 없습니다. ## 재연결 재연결이 필요한 경우 `destroy()` → `init()`을 사용하세요. ```js await SDK.destroy(); await SDK.init(options); ``` ## 라이프사이클 흐름 ```mermaid sequenceDiagram participant App participant SDK App->>SDK: 이벤트 콜백 등록 App->>SDK: init(options) SDK-->>App: CONNECTING SDK-->>App: SOCKET_CONNECTED SDK-->>App: STREAMING_CONNECTED SDK-->>App: CONNECTED_FINISH Note over App,SDK: 아바타 사용 중 App->>SDK: await destroy() SDK-->>App: IDLE Note over App,SDK: 콜백 유지됨 App->>SDK: init(options) SDK-->>App: CONNECTING ... ``` ## 상태 전이 ```mermaid flowchart TD IDLE -->|init| CONNECTING CONNECTING -->|WebSocket 연결| SOCKET_CONNECTED SOCKET_CONNECTED -->|비디오+오디오 수신| STREAMING_CONNECTED STREAMING_CONNECTED -->|완료| CONNECTED_FINISH CONNECTED_FINISH -->|destroy| IDLE CONNECTING -.->|연결 실패| IDLE SOCKET_CONNECTED -.->|스트리밍 실패| IDLE CONNECTED_FINISH -.->|런타임 에러| IDLE ``` ### SDKStatus | 값 | 설명 | |----|------| | `IDLE` | 초기/리셋 상태 | | `CONNECTING` | HTTP 검증 + WebSocket 연결 중 | | `CONNECTING_FAILED` | 연결 실패 (IDLE로 복귀) | | `SOCKET_CONNECTED` | WebSocket 연결 완료 | | `SOCKET_FAILED` | WebSocket 연결 실패 (IDLE로 복귀) | | `STREAMING_CONNECTED` | 비디오+오디오 트랙 수신 완료 | | `STREAMING_FAILED` | 스트리밍 연결 실패 (IDLE로 복귀) | | `CONNECTED_FINISH` | 모든 연결 완료. 사용 가능 | --- # 메시징 ## sendMessage(text) 아바타에게 텍스트 메시지를 전송합니다. 아바타가 LLM 응답을 생성하여 음성으로 답합니다. ```js SDK.sendMessage('안녕하세요'); ``` | 파라미터 | 타입 | 설명 | |---------|------|------| | `text` | `string` | 전송할 메시지 | ## speak(text) 아바타가 주어진 텍스트를 그대로 읽도록 합니다 (LLM 처리 없이). ```js SDK.speak('이 문장을 그대로 읽어주세요'); ``` | 파라미터 | 타입 | 설명 | |---------|------|------| | `text` | `string` | 아바타가 읽을 텍스트 | > **WARNING** `stopSpeaking()` 직후 `speak()`나 `sendMessage()`를 바로 호출하면, 서버가 요청을 드랍할 수 있습니다. `RESPONSE_ENDED` 시그널을 수신한 후에 호출하세요. ```js let pendingText = null; SDK.onSignal((data) => { if (data.signal === 'RESPONSE_ENDED' && pendingText) { SDK.speak(pendingText); pendingText = null; } }); function stopAndSpeak(text) { pendingText = text; SDK.stopSpeaking(); } ``` ## sendSpeakAudio(audio) Base64 인코딩된 오디오 데이터를 전송하여 아바타가 해당 음성으로 lip-sync 합니다. ```js SDK.sendSpeakAudio(base64AudioData); ``` | 파라미터 | 타입 | 설명 | |---------|------|------| | `audio` | `string` | Base64 인코딩된 오디오 데이터 (최대 0.1MB) | 0.1MB 초과 또는 유효하지 않은 Base64 형식은 거부됩니다. 여러 번 호출하여 청크 단위로 전송할 수 있으며, 전송 완료 후 `endSpeakAudio()`를 호출하세요. ## endSpeakAudio() 오디오 에코 전송을 종료합니다. 서버가 이 신호를 받으면 수신한 오디오로 lip-sync 처리를 시작합니다. ```js // 청크 단위로 전송 후 종료 for (const chunk of audioChunks) { SDK.sendSpeakAudio(chunk); } SDK.endSpeakAudio(); ``` ## clearMessages() `` 사용 시 대화 메시지 목록을 비웁니다. ```js SDK.clearMessages(); ``` --- # 음성 인식 (STT) > **TIP** 음성 입력을 기본 인터페이스로 사용하는 아바타의 경우, 연결 완료 시 자동으로 음성 인식을 시작할 수 있습니다. ```js SDK.onStatus((status) => { if (status === 'CONNECTED_FINISH') { SDK.startListening(); } }); ``` ## startListening() 마이크를 활성화하고 음성 인식을 시작합니다. ```js SDK.startListening(); ``` ## endListening() 음성 인식을 종료하고 마이크를 비활성화합니다. 인식된 텍스트가 있으면 아바타가 응답합니다. ```js SDK.endListening(); ``` ## cancelListening() 음성 인식을 취소하고 마이크를 비활성화합니다. 인식된 텍스트를 폐기하고 아바타가 응답하지 않습니다. ```js SDK.cancelListening(); ``` ## STT 관련 시그널 > **WARNING** `startListening()` / `endListening()`은 **STT 세션**(마이크 ON/OFF)을 제어합니다. `USER_SPEECH_STARTED` / `USER_SPEECH_STOPPED`는 세션 내 **발화 구간 감지**이며, 마이크는 계속 켜져 있습니다. `USER_SPEECH_STOPPED`을 수신해도 STT 세션은 종료되지 않습니다. `endListening()`을 호출해야 종료됩니다. ```js SDK.onSignal((data) => { switch (data.signal) { case 'USER_SPEECH_STARTED': // 사용자가 말하기 시작 (마이크는 이미 켜져 있음) console.log('사용자 발화 시작'); break; case 'USER_SPEECH_STOPPED': // 사용자가 말을 멈춤 (마이크는 여전히 켜져 있음) console.log('사용자 발화 종료'); break; case 'STT_RESULT': console.log('인식 결과:', data.payload.text); break; } }); ``` ## 전체 STT 흐름 ```mermaid sequenceDiagram participant App participant SDK participant Server App->>SDK: startListening() Server-->>SDK: USER_SPEECH_STARTED Server-->>SDK: USER_SPEECH_STOPPED App->>SDK: endListening() Server-->>SDK: STT_RESULT (인식된 텍스트) Server-->>SDK: RESPONSE_TEXT (아바타 응답) Server-->>SDK: RESPONSE_ENDED ``` --- # 아바타 제어 ## stopSpeaking() 아바타의 현재 발화를 즉시 중단합니다. ```js SDK.stopSpeaking(); ``` > **TIP** `stopSpeaking()` 직후 `speak()`나 `sendMessage()`를 호출하면 서버가 요청을 드랍할 수 있습니다. 안전한 호출 패턴은 [메시징 > speak()](/ko/api/messaging#speak-text)를 참고하세요. ## setVolume(volume) 아바타 음성 볼륨을 설정합니다. ```js SDK.setVolume(50); // 0 ~ 100 ``` | 파라미터 | 타입 | 설명 | |---------|------|------| | `volume` | `number` | 볼륨 (0 ~ 100) | ## getStatus() 현재 SDK 연결 상태를 반환합니다. ```js const status = SDK.getStatus(); // 'IDLE', 'CONNECTED_FINISH', ... ``` --- # 이벤트 ### 이벤트 등록 타이밍 콜백은 `init()` 전후 언제든 등록할 수 있습니다. 단, `onStatus`는 `init()` 중 상태 변경(`CONNECTING`, `SOCKET_CONNECTED` 등)을 수신하려면 `init()` 호출 전에 등록하세요. - 각 타입당 콜백 1개만 등록 가능하며, 마지막 등록이 이전을 덮어씁니다. - `destroy()` 후에도 콜백은 유지되므로 재초기화 시 다시 등록할 필요 없습니다. ## onSignal(callback) 서버로부터 수신한 시그널을 받습니다. ```js SDK.onSignal((data) => { console.log(data.signal, data.payload); }); ``` ```jsx useEffect(() => { window.KlleonSDK.onSignal((data) => { console.log(data.signal, data.payload); }); }, []); ``` ### 콜백 파라미터 | 필드 | 타입 | 필수 | 설명 | |------|------|------|------| | `signal` | `IncomingSignal` | O | 시그널 종류 (enum, 런타임에는 문자열) | | `payload` | `object` | X | 시그널별 추가 데이터 | | `id` | `string` | O | 시그널 UUID | | `type` | `string` | O | `"SESSION"` \| `"INPUT"` \| `"OUTPUT"` | ### 주요 시그널 #### 대화 흐름 | 시그널 | 설명 | payload | |--------|------|---------| | `RESPONSE_TEXT` | 아바타 응답 텍스트 | `{ text: string, language: string }` | | `STT_RESULT` | 음성 인식 결과 | `{ text: string }` | | `RESPONSE_PREPARING` | 응답 준비 중 (LLM 요청 직전) | - | | `RESPONSE_STARTED` | 응답 시작 | - | | `RESPONSE_ENDED` | 응답 완료 | - | | `USER_SPEECH_STARTED` | STT 세션 내 발화 구간 시작 (마이크는 이미 켜져 있음) | - | | `USER_SPEECH_STOPPED` | STT 세션 내 발화 구간 종료 (마이크는 여전히 켜져 있음) | - | | `ERROR` | 서버 에러 (세션 유지) | `{ type: "STT_ERROR" \| "LLM_ERROR" \| ... }` | | `REJECTED` | 입력 거절 (세션 유지) | `{ type: "STT_EMPTY" \| "MODERATION" \| ... }` | #### 세션 관리 | 시그널 | 설명 | payload | |--------|------|---------| | `MATCH_WAITING` | 매칭 대기 중 (로딩 UI 표시용) | - | | `WAIT` | 서버 준비 중 (계속 대기) | - | | `SESSION_ENDED` | 세션 정상 종료 | - | | `SESSION_TIMED_OUT` | 세션 타임아웃 | - | | `TIME_EXHAUSTED` | 사용 시간 소진 | - | | `QUOTA_EXCEEDED` | 동접 quota 초과 | - | | `SUSPEND_WARNED` | 오랜 대기 경고 | - | ## onStatus(callback) SDK 연결 상태 변경을 받습니다. ```js SDK.onStatus((status) => { console.log('상태:', status); if (status === 'CONNECTED_FINISH') { console.log('아바타 준비 완료'); } }); ``` ```jsx const [status, setStatus] = useState('IDLE'); useEffect(() => { window.KlleonSDK.onStatus((s) => setStatus(s)); }, []); ``` ## onError(callback) SDK 내부 에러 + 서버 에러를 받습니다. ```js SDK.onError((error) => { console.error(error.code, error.message); }); ``` ```jsx useEffect(() => { window.KlleonSDK.onError((error) => { console.error(error.code, error.message); }); }, []); ``` ### 콜백 파라미터 | 필드 | 타입 | 필수 | 설명 | |------|------|------|------| | `code` | `string` | O | 에러 코드 | | `message` | `string` | X | 상세 메시지 | ### 에러 코드 상세 #### 초기화 에러 (init 실패 시) | 코드 | 발생 시점 | message 예시 | |------|-----------|-------------| | `SOCKET_FAILED` | SDK 키 검증 실패 또는 WebSocket 연결 실패 | `"Failed to connect: ..."` | | `STREAMING_FAILED` | 스트리밍 연결 실패 | `"Failed to connect streaming: ..."` | > init 실패 시 `onError`는 호출되지 않습니다. `init()`이 `Error`를 throw하므로 `try/catch`로 처리하세요. #### 런타임 에러 (연결 중 발생) | 코드 | 발생 시점 | message 예시 | |------|-----------|-------------| | `SOCKET_DISCONNECTED_UNEXPECTEDLY` | WebSocket 에러 발생 | `"WebSocket disconnected unexpectedly"` | | `STREAMING_FAILED` | 원격 트랙 구독 실패 | `"Failed to subscribe to remote track"` | | `STREAMING_DISCONNECTED_UNEXPECTEDLY` | 스트리밍 비정상 종료 | `"Streaming disconnected: NETWORK_ERROR"` | #### 서버 에러 | 코드 | 발생 시점 | message 예시 | |------|-----------|-------------| | `SERVER_ERROR` | 서버에서 `SERVER_ERROR` 시그널 수신 | 서버가 보낸 메시지 또는 `"SERVER_ERROR"` | | `WORKER_DISCONNECTED` | 서버 연결 종료 시그널 수신 | `"WORKER_DISCONNECTED"` | ## 이벤트 등록 예제 ```js const SDK = window.KlleonSDK; SDK.onStatus((status) => console.log('상태:', status)); SDK.onSignal((data) => { if (data.signal === 'RESPONSE_TEXT') { console.log('아바타:', data.payload.text); } }); SDK.onError((error) => { console.error(`[${error.code}] ${error.message}`); }); SDK.init({ sdk_key: 'YOUR_SDK_KEY', avatar_id: 'YOUR_AVATAR_ID', }).catch((e) => console.error(e.message)); ``` ```jsx import { useEffect, useRef } from 'react'; function App() { const initialized = useRef(false); useEffect(() => { const SDK = window.KlleonSDK; SDK.onStatus((status) => console.log('상태:', status)); SDK.onSignal((data) => { if (data.signal === 'RESPONSE_TEXT') { console.log('아바타:', data.payload.text); } }); SDK.onError((error) => { console.error(`[${error.code}] ${error.message}`); }); SDK.init({ sdk_key: 'YOUR_SDK_KEY', avatar_id: 'YOUR_AVATAR_ID', }) .then(() => { initialized.current = true; }) .catch((e) => console.error(e.message)); return () => { if (initialized.current) SDK.destroy(); }; }, []); return ; } ``` --- # avatar-container 아바타 비디오를 표시하는 Web Component입니다. 비디오 트랙이 자동으로 바인딩됩니다. ## 사용법 ```html ``` ## 속성 | 속성 | 타입 | 기본값 | 설명 | |------|------|--------|------| | `volume` | `number` | `100` | 오디오 볼륨 (0-100). 변경 시 `SDK.setVolume()`을 자동 호출합니다. | | `fit` | `string` | `"cover"` | 비디오 맞춤 모드. `"cover"` \| `"contain"` \| `"fill"` | ### fit 옵션 | 값 | 설명 | |----|------| | `"cover"` | 컨테이너를 꽉 채우며 비율 유지 (넘치는 부분 잘림) | | `"contain"` | 비율 유지하며 컨테이너 안에 전체 표시 (여백 발생 가능) | | `"fill"` | 비율 무시하고 컨테이너에 맞춤 | ## 이벤트 | 이벤트 | 설명 | |--------|------| | `avatar-connected` | 컴포넌트가 DOM에 마운트되고 첫 렌더가 완료된 후 발생합니다. SDK가 이 이벤트를 수신하여 비디오 트랙을 자동으로 바인딩합니다. | ## 비디오 자동 바인딩 비디오 트랙은 다음 순서로 자동 연결됩니다: 1. ``가 DOM에 추가되면 첫 렌더 완료 후 `avatar-connected` 이벤트를 발생시킵니다. 2. SDK Core가 이 이벤트를 수신하여 Agora 비디오 트랙을 컴포넌트 내부의 `.avatar` 요소에 재생합니다. 3. 비디오 트랙이 먼저 수신된 경우에도, ``가 나중에 DOM에 추가되면 자동으로 바인딩됩니다. > **WARNING** 웰컴 메시지가 설정된 아바타의 경우, ``를 `init()` 호출 **전에** DOM에 배치하세요. 웰컴 메시지는 연결 즉시 재생되므로, ``가 DOM에 없으면 영상 없이 소리만 먼저 나올 수 있습니다. ## 스타일링 > **WARNING** Shadow DOM으로 캡슐화되어 있어, `style` 속성은 컴포넌트의 크기, 위치, 테두리 등 **외부 레이아웃**에만 적용됩니다. 내부 비디오 요소의 스타일은 변경할 수 없습니다. ```html ``` ## 볼륨 제어 ```js // 방법 1: 속성 변경 (내부적으로 SDK.setVolume()을 호출) document.querySelector('avatar-container').volume = 50; // 방법 2: SDK API 직접 호출 SDK.setVolume(50); ``` 두 방법 모두 동일한 결과를 냅니다. 속성 변경 시 컴포넌트가 `SDK.setVolume()`을 자동 호출합니다. --- # chat-container 채팅 인터페이스를 제공하는 Web Component입니다. 텍스트 입력과 음성 입력 모드를 지원합니다. ## 사용법 ```html ``` ## 속성 | 속성 | 타입 | 기본값 | 설명 | |------|------|--------|------| | `type` | `string` | `"text"` | 입력 모드. `"text"` \| `"voice"` | | `delay` | `number` | `30` | 응답 텍스트 타이핑 효과 딜레이 (글자당 ms). `0`이면 즉시 표시. | | `isShowCount` | `boolean` | `true` | 텍스트 모드에서 글자 수 카운터 표시 여부 | | `scrollbar` | `boolean` | `false` | 메시지 영역 스크롤바 표시 여부. `false`이면 스크롤바를 숨깁니다. | ## 입력 모드 ### 텍스트 모드 (`type="text"`) 텍스트 입력창과 전송 버튼을 표시합니다. - **전송**: Enter 키 또는 전송 아이콘 클릭 - **줄바꿈**: Shift + Enter - **IME 처리**: 한국어/일본어 등 조합 중에는 Enter로 전송되지 않습니다. - **글자 제한**: 최대 350자 - **모바일**: 모바일 환경에서는 Enter 키가 줄바꿈으로 동작합니다 (전송 버튼 사용). - **자동 포커스**: 아바타 응답 완료 후 텍스트 입력창에 자동 포커스됩니다. 우측의 마이크 아이콘을 클릭하면 음성 모드로 전환됩니다. ### 음성 모드 (`type="voice"`) 마이크 버튼을 표시합니다. - **녹음 시작**: 마이크 버튼 클릭 - **녹음 종료**: 정지 버튼 클릭 또는 **10초** 경과 시 자동 종료 - **시각 피드백**: 녹음 중 파형 애니메이션과 카운트다운 타이머 표시 - 내부적으로 `SDK.startListening()`, `SDK.endListening()`을 호출합니다. 좌측의 키보드 아이콘을 클릭하면 텍스트 모드로 전환됩니다. ## 메시지 표시 - SDK의 메시지 목록(`sendMessage`, `speak`, STT 결과, 아바타 응답)을 자동으로 표시합니다. - 가상 스크롤링을 사용하여 대량의 메시지도 성능 저하 없이 렌더링합니다. - 새 메시지 수신 시 자동으로 하단으로 스크롤됩니다. ### 타이핑 효과 - 아바타 응답(response) 메시지는 `delay` 속성에 지정된 간격으로 한 글자씩 표시됩니다. - 사용자 메시지(request)는 타이핑 효과 없이 즉시 표시됩니다. - 새 메시지가 도착하면 진행 중인 타이핑이 즉시 완료되고 새 메시지의 타이핑이 시작됩니다. ### 로딩 표시 아바타가 응답을 준비 중일 때(`RESPONSE_PREPARING` 시그널) 마지막 사용자 메시지 아래에 로딩 애니메이션이 표시됩니다. ## 비활성 상태 SDK 상태가 `CONNECTED_FINISH`가 아니거나, 아바타가 응답 중일 때 입력이 비활성화됩니다. 다음 시그널이 수신되면 입력이 다시 활성화됩니다: - `RESPONSE_ENDED` — 아바타 응답 완료 - `ERROR` — 에러 수신 - `REJECTED` — 요청 거절 - `SLEEP_STARTED` / `SLEEP_ENDED` — 수면 상태 변경 ## 스타일링 > **WARNING** Shadow DOM으로 캡슐화되어 있어, `style` 속성은 컴포넌트의 크기, 위치, 테두리 등 **외부 레이아웃**에만 적용됩니다. 내부 요소(배경색, 메시지 버블 등)의 스타일은 변경할 수 없습니다. ```html ``` --- # 에러 처리 SDK 에러는 3가지 경로로 전달됩니다: 1. **init 에러** — `init()`이 `Error`를 throw 2. **런타임 에러** — `onError` 콜백으로 전달 3. **처리 에러** — `onSignal`에서 `ERROR`/`REJECTED` 시그널로 전달 (세션 유지) ## 에러 코드 | 에러 코드 | 분류 | 설명 | |-----------|------|------| | `SOCKET_FAILED` | init | WebSocket 연결 실패 또는 SDK 키 검증 실패 | | `STREAMING_FAILED` | init | Agora 스트리밍 연결 실패 | | `SOCKET_DISCONNECTED_UNEXPECTEDLY` | 런타임 | WebSocket 비정상 종료 | | `STREAMING_DISCONNECTED_UNEXPECTEDLY` | 런타임 | 네트워크 끊김 등으로 Agora 연결 끊김 | | `SERVER_ERROR` | 런타임 | 서버에서 SERVER_ERROR 시그널 수신 | | `WORKER_DISCONNECTED` | 런타임 | 서버 연결 종료 시그널 수신 | ## init 에러 `init()`은 실패 시 `Error`를 throw합니다. SDK는 자동으로 cleanup 후 `IDLE` 상태로 돌아갑니다. ```js try { await SDK.init(options); } catch (error) { console.error(error.message); } ``` ```jsx SDK.init({ sdk_key: 'YOUR_SDK_KEY', avatar_id: 'YOUR_AVATAR_ID' }) .then(() => { initialized.current = true; }) .catch((error) => console.error(error.message)); ``` init 실패 시 `onError`는 호출되지 않습니다. `init()`이 `Error`를 throw하므로 `try/catch`로 처리하세요. | message | 원인 | |---------|------| | `"Failed to connect: ..."` | SDK 키 검증 또는 WebSocket 연결 실패 | | `"Failed to connect streaming: ..."` | 스트리밍 연결 실패 | ## 런타임 에러 init 이후 연결 중 발생하는 에러입니다. `onError` 콜백으로 전달됩니다. 런타임 에러 발생 시 SDK는 자동으로 연결을 정리하고 `IDLE` 상태로 복귀합니다. 클라이언트는 `onError` + `onStatus(IDLE)`를 수신한 후 `init()`으로 재연결할 수 있습니다. ### 에러별 상태 흐름 | 에러 코드 | 발생 시점 | 상태 전이 | |-----------|-----------|-----------| | (init throw) | init 중 | `CONNECTING` 또는 `SOCKET_CONNECTED` → `IDLE` | | `SOCKET_DISCONNECTED_UNEXPECTEDLY` | 런타임 | `CONNECTED_FINISH` → cleanup → `IDLE` | | `STREAMING_DISCONNECTED_UNEXPECTEDLY` | 런타임 | `CONNECTED_FINISH` → cleanup → `IDLE` | | `SERVER_ERROR` | 런타임 | `CONNECTED_FINISH` → cleanup → `IDLE` | | `WORKER_DISCONNECTED` | 런타임 | `CONNECTED_FINISH` → cleanup → `IDLE` | init 에러는 `init()`이 throw하고, 런타임 에러는 `onError` 콜백 + `onStatus(IDLE)`로 전달됩니다. ### 에러 처리 예시 ```js SDK.onError((error) => { console.error('SDK Error:', error.code, error.message); }); ``` | 에러 코드 | message | 원인 | |-----------|---------|------| | `SOCKET_DISCONNECTED_UNEXPECTEDLY` | `"WebSocket disconnected unexpectedly: {code}"` | WebSocket 비정상 종료 | | `STREAMING_DISCONNECTED_UNEXPECTEDLY` | `"Streaming disconnected: {reason}"` | 네트워크 끊김 등 Agora 연결 끊김 | | `SERVER_ERROR` | 서버가 보낸 메시지 또는 `"SERVER_ERROR"` | 서버에서 SERVER_ERROR 시그널 수신 | | `WORKER_DISCONNECTED` | 서버가 보낸 메시지 또는 `"WORKER_DISCONNECTED"` | 서버 연결 종료 시그널 수신 | ## 처리 에러 시그널 `ERROR`, `REJECTED` 시그널은 **세션을 종료하지 않습니다**. 개별 요청에 대한 실패를 나타냅니다. `onSignal`에서 수신합니다 (`onError`가 아님). ```js SDK.onSignal((data) => { if (data.signal === 'ERROR') { // 서버 처리 중 오류 console.warn('에러:', data.payload?.type); } if (data.signal === 'REJECTED') { // 입력이 비정상적 console.warn('거절:', data.payload?.type); } }); ``` | 시그널 | payload.type 예시 | 설명 | |--------|------------------|------| | `ERROR` | `STT_ERROR`, `LLM_ERROR`, `TTS_ERROR` | 서버 내부 처리 오류 | | `REJECTED` | `STT_EMPTY`, `MODERATION` | 입력 검증 실패 또는 정책 위반 | --- # TypeScript 지원 Klleon SDK는 UMD 번들로 제공되므로 TypeScript 프로젝트에서 사용하려면 타입 정의 파일을 추가해야 합니다. ## 타입 정의 파일 프로젝트 루트에 `KlleonSDK.d.ts` 파일을 생성합니다. ```typescript /** SDK 연결 상태 */ type SDKStatus = | "IDLE" | "CONNECTING" | "CONNECTING_FAILED" | "SOCKET_CONNECTED" | "SOCKET_FAILED" | "STREAMING_CONNECTED" | "STREAMING_FAILED" | "CONNECTED_FINISH"; type LogLevel = "debug" | "info" | "warn" | "error" | "silent"; type LanguageCode = "ko_kr" | "en_us" | "ja_jp" | "id_id"; /** init() 옵션 */ interface InitOption { sdk_key: string; avatar_id: string; log_level?: LogLevel; language?: LanguageCode; enable_microphone?: boolean; auto_send?: boolean; stt_only?: boolean; } /** onSignal 콜백 파라미터 */ interface IncomingSignalData { signal: string; payload?: Record; id: string; type: "SESSION" | "INPUT" | "OUTPUT"; } /** onError 콜백 파라미터 */ interface ErrorData { code: string; message?: string; } /** SDK 인터페이스 */ interface KlleonSDK { // 생명주기 init: (option: InitOption) => Promise; destroy: () => Promise; // 이벤트 onSignal: (cb: (data: IncomingSignalData) => void) => void; onStatus: (cb: (status: SDKStatus) => void) => void; onError: (cb: (error: ErrorData) => void) => void; // 텍스트 sendMessage: (text: string) => void; speak: (text: string) => void; // 오디오 에코 sendSpeakAudio: (audio: string) => void; endSpeakAudio: () => void; // 음성 startListening: () => void; endListening: () => void; cancelListening: () => void; // 아바타 제어 stopSpeaking: () => void; // 미디어 setVolume: (volume: number) => void; // 상태 조회 getStatus: () => SDKStatus; clearMessages: () => void; } declare global { interface Window { KlleonSDK: KlleonSDK; } // React에서 Web Component 사용 시 타입 에러 방지 namespace JSX { interface IntrinsicElements { "avatar-container": React.DetailedHTMLProps< React.HTMLAttributes & { volume?: number; fit?: "cover" | "contain" | "fill"; }, HTMLElement >; "chat-container": React.DetailedHTMLProps< React.HTMLAttributes & { type?: "text" | "voice"; delay?: number; isShowCount?: boolean; scrollbar?: boolean; }, HTMLElement >; } } } export {}; ``` ## tsconfig.json 설정 타입 정의 파일을 TypeScript가 인식하도록 `include`에 추가합니다. ```json { "compilerOptions": { "jsx": "react-jsx" }, "include": ["src", "KlleonSDK.d.ts"] } ``` ## 사용 예시 타입 정의를 추가하면 `window.KlleonSDK`에 대한 자동 완성과 타입 검사가 동작합니다. ```tsx const SDK = window.KlleonSDK; SDK.onStatus((status) => console.log(status)); SDK.onSignal((data) => console.log(data.signal, data.payload)); SDK.init({ sdk_key: 'YOUR_SDK_KEY', avatar_id: 'YOUR_AVATAR_ID', }).catch((e) => console.error(e.message)); ``` --- # v1에서 v2 마이그레이션 ## CDN 경로 변경 ```diff - + ``` ## 글로벌 변수 변경 ```diff - window.KlleonChat + window.KlleonSDK ``` ## API 변경사항 ### 호환 API (그대로 사용 가능) - ``, `` — 동일 ### init(option) 변경사항 `init()` 호출 방식은 동일하지만, 옵션이 변경되었습니다. | v1 옵션 | v2 옵션 | 비고 | |---------|---------|------| | `sdk_key` | `sdk_key` | 동일 | | `avatar_id` | `avatar_id` | 동일 | | `enable_microphone` | `enable_microphone` | 동일 | | `log_level` | `log_level` | `"silent"` 추가됨 | | `voice_code` | — | 제거됨 | | `subtitle_code` | — | 제거됨. `language`로 통합 | | — | `language` | 신규. TTS 음성 + 자막 언어 통합 (`"ko_kr"`, `"en_us"`, `"ja_jp"`, `"id_id"`) | | — | `auto_send` | 신규. `true` 시 서버 VAD 자동 응답 | | — | `stt_only` | 신규. `true` 시 STT 결과만 반환 | ### 제거된 API | v1 | 대체 방법 | |----|-----------| | `reconnect()` | `await destroy()` → `await init(options)` | | `changeAvatar(option)` | 제거됨 | | `wakeUpAvatar()` | 제거됨 | ### 변경된 API (이름 변경) | v1 | v2 | 비고 | |----|----|------| | `onChatEvent(cb)` | `onSignal(cb)` | 모든 서버 시그널 수신 | | `onStatusEvent(cb)` | `onStatus(cb)` | | | `onErrorEvent(cb)` | `onError(cb)` | | | `destroy()` (sync) | `await destroy()` | async 변경 | | `sendTextMessage(text)` | `sendMessage(text)` | | | `echo(text)` | `speak(text)` | | | `startAudioEcho(audio)` | `sendSpeakAudio(audio)` | | | `endAudioEcho()` | `endSpeakAudio()` | | | `startStt()` | `startListening()` | | | `endStt()` | `endListening()` | | | `cancelStt()` | `cancelListening()` | | | `stopSpeech()` | `stopSpeaking()` | | | `clearMessageList()` | `clearMessages()` | | ### 새로운 API | API | 설명 | |-----|------| | `setVolume(volume)` | 볼륨 제어 (0-100) | | `getStatus()` | 현재 SDK 상태 조회 | ### 코드 변경 예시 ```diff // 글로벌 변수 변경 - const SDK = window.KlleonChat; + const SDK = window.KlleonSDK; // destroy가 async로 변경 - SDK.destroy(); + await SDK.destroy(); // 이벤트 이름 변경 + init 전에 등록 - SDK.onStatusEvent(cb); + SDK.onStatus(cb); - SDK.onChatEvent(cb); + SDK.onSignal(cb); - SDK.onErrorEvent(cb); + SDK.onError(cb); await SDK.init(option); // 메서드 이름 변경 - SDK.sendTextMessage('안녕하세요'); + SDK.sendMessage('안녕하세요'); - SDK.echo('반복할 텍스트'); + SDK.speak('반복할 텍스트'); - SDK.startStt(); + SDK.startListening(); - SDK.endStt(); + SDK.endListening(); - SDK.stopSpeech(); + SDK.stopSpeaking(); ``` ## 주의사항 - v2는 ES2018+ 환경이 필요합니다 - IE11은 미지원입니다 - `destroy()`에 반드시 `await`를 사용해야 합니다 --- # Vanilla JS 예제 `index.html` 파일 하나로 SDK를 연동하는 전체 예제입니다. `{VERSION}`, `YOUR_SDK_KEY`, `YOUR_AVATAR_ID`를 실제 값으로 변경 후 브라우저에서 실행합니다. ```html Klleon SDK - Vanilla JS

Klleon SDK - Vanilla JS

``` --- # React 예제 ## 사전 준비 `index.html`의 ``에 SDK 스크립트를 추가합니다. ```html ``` ## 기본 예제 SDK Web Component를 사용하는 기본 React 예제입니다. > **TIP** SDK는 내부 Mutex로 `init()`/`destroy()` 호출을 직렬화합니다. React StrictMode에서 mount → unmount → remount가 발생하면 `init → destroy → init` 순서로 큐잉되어 3회 왕복이 실행됩니다. **개발 환경에서 초기 연결이 느릴 수 있으나**, 프로덕션에서는 StrictMode가 없으므로 1회만 실행됩니다. ```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 () => { SDK.destroy(); }; }, []); const handleSpeak = () => { if (speakText.trim()) { window.KlleonSDK.speak(speakText); setSpeakText(''); } }; return (

상태: {status}

setSpeakText(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && !e.nativeEvent.isComposing && handleSpeak()} placeholder="speak 내용 입력..." />
); } export default App; ``` > **TIP** TypeScript에서 Web Component 사용 시 타입 에러가 발생하면 [TypeScript 지원](/ko/guides/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([]); 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: data.payload?.text }]); setIsSpeaking(true); break; case 'STT_RESULT': setMessages((prev) => [...prev, { type: 'request', text: 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 () => { 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 (
{/* 아바타 영역 */} {/* 채팅 영역 (직접 구현) */}

상태: {status}

{messages.map((m, i) => (
{m.text}
))}
{/* 컨트롤 */}
setText(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && !e.nativeEvent.isComposing && send()} placeholder={isSpeaking ? '아바타 발화 중...' : '메시지 입력'} disabled={!isReady || isSpeaking} style={{ flex: 1, padding: 8 }} />
); } export default CustomChat; ``` ---