모바일 앱 웹뷰 연동 가이드
Klever One SDK를 Flutter, React Native 앱의 WebView에서 사용하는 방법을 알아보세요.
플랫폼별 차이점
| 플랫폼 | 방식 | 통신 방법 | 파일 |
|---|---|---|---|
| Flutter | 파일 방식 | flutter_inappwebview.callHandler() | digital-human-webview-example.html |
| React Native | 임베디드 방식 | ReactNativeWebView.postMessage() | 컴포넌트 내 HTML |
웹뷰 아키텍처
모바일 앱
↓
WebView Container
↓
HTML + Klever SDK
↓
JavaScript Bridge ⟷ Native App Communication
빠른 시작
1. 기본 HTML 구조
digital-human-webview-example.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 - WebView</title>
<style>
body,
html {
margin: 0;
padding: 0;
width: 100dvw;
height: 100dvh;
overflow: hidden;
}
#streaming-container {
width: 100vw;
height: 100vh;
background: #000;
}
</style>
</head>
<body>
<div id="streaming-container"></div>
<script 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 src="./webview-bridge.js"></script>
</body>
</html>
2. SDK 초기화
webview-bridge.js
const { KleverOneClient } = KleverOneSdk;
let client = null;
// Flutter 통신 함수
function postToFlutter(event, payload) {
try {
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler(
"kleverOneEventHandler",
event,
payload,
);
} else {
console.log(`[Event To Flutter] ${event}:`, payload);
}
} catch (error) {
console.error(`Failed to post event: ${event}`, error);
}
}
// JavaScript Bridge 초기화
window.KleverSDKBridge = {
init: (config) => initializeSDK(config),
connect: () => connectToServer(),
disconnect: () => disconnectFromServer(),
// ... 기타 메서드들
};
핵심 기능 구현
SDK 초기화
function initializeSDK(config) {
if (client) {
postToFlutter("onInitResult", {
success: false,
message: "Already initialized",
});
return;
}
try {
const container = document.getElementById("streaming-container");
if (!container) throw new Error("Container not found");
client = new KleverOneClient({
apiKey: config.apiKey,
container: container,
callbacks: {
onReady: () => postToFlutter("onReady"),
onError: (error) => postToFlutter("onError", { error: error.message }),
onMessageReceived: (message) =>
postToFlutter("onMessageReceived", {
messageId: message.id,
content: message.content,
role: message.role,
}),
},
});
postToFlutter("onInitResult", {
success: true,
message: "SDK initialized",
});
} catch (error) {
postToFlutter("onInitResult", { success: false, message: error.message });
}
}
연결 관리
// 서버 연결
async function connectToServer() {
try {
if (!client) throw new Error("SDK not initialized");
await client.connect();
postToFlutter("onConnectResult", { success: true });
} catch (error) {
postToFlutter("onConnectResult", {
success: false,
message: error.message,
});
}
}
// 연결 해제
function disconnectFromServer() {
try {
if (!client) throw new Error("SDK not initialized");
client.disconnect();
postToFlutter("onDisconnectResult", { success: true });
} catch (error) {
postToFlutter("onDisconnectResult", {
success: false,
message: error.message,
});
}
}
// 재연결
async function reconnectToServer() {
try {
if (!client) throw new Error("SDK not initialized");
await client.reconnect();
postToFlutter("onReconnectResult", { success: true });
} catch (error) {
postToFlutter("onReconnectResult", {
success: false,
message: error.message,
});
}
}
메시지 전송
// 텍스트 메시지 전송
function sendTextMessage(message) {
try {
if (!client) throw new Error("SDK not initialized");
client.sendText(message);
postToFlutter("onSendTextResult", { success: true });
} catch (error) {
postToFlutter("onSendTextResult", {
success: false,
message: error.message,
});
}
}
// 말하기 (단일 텍스트 또는 배열)
function speakMessage(input) {
try {
if (!client) throw new Error("SDK not initialized");
client.speak(input); // 단일 텍스트 또는 배열 모두 지원
postToFlutter("onSpeakResult", { success: true });
} catch (error) {
postToFlutter("onSpeakResult", { success: false, message: error.message });
}
}
음성 녹음
// 녹음 시작
async function startRecording() {
try {
if (!client) throw new Error("SDK not initialized");
await client.startRecording();
postToFlutter("onStartRecordingResult", { success: true });
return { success: true };
} catch (error) {
postToFlutter("onStartRecordingResult", {
success: false,
message: error.message,
});
return { success: false, message: error.message };
}
}
// 녹음 중지
async function stopRecording() {
try {
if (!client) throw new Error("SDK not initialized");
await client.stopRecording();
postToFlutter("onStopRecordingResult", { success: true });
return { success: true };
} catch (error) {
postToFlutter("onStopRecordingResult", {
success: false,
message: error.message,
});
return { success: false, message: error.message };
}
}
상태 조회
// 메시지 목록 조회
function getMessages() {
try {
if (!client) throw new Error("SDK not initialized");
const messages = client.getMessages() || [];
postToFlutter("getMessagesResult", { success: true, messages });
} catch (error) {
postToFlutter("getMessagesResult", {
success: false,
message: error.message,
});
}
}
// 클라이언트 상태 조회
function getClientState() {
try {
if (!client) throw new Error("SDK not initialized");
const state = client.getState();
postToFlutter("getStateResult", { success: true, state });
} catch (error) {
postToFlutter("getStateResult", { success: false, message: error.message });
}
}
// 녹음 상태 확인
function checkRecordingStatus() {
try {
const isRecording = client?.isRecording() || false;
postToFlutter("isRecordingResult", { success: true, isRecording });
} catch (error) {
postToFlutter("isRecordingResult", {
success: false,
message: error.message,
});
}
}
// 준비 상태 확인
function checkReadyStatus() {
try {
const isReady = client?.isReady() || false;
postToFlutter("isReadyResult", { success: true, isReady });
} catch (error) {
postToFlutter("isReadyResult", { success: false, message: error.message });
}
}
Flutter 연동
Flutter InAppWebView 설정
// Flutter 코드
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
class KleverWebView extends StatefulWidget {
@override
_KleverWebViewState createState() => _KleverWebViewState();
}
class _KleverWebViewState extends State<KleverWebView> {
InAppWebViewController? webViewController;
@override
Widget build(BuildContext context) {
return InAppWebView(
initialFile: "assets/webview/digital-human-webview-example.html",
onWebViewCreated: (controller) {
webViewController = controller;
// JavaScript Handler 등록
controller.addJavaScriptHandler(
handlerName: "kleverOneEventHandler",
callback: (args) => handleWebViewEvent(args),
);
},
onLoadStop: (controller, url) async {
// SDK 초기화
await initializeKleverSDK();
},
);
}
// 이벤트 처리
void handleWebViewEvent(List<dynamic> args) {
final event = args[0] as String;
final payload = args.length > 1 ? args[1] : null;
switch (event) {
case "onReady":
print("SDK Ready");
break;
case "onMessageReceived":
handleNewMessage(payload);
break;
case "onError":
handleError(payload);
break;
}
}
// SDK 초기화 호출
Future<void> initializeKleverSDK() async {
await webViewController?.evaluateJavascript(source: '''
KleverSDKBridge.init({
apiKey: "your-api-key",
sendWelcomeMessage: true
});
''');
}
}
Flutter에서 JavaScript 함수 호출
class KleverController {
final InAppWebViewController webViewController;
KleverController(this.webViewController);
// 연결
Future<void> connect() async {
await webViewController.evaluateJavascript(
source: 'KleverSDKBridge.connect();'
);
}
// 메시지 전송
Future<void> sendMessage(String message) async {
await webViewController.evaluateJavascript(
source: 'KleverSDKBridge.sendText("${message.replaceAll('"', '\\"')}");'
);
}
// 말하기
Future<void> speak(String text) async {
await webViewController.evaluateJavascript(
source: 'KleverSDKBridge.speak("${text.replaceAll('"', '\\"')}");'
);
}
// 녹음 시작
Future<void> startRecording() async {
await webViewController.evaluateJavascript(
source: 'KleverSDKBridge.startRecording();'
);
}
// 녹음 중지
Future<void> stopRecording() async {
await webViewController.evaluateJavascript(
source: 'KleverSDKBridge.stopRecording();'
);
}
}
에러 처리 및 디버깅
안전한 콜백 래퍼
function safeCallback(callbackName, fn) {
return (...args) => {
try {
return fn(...args);
} catch (error) {
console.error(`Error in ${callbackName}:`, error);
postToFlutter("onError", {
error: {
message: `Callback error: ${error.message}`,
name: error.name || "CallbackError",
},
});
}
};
}
// 사용 예
const callbacks = {
onReady: safeCallback("onReady", () => postToFlutter("onReady")),
onError: safeCallback("onError", (error) =>
postToFlutter("onError", { error }),
),
};
연결 상태 모니터링
function monitorConnectionStatus() {
if (!client) return;
const state = client.getState();
const metrics = client.getMetrics();
postToFlutter("onStatusUpdate", {
connection: state.connection,
recording: state.recording,
isReady: client.isReady(),
metrics: {
messagesSent: metrics.messagesSent,
messagesReceived: metrics.messagesReceived,
errorCount: metrics.errorCount,
},
});
}
// 주기적 상태 확인
setInterval(monitorConnectionStatus, 5000);
실제 사용 예제
완전한 WebView 브리지 구현
// 완전한 KleverSDKBridge 구현
window.KleverSDKBridge = {
// 초기화
init: (config) => initializeSDK(config),
// 연결 관리
connect: () => connectToServer(),
disconnect: () => disconnectFromServer(),
reconnect: () => reconnectToServer(),
// 메시지 전송
sendText: (message) => sendTextMessage(message),
speak: (input) => speakMessage(input),
// 녹음
startRecording: () => startRecording(),
stopRecording: () => stopRecording(),
// 상태 조회
getMessages: () => getMessages(),
getState: () => getClientState(),
isRecording: () => checkRecordingStatus(),
isReady: () => checkReadyStatus(),
// 정리
destroy: () => destroySDK(),
};
function destroySDK() {
try {
if (client && client.disconnect) {
client.disconnect();
}
client = null;
postToFlutter("onDestroyResult", { success: true });
} catch (error) {
postToFlutter("onDestroyResult", {
success: false,
message: error.message,
});
}
}
// 웹뷰 준비 완료 알림
postToFlutter("onWebViewReady");
주요 고려사항
성능 최적화
- 이벤트 최소화: 불필요한 Flutter ↔ JavaScript 통신 줄이기
- 에러 재시도: 중요한 이벤트는 재시도 로직 구현
- 메모리 관리: 사용 완료 후 클라이언트 정리
보안 고려사항
- API 키 보호: Flutter에서 안전하게 전달
- HTTPS 사용: 프로덕션에서는 반드시 HTTPS
- 권한 관리: 마이크 권한 적절히 처리