Skip to content

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;