이 문서는 고객사 모바일 앱에 LiveForm을 WebView로 연동하는 방법을 설명합니다.
대상 독자: 기존 iOS 또는 Android 앱에 LiveForm을 연동하는 고객사 모바일 엔지니어.
구현 목표
고객사 앱은 다음 흐름을 구현합니다.
고객사 서버 또는 앱에서 전달받은 pid로 LiveForm URL을 생성합니다.
WebView를 열기 전에 LiveForm에 필요한 앱 권한을 확인합니다.
LiveForm URL을 전체 화면 WebView로 엽니다.
카메라 촬영, 이미지 업로드, 쿠키, 외부 링크 이동이 동작하도록 WebView를 설정합니다.
지원 및 최소 사양
LiveForm은 HTTPS 기반 Web Application이므로 앱의 WebView가 최신 브라우저 기능, 카메라 권한, 파일 선택, cookie/storage를 정상 지원해야 합니다.
구분 최소 사양 권장 사양 비고 iOS iOS 15 이상 iOS 16 이상 iOS 15 이상에서는 WKUIDelegate의 media capture permission callback으로 LiveForm host만 허용할 수 있습니다. Android Android 9, API 28 이상 Android 10, API 29 이상 파일 선택과 카메라 capture 동작 안정성을 위해 Android System WebView 또는 Chrome 최신 버전 사용을 권장합니다. Android WebView 업데이트 가능한 Android System WebView 또는 Chrome 최신 Android System WebView 또는 Chrome 파일 선택, 카메라 capture, cookie/storage 동작이 WebView/Chrome 버전에 영향을 받을 수 있습니다. React Native react-native-webview 13.x 이상최신 안정 버전 네이티브 앱은 동일 기능을 WKWebView/Android WebView로 직접 구현하면 됩니다.
실기기 검수에서 확인해야 하는 범위:
Android 9(API 28) 이상에서 앱 설치, WebView 로딩, 파일 선택, 카메라 capture, cookie/storage를 포함한 전체 흐름을 확인합니다.
iOS 15 이상에서 WKWebView 파일 선택, 카메라 capture, cookie/storage를 포함한 전체 흐름을 확인합니다.
최종 운영 판정은 실제 LiveForm URL에서 촬영, 업로드, 제출, returnUrl/deep link 이동까지 완료되는지 보고 내려야 합니다.
고객사 앱은 LiveForm 운영 host만 사용합니다. pid는 query parameter로 전달합니다.
https://form.argosidentity.com?pid={pid}
pid는 URL에 넣기 전에 반드시 URL encode 처리해야 합니다.
고객사 앱에서는 별도의 host나 form type path를 구분하지 않고, 전달받은 pid로만 LiveForm 세션을 구분합니다.
앱 권한
LiveForm은 WebView 내부에서 카메라와 이미지 업로드 기능을 사용합니다. 모바일 앱에서 WebView로 실행하는 경우, 브라우저가 처리하던 카메라와 파일 접근 권한을 고객사 앱이 먼저 확보해야 합니다.
WebView를 열기 전에 앱 레벨에서 카메라와 사진첩 또는 미디어 권한을 요청하고, 허용 상태를 확인하는 것은 필수입니다. 필수 권한이 허용되지 않은 상태에서는 LiveForm의 촬영 또는 이미지 업로드 흐름이 정상 동작하지 않을 수 있으므로 WebView를 열지 않아야 합니다.
Info.plist에 다음 usage description을 추가합니다.< key > NSCameraUsageDescription </ key >
< string > LiveForm 신분증 촬영을 위해 카메라 접근이 필요합니다. </ string >
< key > NSPhotoLibraryUsageDescription </ key >
< string > LiveForm 이미지 업로드를 위해 사진첩 접근이 필요합니다. </ string >
AndroidManifest.xml에 다음 권한을 추가합니다.< uses-permission android:name = "android.permission.INTERNET" />
< uses-permission android:name = "android.permission.CAMERA" />
이미지 업로드를 지원하는 경우, 고객사 앱의 target SDK와 파일 선택 방식에 맞는 Android 사진/미디어 권한도 함께 설정해야 합니다.
필수 연동 흐름
사용자 액션
└─ 고객사 앱에서 LiveForm 시작
└─ pid 기반 LiveForm URL 생성
WebView 진입 전
├─ 카메라 권한 확인
├─ 사진첩/미디어 권한 확인
└─ 전체 화면 WebView 열기
WebView 내부
├─ JavaScript와 DOM storage 허용
├─ Cookie 허용
├─ inline media와 media capture 허용
├─ http/https/data/about URL은 WebView에서 처리
└─ custom scheme은 외부 앱으로 전달
URL이 유효하지 않거나 필수 권한 확인이 끝나지 않았다면 WebView를 열지 않습니다.
WebView 설정
WebView는 JavaScript, storage, cookie, media playback, media capture를 허용해야 합니다.
React Native
Android 네이티브
iOS 네이티브
React Native 앱에서 react-native-webview를 사용하는 경우, Android WebChromeClient와 iOS WKUIDelegate의 세부 구현은 라이브러리 native layer가 처리합니다. 앱 코드에서는 아래 WebView props를 설정하고, 실제 기기에서 Android/iOS 검수 체크리스트를 각각 확인해야 합니다. import { Linking } from 'react-native' ;
import { WebView } from 'react-native-webview' ;
function openExternalUrl ( url : string ) {
Linking . canOpenURL ( url )
. then (( supported ) => {
if ( supported ) return Linking . openURL ( url );
return undefined ;
})
. catch (() => undefined );
}
export function LiveFormWebView ({ url } : { url : string }) {
return (
< WebView
source = { { uri: url } }
originWhitelist = { [ '*' ] }
javaScriptEnabled
domStorageEnabled
sharedCookiesEnabled
thirdPartyCookiesEnabled
allowsInlineMediaPlayback
mediaPlaybackRequiresUserAction = { false }
mediaCapturePermissionGrantType = "grantIfSameHostElsePrompt"
allowsFullscreenVideo
allowFileAccess
mixedContentMode = "compatibility"
setSupportMultipleWindows = { false }
onOpenWindow = { ({ nativeEvent }) => {
if ( nativeEvent . targetUrl ) openExternalUrl ( nativeEvent . targetUrl );
} }
onShouldStartLoadWithRequest = { ({ url : nextUrl }) => {
if (
nextUrl === 'about:blank' ||
nextUrl . startsWith ( 'about:' ) ||
nextUrl . startsWith ( 'data:' ) ||
nextUrl . startsWith ( 'http://' ) ||
nextUrl . startsWith ( 'https://' )
) {
return true ;
}
openExternalUrl ( nextUrl );
return false ;
} }
/>
);
}
주요 설정 설명 설정 이유 javaScriptEnabledLiveForm은 Web Application으로 실행됩니다. domStorageEnabledLiveForm이 브라우저 storage를 사용할 수 있게 합니다. sharedCookiesEnabled / thirdPartyCookiesEnabled일반 브라우저와 유사한 cookie 동작을 유지합니다. allowsInlineMediaPlayback지원되는 환경에서 media UI가 WebView 내부에서 동작하도록 합니다. mediaCapturePermissionGrantTypeLiveForm host에서 media capture 권한을 처리할 수 있게 합니다. setSupportMultipleWindows={false}새 창 흐름을 앱이 제어하는 WebView 경로 안에 둡니다. onShouldStartLoadWithRequest알 수 없는 custom scheme이 WebView를 깨지 않도록 외부 앱으로 전달합니다.
Android 네이티브 앱에서는 WebView.settings를 설정하고, WebChromeClient에서 WebView 내부 카메라 권한 요청과 파일 업로드 요청을 처리합니다. import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.webkit.CookieManager
import android.webkit.PermissionRequest
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebChromeClient.FileChooserParams
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager
class LiveFormActivity : AppCompatActivity () {
private lateinit var webView: WebView
private var filePathCallback: ValueCallback < Array < Uri >>? = null
private val fileChooserLauncher =
registerForActivityResult (ActivityResultContracts. StartActivityForResult ()) { result ->
val callback = filePathCallback
filePathCallback = null
if (callback == null ) return @registerForActivityResult
if (result.resultCode != Activity.RESULT_OK) {
callback. onReceiveValue ( null )
return @registerForActivityResult
}
val data = result. data
val uris = when {
data ?.clipData != null -> {
val clipData = data .clipData !!
Array (clipData.itemCount) { index -> clipData. getItemAt (index).uri }
}
data ?. data != null -> arrayOf ( data . data !! )
else -> null
}
callback. onReceiveValue (uris)
}
@SuppressLint ( "SetJavaScriptEnabled" )
override fun onCreate (savedInstanceState: Bundle ?) {
super . onCreate (savedInstanceState)
val liveFormUrl = intent. getStringExtra ( "LIVE_FORM_URL" ) ?: return finish ()
webView = WebView ( this )
setContentView (webView)
CookieManager. getInstance (). setAcceptCookie ( true )
CookieManager. getInstance (). setAcceptThirdPartyCookies (webView, true )
webView.settings. apply {
javaScriptEnabled = true
domStorageEnabled = true
mediaPlaybackRequiresUserGesture = false
mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
allowFileAccess = true
}
webView.webViewClient = object : WebViewClient () {
override fun shouldOverrideUrlLoading (
view: WebView ,
request: WebResourceRequest ,
): Boolean {
val uri = request.url
val scheme = uri.scheme
if (scheme == "http" || scheme == "https" || scheme == "about" || scheme == "data" ) {
return false
}
return try {
startActivity ( Intent (Intent.ACTION_VIEW, uri))
true
} catch (_: ActivityNotFoundException ) {
true
}
}
}
webView.webChromeClient = object : WebChromeClient () {
override fun onPermissionRequest (request: PermissionRequest ) {
val origin = request.origin.host ?: return request. deny ()
val allowedHost = origin == "form.argosidentity.com"
val cameraGranted = ContextCompat. checkSelfPermission (
this@LiveFormActivity ,
Manifest.permission.CAMERA,
) == PackageManager.PERMISSION_GRANTED
val resources = request.resources. filter {
it == PermissionRequest.RESOURCE_VIDEO_CAPTURE
}. toTypedArray ()
if (allowedHost && cameraGranted && resources. isNotEmpty ()) {
request. grant (resources)
} else {
request. deny ()
}
}
override fun onShowFileChooser (
webView: WebView ,
filePathCallback: ValueCallback < Array < Uri >>,
fileChooserParams: FileChooserParams ,
): Boolean {
this@LiveFormActivity .filePathCallback?. onReceiveValue ( null )
this@LiveFormActivity .filePathCallback = filePathCallback
return try {
fileChooserLauncher. launch (fileChooserParams. createIntent ())
true
} catch (_: ActivityNotFoundException ) {
this@LiveFormActivity .filePathCallback = null
filePathCallback. onReceiveValue ( null )
false
}
}
}
webView. loadUrl (liveFormUrl)
}
}
Android 필수 사항
CAMERA 권한은 WebView를 열기 전에 앱에서 먼저 요청하고 허용 상태를 확인합니다.
Android 13 이상에서 이미지 선택 권한이 필요한 구조라면 고객사 파일 선택 구현에 맞춰 READ_MEDIA_IMAGES 등을 처리합니다.
onPermissionRequest에서는 LiveForm host만 허용합니다. 모든 origin에 camera 권한을 grant하지 마세요.
onShowFileChooser를 구현해야 <input type="file"> 기반 업로드가 안정적으로 동작합니다.
iOS 네이티브 앱에서는 WKWebView를 사용하고, JavaScript와 inline media playback을 허용합니다. iOS 15 이상에서는 WKUIDelegate의 media capture permission callback에서 LiveForm host만 허용할 수 있습니다. import UIKit
import WebKit
import AVFoundation
import Photos
final class LiveFormViewController : UIViewController , WKNavigationDelegate , WKUIDelegate {
private var webView: WKWebView !
private let liveFormURL: URL
init ( liveFormURL : URL) {
self . liveFormURL = liveFormURL
super . init ( nibName : nil , bundle : nil )
}
required init? ( coder : NSCoder) {
fatalError ( "init(coder:) has not been implemented" )
}
override func viewDidLoad () {
super . viewDidLoad ()
let configuration = WKWebViewConfiguration ()
configuration. allowsInlineMediaPlayback = true
configuration. defaultWebpagePreferences . allowsContentJavaScript = true
configuration. websiteDataStore = . default ()
webView = WKWebView ( frame : . zero , configuration : configuration)
webView. navigationDelegate = self
webView. uiDelegate = self
webView. translatesAutoresizingMaskIntoConstraints = false
view. addSubview (webView)
NSLayoutConstraint. activate ([
webView. topAnchor . constraint ( equalTo : view. safeAreaLayoutGuide . topAnchor ),
webView. leadingAnchor . constraint ( equalTo : view. safeAreaLayoutGuide . leadingAnchor ),
webView. trailingAnchor . constraint ( equalTo : view. safeAreaLayoutGuide . trailingAnchor ),
webView. bottomAnchor . constraint ( equalTo : view. safeAreaLayoutGuide . bottomAnchor ),
])
webView. load ( URLRequest ( url : liveFormURL))
}
func webView (
_ webView : WKWebView,
decidePolicyFor navigationAction : WKNavigationAction,
decisionHandler : @escaping (WKNavigationActionPolicy) -> Void
) {
guard let url = navigationAction.request. url else {
decisionHandler (. cancel )
return
}
if let scheme = url.scheme, [ "http" , "https" , "about" , "data" ]. contains (scheme) {
decisionHandler (. allow )
return
}
UIApplication. shared . open (url)
decisionHandler (. cancel )
}
@available ( iOS 15.0 , * )
func webView (
_ webView : WKWebView,
requestMediaCapturePermissionFor origin : WKSecurityOrigin,
initiatedByFrame frame : WKFrameInfo,
type : WKMediaCaptureType,
decisionHandler : @escaping (WKPermissionDecision) -> Void
) {
let allowedHost = origin. host == "form.argosidentity.com"
decisionHandler (allowedHost ? . grant : . deny )
}
}
iOS 필수 사항
NSCameraUsageDescription과 NSPhotoLibraryUsageDescription을 반드시 설정합니다.
WebView를 열기 전에 AVCaptureDevice.requestAccess(for: .video) 등으로 카메라 권한을 먼저 요청하고 허용 상태를 확인합니다.
이미지 업로드 흐름을 사용하는 경우 Photo Library 접근 정책을 고객사 앱 UX에 맞게 확인합니다.
media capture permission은 LiveForm host만 허용합니다.
const LIVE_FORM_ORIGIN = 'https://form.argosidentity.com' ;
export function buildLiveFormUrl ( pid : string ) {
return ` ${ LIVE_FORM_ORIGIN } ?pid= ${ encodeURIComponent ( pid . trim ()) } ` ;
}
권한 처리 예시 (Expo/React Native)
import * as ImagePicker from 'expo-image-picker' ;
export async function requestLiveFormPermissions () {
const camera = await ImagePicker . requestCameraPermissionsAsync ();
const mediaLibrary = await ImagePicker . requestMediaLibraryPermissionsAsync ();
return {
granted: camera . granted && mediaLibrary . granted ,
camera ,
mediaLibrary ,
};
}
권한이 거부되면:
WebView를 열지 않습니다.
LiveForm 촬영과 업로드에 카메라/사진첩 권한이 필요하다는 메시지를 보여줍니다.
앱 설정 화면으로 이동할 수 있는 버튼을 제공합니다.
전체 화면 레이아웃
LiveForm은 가능한 많은 화면 영역을 사용하는 것이 좋습니다.
권장 사항:
WebView route에서는 native header를 숨깁니다.
notch와 home indicator를 피하기 위해 safe area를 적용합니다.
header를 숨긴 경우 작은 앱 레벨 뒤로가기 버튼을 제공합니다.
WebView를 card나 modal 안에 넣지 말고 화면을 직접 채우게 합니다.
┌──────────────────────────┐
│ back │
│ ┌──────────────────────┐ │
│ │ │ │
│ │ LiveForm │ │
│ │ WebView │ │
│ │ │ │
│ └──────────────────────┘ │
└──────────────────────────┘
검수 체크리스트
iOS와 Android에서 각각 확인합니다.
검수 방식 고객사 앱은 제공된 예시 구현을 참고해 자체 WebView 화면을 구현한 뒤, 실제 LiveForm URL과 권한/기능 테스트 페이지를 분리해서 검수해야 합니다.
LiveForm URL 모드: LiveForm 운영 호스트에서 실제 제출 흐름을 확인합니다.
권한/기능 테스트 페이지: WebView의 storage/cookie, fetch, 파일 입력, 카메라 capture 입력을 LiveForm 상태와 무관하게 한 번의 흐름으로 순차 확인합니다.
권한/기능 테스트 성공은 WebView 메커니즘이 동작한다는 뜻이지, 실제 LiveForm 제출 성공을 의미하지 않습니다. 최종 판정은 LiveForm URL 모드와 권한/기능 테스트 모드를 모두 보고 내려야 합니다.
공통 (URL·권한)
Android
iOS WKWebView
React Native
URL 및 이동
https://form.argosidentity.com?pid={pid} 형식의 LiveForm URL이 열린다.
pid가 URL encode 처리되어 전달된다.
외부 custom scheme은 외부 앱으로 열리거나 안전하게 실패한다.
권한
첫 실행 시 WebView 진입 전에 카메라 권한을 확인한다.
첫 실행 시 WebView 진입 전에 사진첩/미디어 권한을 확인한다.
권한 거부 시 복구 가능한 메시지를 보여준다.
설정 열기 버튼이 앱 설정 화면으로 이동한다.
WebView 동작
JavaScript가 활성화된 상태로 LiveForm이 로드된다.
신분증 촬영 시 카메라 UI가 열린다.
이미지 업로드 시 picker가 열린다.
Cookie와 storage가 일반 WebView navigation 동안 유지된다.
페이지 로딩 중 loading state가 표시된다.
WebView load error 또는 HTTP error가 발생했을 때 사용자에게 복구 가능한 안내를 보여준다.
AndroidManifest.xml에 android.permission.CAMERA가 선언되어 있다.
WebView를 열기 전에 Android runtime camera permission을 요청하고 허용 상태를 확인한다.
Native WebView 구현에서는 WebChromeClient.onPermissionRequest를 구현한다.
Native WebView 구현에서는 WebChromeClient.onShowFileChooser를 구현한다.
Android System WebView 또는 Chrome이 최신 버전인지 확인한다.
javaScriptEnabled가 활성화되어 있다.
domStorageEnabled가 활성화되어 있다.
Cookie와 third-party cookie가 허용되어 있다.
media playback 또는 camera access가 사용자 gesture 정책이나 WebView 설정으로 차단되지 않는다.
파일 선택 성공, 취소, 실패 모든 경로에서 ValueCallback이 정확히 한 번 호출된다.
파일 업로드 결과가 content:// URI로 전달되어도 정상 처리된다.
고객사 앱의 파일 선택 구현이 file:// URI를 직접 노출하지 않는다.
LiveForm의 파일 선택 UI에서 accept 속성에 맞는 파일 타입이 선택 가능하다.
LiveForm의 촬영 UI에서 capture 속성에 맞게 카메라 촬영 경로가 열린다.
HTTPS 정책 또는 mixed content 정책으로 LiveForm 리소스가 차단되지 않는다.
returnUrl 또는 deep link 이동이 외부 앱/브라우저/고객사 앱으로 정상 전달된다.
Info.plist에 NSCameraUsageDescription이 선언되어 있다.
Info.plist에 NSPhotoLibraryUsageDescription이 선언되어 있다.
WebView를 열기 전에 앱의 카메라 권한 요청과 허용 상태 확인이 정상 동작한다.
WebView를 열기 전에 앱의 사진첩 권한 요청과 허용 상태 확인이 정상 동작한다.
Native WKWebView 구현에서는 WKUIDelegate가 설정되어 있다.
iOS 15 이상에서는 WKUIDelegate의 media capture permission 처리를 확인한다.
WKWebView에서 input type="file"이 정상 동작한다.
파일 선택 또는 카메라 촬영 후 선택된 파일이 WebView로 정상 전달된다.
LiveForm의 파일 선택 UI에서 accept 속성에 맞는 파일 타입이 선택 가능하다.
LiveForm의 촬영 UI에서 capture 속성에 맞게 카메라 촬영 경로가 열린다.
HEIC 이미지 업로드가 서버 또는 WebView 흐름에서 정상 처리된다.
JavaScript 실행이 활성화되어 있다.
window.open 또는 새 창 요청이 차단되지 않고 앱 정책에 맞게 처리된다.
WKWebView의 cookie와 website data store 정책이 LiveForm 흐름을 차단하지 않는다.
HTTPS 정책 또는 mixed content 정책으로 LiveForm 리소스가 차단되지 않는다.
returnUrl, universal link, deep link 이동이 고객사 앱 정책에 맞게 정상 처리된다.
react-native-webview의 javaScriptEnabled가 활성화되어 있다.
react-native-webview의 domStorageEnabled가 활성화되어 있다.
sharedCookiesEnabled와 thirdPartyCookiesEnabled가 앱 정책에 맞게 설정되어 있다.
media capture 관련 WebView prop이 카메라 접근을 차단하지 않는다.
allowFileAccess와 파일 선택 흐름이 Android/iOS 모두에서 정상 동작한다.
Expo/RN permission API가 카메라와 사진첩/미디어 권한 상태를 정확히 반환한다.
onShouldStartLoadWithRequest, onOpenWindow, Linking.openURL 조합이 returnUrl/deep link를 안전하게 처리한다.
onError와 onHttpError가 사용자에게 복구 가능한 안내와 진단 리포트를 제공한다.
문제 해결
확인할 항목:
URL에 유효한 pid가 포함되어 있는지
기기가 네트워크에 연결되어 있는지
JavaScript와 DOM storage가 활성화되어 있는지
WebView callback에서 HTTP/network error가 발생했는지