# 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 (
);
}
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;
```
---