앱 내 채팅(WebView)
WebView로 전체 채팅 위젯을 모바일·데스크톱 앱 어디에나 넣을 수 있습니다. 모든 기능을 그대로 쓰고, 업데이트도 자동입니다.
네이티브 앱에 고객 지원을 넣는 가장 빠른 방법은 저희가 호스팅하는 전체 화면 위젯 페이지를 WebView로 불러오는 것입니다. 홈, 메시지, 도움말, 검색, 이모지, 첨부 파일, 타임스탬프, 읽음 표시, 대화 종료까지 웹사이트와 똑같은 위젯을 그대로 쓸 수 있고, 자동으로 업데이트됩니다. 한 번 연동해 두면 위젯이 바뀌어도 앱을 다시 출시할 필요가 없습니다.
빠른 시작
WebView에서 이 URL을 여세요. 로그인한 사용자라면 반드시 externalId와 email(가능하면 name도)을 넣어야 합니다. 그렇지 않으면 상담원에게는 늘 익명 방문자로만 보입니다. 모든 값은 URL 인코딩하세요.
https://<your-domain>/api/widget-embed?appId=YOUR_APP_ID&externalId=USER_ID&email=USER_EMAIL&name=USER_NAME사용자가 로그인하지 않았을 때만 익명 형식을 씁니다.
https://<your-domain>/api/widget-embed?appId=YOUR_APP_ID⚠️ 상담원 화면에 이메일 없이 “익명 방문자”로 표시되나요? 가장 흔한 연동 실수입니다. WebView URL에 email/externalId가 빠져 있는 것입니다. 서버가 이 값을 만들어 낼 수는 없으므로 앱에서 넘겨야 합니다. 방문자 ID가 anon-… 형태라면 아무것도 전달되지 않은 것입니다.
JavaScript / 모든 WebView(Flutter 제외)
Electron, 데스크톱 셸, React Native, 또는 URL을 직접 지정하는 네이티브 WKWebView라면 JS로 URL을 만드세요. URLSearchParams가 모든 값을 알아서 인코딩합니다.
// Build the support URL for the signed-in user before opening the WebView.
function buildSupportUrl(domain, user) {
const params = new URLSearchParams({
appId: 'YOUR_APP_ID',
platform: 'windows', // Strongly recommended: decides which help articles this visitor sees
// ios / macos_appstore / macos / android / windows / web
// locale: userSelectedLanguage, // Optional: only if your app has its own language switch; also add forceLocale: '1'
// Omitted = follow the device language (covers the App Store's 50 localizations)
// — Always pass these for signed-in users, or agents only see an "anonymous visitor" —
externalId: user.id, // The user's unique ID in your system
email: user.email, // The user's email (important)
name: user.name || '', // The user's name (optional)
// hmac: user.hmac, // Optional: computed on your server to prevent impersonation (see HMAC below)
// attrs: JSON.stringify({ plan: user.plan, expiresAt: user.expireAt }), // Optional: custom attributes
});
return `https://${domain}/api/widget-embed?${params.toString()}`;
}
// Usage: load the returned url in your WebView (instead of the old ?appId=...-only URL).
const url = buildSupportUrl('askais.com', currentUser);
myWebView.loadURL(url); // Electron: win.loadURL(url); native: load this URLFlutter(webview_flutter)
pubspec.yaml에 webview_flutter: ^4.x를 추가한 뒤 다음과 같이 작성합니다.
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class SupportPage extends StatefulWidget {
const SupportPage({super.key});
@override
State<SupportPage> createState() => _SupportPageState();
}
class _SupportPageState extends State<SupportPage> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
// For signed-in users always include externalId + email, or agents see an "anonymous visitor" (no name / email).
// Pass only appId + locale when the user is not signed in. Uri.https URL-encodes the values.
final uri = Uri.https('askais.com', '/api/widget-embed', {
'appId': 'YOUR_APP_ID',
'platform': Platform.isIOS ? 'ios' : 'android', // Strongly recommended: decides which help articles are visible
// No locale = follow the device language (covers the App Store's 50 localizations).
// Only pass it if your app has its own language switch, together with 'forceLocale': '1':
// 'locale': appSettings.selectedLanguage,
'externalId': user.id, // The user's unique ID in your system (important)
'email': user.email, // The user's email (important)
'name': user.name, // Optional
// 'hmac': hmacFromYourServer, // Optional: computed on your server to prevent impersonation
});
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
..loadRequest(uri);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Support')),
// Full-screen: drop the appBar and use your own close button.
body: SafeArea(child: WebViewWidget(controller: _controller)),
);
}
}
// Open it from wherever your "Support" button lives:
// Navigator.push(context,
// MaterialPageRoute(builder: (_) => const SupportPage()));다른 스택의 iOS/Android WebView(Swift WKWebView, Kotlin WebView, React Native react-native-webview)도 방식은 같습니다. URL을 불러오고 JavaScript를 켜면 됩니다.
네이티브 iOS 및 Android
설치할 SDK는 없습니다. 같은 URL을 만들어 WebView에 넘기기만 하면 됩니다. 단, Mac은 주의하세요. App Store 빌드와 DMG 빌드는 같은 운영체제에서 실행되므로, 이 값은 런타임 검사가 아니라 빌드 플래그로 정해야 합니다.
// ── iOS (Swift / WKWebView) ────────────────────────────────
// On Mac Catalyst / macOS, set platform to macos_appstore or macos.
#if targetEnvironment(macCatalyst)
let platform = "macos_appstore" // App Store build; a DMG build from your website passes "macos"
#else
let platform = "ios" // iPhone and iPad are both ios
#endif
var comps = URLComponents(string: "https://\(domain)/api/widget-embed")!
comps.queryItems = [
.init(name: "appId", value: "YOUR_APP_ID"),
.init(name: "platform", value: platform),
.init(name: "externalId", value: user.id), // Required for signed-in users
.init(name: "email", value: user.email), // Required for signed-in users
.init(name: "name", value: user.name),
// Only pass these two if your app has its own language switch:
// .init(name: "locale", value: settings.language),
// .init(name: "forceLocale", value: "1"),
]
webView.load(URLRequest(url: comps.url!)) // URLComponents URL-encodes the values
// ── Android (Kotlin / WebView) ─────────────────────────────
val url = Uri.parse("https://$domain/api/widget-embed")
.buildUpon()
.appendQueryParameter("appId", "YOUR_APP_ID")
.appendQueryParameter("platform", "android")
.appendQueryParameter("externalId", user.id)
.appendQueryParameter("email", user.email)
.appendQueryParameter("name", user.name)
// .appendQueryParameter("locale", settings.language)
// .appendQueryParameter("forceLocale", "1")
.build()
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true // The widget uses localStorage to remember the visitor
webView.loadUrl(url.toString())URL 매개변수
appId(필수): 받은 메시지함의 App ID.locale: UI 언어. 거의 모든 경우 생략하세요. 생략하면 위젯은 WebView가 알려 주는 기기 언어를 따르며, 사용자도 이를 기대합니다. UI는 App Store Connect의 50개 현지화 언어로 모두 번역되어 있고, 그 밖의 언어는 영어로 표시됩니다. 앱에 자체 언어 전환 기능이 있을 때만 사용자가 고른 언어를 넘기고, 기기 설정보다 우선하도록forceLocale=1도 함께 보내세요. 값을 하드코딩해서는 안 됩니다.zh-Hans를 하드코딩하면 일본어 앱에 중국어 위젯이 뜨는 일이 생깁니다.platform(강력 권장): 방문자가 사용하는 빌드. 어떤 도움말 문서를 보여 줄지, AI가 App Store 규칙에 맞춰 답할지를 결정합니다. 생략하면 WebView의 User-Agent로 플랫폼을 추측합니다. iPhone, Android, Windows는 알아볼 수 있지만 Mac App Store 빌드와 DMG 빌드는 구분하지 못하고, 데스크톱 모드의 iPad도 알아보지 못합니다. 값:ios(iPhone 및 iPad),macos_appstore(Mac App Store 빌드),macos(DMG 직접 배포 빌드),android,windows,web.externalId: 앱에서 사용자를 고유하게 식별하는 ID. 넘기면 상담원이 사용자를 알아보고 이전 대화 기록을 합칠 수 있습니다.email,name,avatarUrl: 상담원에게 보이는 프로필.hmac: 신원 서명(아래 참고). 선택 사항.
플랫폼별 콘텐츠 숨기기(App Store 규정 준수)
App Store 가이드라인 3.1.1은 iOS 앱에서 외부 구매, 구독, 추천 보상 관련 콘텐츠를 보여 주는 것을 허용하지 않습니다. 그래서 모든 도움말 문서와 자주 묻는 질문 항목에는 플랫폼별 표시 설정이 있습니다. 대시보드에서 도움말 센터나 자주 묻는 질문 페이지를 열고 해당 항목의 플랫폼 칩을 켜거나 끄세요. 선택한 플랫폼의 방문자는 목록에서 그 항목을 볼 수 없고, 직접 링크로도 열 수 없습니다.
AI는 도움말 문서나 자주 묻는 질문으로 답하지 않고, “Training”에 추가한 내용으로 답합니다. 그곳의 파일과 Q&A에도 같은 플랫폼 칩이 있으며, 특정 플랫폼에서 숨긴 항목은 그 플랫폼 방문자에게 하는 답변에 쓰이지 않습니다. AI도 언급하지 않게 하려면 양쪽 모두에서 숨기세요.
확실하게 효과가 있으려면 두 가지가 필요합니다. 첫째, 앱이 직접 platform을 보내야 합니다. User-Agent로 추측하는 것은 대비책일 뿐입니다. 둘째, 그 값은 운영체제만이 아니라 빌드를 반영해야 합니다.
- iPad는 따로 구분하지 않습니다. 같은 iOS 앱이 같은 규칙으로 실행되므로
ios하나로 iPhone과 iPad를 함께 처리합니다. - Mac에는 빌드가 두 가지입니다. Mac App Store 빌드는 3.1.1 규정을 따라야 하지만, 자체 사이트에서 배포하는 DMG는 그렇지 않습니다. 둘은 같은 운영체제이므로 런타임에는 구분할 방법이 없습니다. 빌드 시점에(예: Xcode 플래그로) 정해서
macos_appstore또는macos를 보내세요. 잘못 보내면 스토어 빌드가 심사 중에 구독 관련 콘텐츠를 노출하거나, DMG 빌드가 괜히 도움말 센터의 절반을 숨기게 됩니다.
변경 사항은 바로 적용됩니다. 설정은 앱이 아니라 대시보드에 있으므로, 표시 범위를 바꾸려고 새 버전을 출시할 필요가 없습니다.
신원 확인(HMAC, 선택)
externalId가 실제로 로그인한 사용자임을 증명하고 사칭을 막으려면 HMAC 서명을 넘기세요. 서명은 자체 서버에서 계산해야 하며, 비밀 키를 앱에 넣어서는 절대 안 됩니다.
hmac = HMAC-SHA256(secretKey, externalId), 소문자 16진수로 출력합니다.externalId그 자체에만 서명하고 반드시 함께 넘기세요. 위젯은 항상externalId를 보내므로(넘기지 않으면 익명 ID를 자동 생성)email만 서명한 값으로는 아무도 인증되지 않고 방문자는 미인증 상태로 남습니다.secretKey는 받은 메시지함의 시크릿 키입니다(받은 메시지함마다 하나).sk_접두사를 포함한 문자열 전체를 사용합니다. 받은 메시지함을 만들 때와 키를 교체할 때(해당 행의 ⋮ 메뉴 → 시크릿 키 교체) 한 번만 표시됩니다.hmac이 틀리거나 없어도 채팅은 막히지 않으며, 방문자는 미인증으로 처리될 뿐입니다.
import { createHmac } from 'node:crypto';
// SECRET_KEY lives only on your server, never ship it inside the app.
const hmac = createHmac('sha256', SECRET_KEY)
.update(externalId) // exactly the externalId you pass to the widget
.digest('hex');
// Return { externalId, email, hmac } to the app, which appends them to the URL.플랫폼별 참고 사항
- Android:
INTERNET권한을 유지하고, 뒤로 가기 버튼을 가로채 먼저 WebView 안에서 뒤로 이동하도록 하세요. 최신webview_flutter는 첨부 파일용<input type="file">를 지원합니다. - iOS: HTTPS를 쓰므로 ATS 예외가 필요 없습니다. 이미지 업로드는 위젯에 기본으로 들어 있는 기능입니다.
WKWebView가 네이티브 사진 선택기를 알아서 열기 때문에 따로 만들 것이 없습니다. 사진 보관함에서 고르는 데는 권한이 필요 없습니다. “사진 찍기” 카메라 옵션을 쓰려는 경우에만Info.plist에NSCameraUsageDescription을 추가하세요. 추가하지 않으면 사용자가 그 옵션을 누를 때 앱이 강제 종료됩니다.
네이티브 UI가 필요하신가요?
WebView 대신 직접 만든 네이티브 채팅 화면이 필요하다면 모바일 SDK 페이지의 로직 전용 클라이언트를 사용하세요. 대부분의 앱에는 위의 WebView 방식이 더 빨리 출시할 수 있고, 언제나 모든 기능을 제공합니다.