모바일 구현 가이드
이 가이드는 CDK 위젯을 모바일 애플리케이션에 통합하는 모범 사례를 다룹니다. 웹에서는 위젯이 일반적으로 iframe에 임베드되지만, 모바일 앱은 위젯 URL을 효과적으로 표시하기 위해 다른 접근 방식이 필요합니다.
개요
CDK 위젯은 연령 게이트, VPC, 데이터 고지, 권한 관리 등을 포함한 완전한 규정 준수 흐름을 포함하는 API 호출에서 반환되는 URL입니다. 이러한 위젯을 모바일 애플리케이션에 통합할 때 표시할 수 있는 여러 옵션이 있으며, 각각 다른 기능과 트레이드오프가 있습니다. 주요 고려 사항은 다음과 같습니다:
- AgeKeys 지원: 사용자가 향후 확인을 위해 AgeKeys(FIDO 기반 패스키)를 생성하고 사용할 수 있는지 여부
- 결과 통신: 위젯 결과가 앱으로 전달되는 방식
- 사용자 경험: 통합 수준과 네이티브 느낌
모바일 구현 방법
Android 옵션
Android는 위젯 URL을 표시하는 세 가지 주요 방법을 제공합니다:
-
Chrome Custom Tabs ⭐ 권장 - 앱의 브랜딩을 유지하는 맞춤형 Chrome 브라우저 탭에서 위젯 URL을 엽니다. 이를 통해 사용자를 앱의 컨텍스트 내에 유지하면서 전체 브라우저 기능을 제공합니다. Custom Tabs는 Chrome과 쿠키 및 인증 상태를 공유하여 원활한 경험을 가능하게 합니다.
-
WebView - Android의 네이티브 WebView 컴포넌트를 사용하여 앱 내에 웹 콘텐츠를 직접 임베드합니다. 구현이 간단하지만 WebView는 최신 웹 표준에 대한 지원이 제한적이며 특정 브라우저 기능에 액세스할 수 없습니다.
WebView는 AgeKeys에 필요한 WebAuthn을 지원하지 않습니다. WebView를 사용하여 위젯을 임베드하는 경우 사용자는 AgeKeys를 생성하거나 사용할 수 없습니다. 전체 AgeKeys 지원을 갖춘 최상의 사용자 경험을 위해서는 대신 Chrome Custom Tabs를 사용하세요.
- Trusted Web Activity (TWA) - 주로 Progressive Web Apps용으로 설계된 전체 화면 모드로 웹 콘텐츠를 표시합니다. TWA는 앱과 k-ID 도메인 간에 디지털 자산 링크를 설정해야 합니다. 디지털 자산 링크가 감지되지 않으면 TWA는 자동으로 Chrome Custom Tabs로 폴백합니다.
Trusted Web Activities는 위젯 통합에 지원되지 않습니다. k-ID의 도메인에서 디지털 자산 링크를 구성해야 하지만 이는 사용할 수 없습니다. 이 경우 TWA가 Chrome Custom Tabs로 폴백하므로 더 간단한 구현을 위해 Chrome Custom Tabs를 직접 사용하는 것이 좋습니다.
iOS 옵션
iOS는 위젯 URL을 표시하는 세 가지 주요 방법을 제공합니다:
-
ASWebAuthenticationSession ⭐ 권장 - 보안 인증 흐름을 위해 특별히 설계된 이 방법은 시스템 관리 브라우저 보기에서 웹 콘텐츠를 표시합니다. Safari와 쿠키를 공유하고 WebAuthn과 같은 최신 웹 기능에 대한 액세스를 제공하므로 확인 흐름에 이상적입니다.
-
SFSafariViewController - Safari와 쿠키 및 인증 상태를 공유하는 Safari와 유사한 인터페이스에서 웹 콘텐츠를 표시합니다. 이를 통해 앱 컨텍스트를 유지하면서 친숙한 브라우징 경험을 제공합니다.
-
WKWebView - 앱 내에 웹 콘텐츠를 임베드하는 Apple의 최신 웹 보기 컴포넌트입니다. Android의 WebView와 유사하게 WKWebView는 특정 웹 표준에 제한이 있으며 모든 브라우저 기능에 액세스할 수 없습니다.
WKWebView는 AgeKeys에 필요한 WebAuthn을 지원하지 않습니다. WKWebView를 사용하여 위젯을 임베드하는 경우 사용자는 AgeKeys를 생성하거나 사용할 수 없습니다. 전체 AgeKeys 지원을 갖춘 최상의 사용자 경험을 위해서는 대신 ASWebAuthenticationSession을 사용하세요.
AgeKeys 지원 제한
AgeKeys는 FIDO 및 WebAuthn 표준을 기반으로 하는 재사용 가능한 익명 연령 증명 자격 증명입니다. 사용자는 한 번 연령을 확인하고 개인 정보를 공개하지 않고 다른 서비스 간에 해당 확인을 재사용할 수 있습니다.
AgeKeys는 WebAuthn 지원이 필요하며, 이는 Android WebView 또는 iOS WKWebView에서 사용할 수 없습니다. 이러한 컴포넌트를 사용하여 위젯을 임베드하는 경우 사용자는 확인 중에 AgeKeys를 옵션으로 볼 수 없으며 확인 성공 후 AgeKeys를 생성할 수 없습니다.
사용자에게 AgeKeys를 활성화하려면 다음 방법 중 하나를 사용해야 합니다:
- Android: Chrome Custom Tabs
- iOS: ASWebAuthenticationSession 또는 SFSafariViewController
위젯 결과 수신
모바일 앱은 사용자가 위젯 흐름을 완료한 후 결과를 수신해야 합니다. 두 가지 접근 방식이 있으며 각각 다른 가용성이 있습니다:
콜백 URL(범용 방법)
모든 구현 방법에 권장되는 접근 방식은 콜백 URL을 사용하는 것입니다. 위젯 URL을 생성하기 위해 API를 호출할 때 redirectUrl 매개변수를 포함하세요. 위젯 흐름이 완료되면 결과를 쿼리 매개변수로 포함하여 이 URL로 리디렉션합니다.
콜백 URL의 장점:
- 모든 구현 방법에서 작동
- DOM 메시지보다 더 안정적
- 모바일 앱의 표준 딥 링크 패턴
- 앱이 백그라운드로 이동해도 결과가 항상 전달됨
콜백 URL 작동 방식
- 앱에 딥 링크 핸들러를 등록합니다(예:
myapp://vpc-complete) - API를 호출할 때 딥 링크를
redirectUrl로 포함합니다 - 위젯이 완료 후 딥 링크로 리디렉션합니다
- 앱이 딥 링크를 처리하고 결과를 추출합니다
리디렉션은 위젯 URL이 브라우저 또는 웹 보기에서 직접 열릴 때만 발생하며 iframe에 임베드된 경우에는 발생하지 않습니다.
콜백 URL 매개변수
위젯이 콜백 URL로 리디렉션할 때 위젯 유형과 관련된 쿼리 매개변수가 포함됩니다. 예를 들어:
- Age Gate 위젯에는
verificationId와result가 포함될 수 있습니다 - 세션 관련 위젯에는
sessionId와 상태 정보가 포함될 수 있습니다
콜백 URL 예시:
myapp://vpc-complete?sessionId=608616da-4fd2-4742-82bf-ec1d4ffd8187&result=PASS
콜백 URL 구현
CDK 위젯 API를 호출할 때 요청에 redirectUrl을 포함하세요. URL은 다음 중 하나일 수 있습니다:
- HTTPS URL:
https://example.com/vpc-complete - 사용자 지정 딥 링크:
myapp://vpc-complete
모든 CDK 위젯 엔드포인트가 아직 redirectUrl을 지원하는 것은 아닙니다. 가용성에 대해서는 특정 API 엔드포인트 문서를 확인하세요. redirectUrl을 사용할 수 없는 경우 WebView 또는 WKWebView에서 DOM 메시지를 사용하세요.
DOM 메시지(WebView/WKWebView만)
Android WebView 또는 iOS WKWebView를 사용하는 경우 위젯에서 전송된 JavaScript 메시지를 수신 대기할 수 있습니다. 이를 통해 다음이 가능합니다:
- 위젯 결과를 실시간으로 수신
- 웹 보기를 닫는 시점 제어
- 위젯 이벤트를 기반으로 앱의 UI 업데이트
CDK 위젯은 위젯 유형에 따라 다른 DOM 이벤트를 발생시킵니다:
- End-to-End 위젯:
Widget.AgeGate.Challenge,Widget.AgeGate.Result - Age Gate 위젯:
Widget.AgeGate.Challenge,Widget.AgeGate.Result - Data Notices 위젯:
Widget.DataNotices.ConsentApproved - Session Permissions 위젯:
Widget.ExitReview
DOM 메시지는 네이티브 코드에서 가로챌 수 있는 postMessage 이벤트로 전송됩니다. 사용 가능한 이벤트에 대한 자세한 내용은 DOM 이벤트 개요를 참조하세요.
DOM 메시지는 WebView와 WKWebView에서만 작동합니다. Chrome Custom Tabs, Trusted Web Activity, ASWebAuthenticationSession 또는 SFSafariViewController에서는 사용할 수 없습니다.
방법 비교
| 방법 | 플랫폼 | AgeKeys | DOM 메시지 | 콜백 URL | 최적 용도 |
|---|---|---|---|---|---|
| WebView | Android | ❌ | ✅ | ✅ | 권장하지 않음(AgeKeys 지원 없음) |
| Chrome Custom Tabs | Android | ✅ | ❌ | ✅ | 대부분의 사용 사례(권장) |
| Trusted Web Activity | Android | ✅ | ❌ | ✅ | 권장하지 않음(디지털 자산 링크 필요) |
| WKWebView | iOS | ❌ | ✅ | ✅ | 권장하지 않음(AgeKeys 지원 없음) |
| ASWebAuthenticationSession | iOS | ✅ | ❌ | ✅ | 대부분의 사용 사례(권장) |
| SFSafariViewController | iOS | ✅ | ❌ | ✅ | Safari와 유사한 경험 |
권장 구현
Android: Chrome Custom Tabs
기능과 사용자 경험의 최적 균형을 위해 콜백 URL과 함께 Chrome Custom Tabs를 사용하세요.
Chrome Custom Tabs를 선택하는 이유:
- WebAuthn을 통한 전체 AgeKeys 지원
- 모든 최신 웹 기능에 대한 액세스
- 앱 브랜딩을 통한 원활한 사용자 경험
- 안정적인 콜백 메커니즘
- Chrome과 인증 상태 공유
구현 단계:
- 콜백 URL에 대한 딥 링크 핸들러를 등록합니다
- API 요청에
redirectUrl을 포함합니다(엔드포인트가 지원하는 경우) - Chrome Custom Tabs를 사용하여 위젯 URL을 엽니다
- 위젯 결과로 딥 링크 콜백을 처리합니다
iOS: ASWebAuthenticationSession
안전하고 네이티브한 느낌의 규정 준수 흐름을 위해 콜백 URL과 함께 ASWebAuthenticationSession을 사용하세요.
ASWebAuthenticationSession을 선택하는 이유:
- WebAuthn을 통한 전체 AgeKeys 지원
- 모든 최신 웹 기능에 대한 액세스
- 시스템 관리 보안 UI
- Safari와 쿠키 공유
- 안정적인 콜백 메커니즘
구현 단계:
- 콜백 URL에 대한 URL 스킴 핸들러를 등록합니다
- API 요청에
redirectUrl을 포함합니다(엔드포인트가 지원하는 경우) - ASWebAuthenticationSession을 사용하여 위젯 URL을 표시합니다
- 위젯 결과로 URL 스킴 콜백을 처리합니다
전체 구현 예제
권장 접근 방식을 구현하는 단계별 예제와 전체 코드 샘플은 다음과 같습니다:
1단계: 딥 링크 핸들러 등록
- iOS (Swift)
- Android (Kotlin)
Info.plist에 URL 스킴을 등록합니다:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
AndroidManifest.xml에 인텐트 필터를 추가합니다:
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="vpc-complete" />
</intent-filter>
</activity>
2단계: 콜백이 있는 위젯 URL 생성
- iOS (Swift)
- Android (Kotlin)
import Foundation
func generateWidgetUrl(completion: @escaping (URL?) -> Void) {
guard let url = URL(string: "https://game-api.k-id.com/api/v1/widget/generate-e2e-url") else {
completion(nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer YOUR_API_KEY", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestBody: [String: Any] = [
"jurisdiction": "US-CA",
"options": [
"redirectUrl": "myapp://vpc-complete"
]
]
guard let httpBody = try? JSONSerialization.data(withJSONObject: requestBody) else {
completion(nil)
return
}
request.httpBody = httpBody
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(nil)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let widgetUrlString = json["url"] as? String,
let widgetUrl = URL(string: widgetUrlString) else {
completion(nil)
return
}
completion(widgetUrl)
}.resume()
}
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException
fun generateWidgetUrl(callback: (String?) -> Unit) {
val client = OkHttpClient()
val mediaType = "application/json".toMediaType()
val requestBody = JSONObject().apply {
put("jurisdiction", "US-CA")
put("options", JSONObject().apply {
put("redirectUrl", "myapp://vpc-complete")
})
}.toString().toRequestBody(mediaType)
val request = Request.Builder()
.url("https://game-api.k-id.com/api/v1/widget/generate-e2e-url")
.post(requestBody)
.addHeader("Authorization", "Bearer YOUR_API_KEY")
.addHeader("Content-Type", "application/json")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
callback(null)
return
}
response.body?.use { body ->
val responseBody = body.string()
try {
val jsonResponse = JSONObject(responseBody)
val widgetUrl = jsonResponse.optString("url", null)
callback(widgetUrl)
} catch (e: Exception) {
callback(null)
}
} ?: callback(null)
}
override fun onFailure(call: Call, e: IOException) {
callback(null)
}
})
}
3단계: 위젯 표시
- iOS (Swift)
- Android (Kotlin)
import AuthenticationServices
// 세션을 속성으로 저장하여 할당 해제 방지
var authSession: ASWebAuthenticationSession?
func displayWidget(widgetUrl: URL) {
authSession = ASWebAuthenticationSession(
url: widgetUrl,
callbackURLScheme: "myapp"
) { callbackURL, error in
if let error = error {
// 오류 처리(사용자 취소 등)
return
}
if let callbackURL = callbackURL {
handleWidgetCallback(callbackURL)
}
}
authSession?.presentationContextProvider = self
authSession?.start()
}
import android.app.Activity
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
fun displayWidget(activity: Activity, widgetUrl: String?) {
if (widgetUrl == null) {
// 오류 처리: 위젯 URL 생성 실패
return
}
val builder = CustomTabsIntent.Builder()
val customTabsIntent = builder.build()
val uri = Uri.parse(widgetUrl)
customTabsIntent.launchUrl(activity, uri)
}
4단계: 콜백 처리
- iOS (Swift)
- Android (Kotlin)
func handleWidgetCallback(_ callbackURL: URL) {
guard let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else {
return
}
let sessionId = queryItems.first(where: { $0.name == "sessionId" })?.value
let verificationId = queryItems.first(where: { $0.name == "verificationId" })?.value
let result = queryItems.first(where: { $0.name == "result" })?.value
// 위젯 결과에 따라 UI 업데이트
if result == "PASS" {
// 성공한 흐름 처리
} else if result == "FAIL" {
// 실패한 흐름 처리
}
// 선택 사항: CDK API 엔드포인트를 사용하여 서버 측에서 확인
if let sessionId = sessionId {
verifyResultServerSide(sessionId: sessionId)
}
}
import android.content.Intent
import android.net.Uri
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent) // 중요: 새 인텐트가 사용되도록 보장
val data: Uri? = intent.data
if (data != null && data.scheme == "myapp" && data.host == "vpc-complete") {
val sessionId = data.getQueryParameter("sessionId")
val verificationId = data.getQueryParameter("verificationId")
val result = data.getQueryParameter("result")
// 위젯 결과에 따라 UI 업데이트
if (result == "PASS") {
// 성공한 흐름 처리
} else {
// 실패한 흐름 처리
}
// 선택 사항: CDK API 엔드포인트를 사용하여 서버 측에서 확인
if (sessionId != null) {
verifyResultServerSide(sessionId)
}
}
}
CDK 위젯 유형
CDK는 특정 규정 준수 워크플로우를 위해 설계된 여러 위젯 유형을 제공합니다:
- End-to-End 위젯: 단일 인터페이스에서 완전한 VPC 흐름을 처리하며 연령 게이트, 확인, 데이터 고지, 권한 및 기본 설정을 포함합니다
- Age Gate 위젯: 사용자 연령을 수집하고 필요할 때 VPC 챌린지를 트리거합니다
- Data Notices 위젯: 관할권에 적합한 데이터 고지를 표시하고 사용자 동의를 수집합니다
- Session Permissions 위젯: 사용자와 부모가 활성 세션 권한을 보고 업데이트할 수 있도록 합니다
각 위젯 유형 및 특정 사용 사례에 대한 자세한 내용은 CDK 문서의 가이드를 참조하세요.