우리 서비스에 맞는 예제 만들기
모바일 네이티브 앱 환경에 Webview 방식으로 eKYC를 연동해요.
유스비 담당자로부터 받은 메일의 첨부 파일에서 계정 정보를 확인할 수 있어요.
인증 진행 시, 계정 정보로 부여된 customer_id, id, key를 유스비 서버에 직접 전달하여 인증하는 방식이에요.
고객사에서 직접 만든 화면에서 엔드 유저의 필수 정보(이름, 생년월일, 휴대폰 번호, 이메일)를 받는 경우예요.
수집한 정보를 eKYC 서버에 직접 전달하는 기본 방식이에요.
기본 언어는 한국어로 제공되며, On 설정 시 영어로 제공돼요.
eKYC 기본 폰트 사용 시, Noto Sans KR로 제공돼요.
현재 선택한 플랫폼/언어에서 카메라 권한 등 필수 설정을 먼저 적용하세요.
JavaScript Packages
Code
"dependencies": {
...
"react-native-permissions": "^5.4.2",
"react-native-webview": "^13.15.0"
...
}
AndroidManifest.xml
Code
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
Podfile
Code
def node_require(script)
# Resolve script with node to allow for hoisting
require Pod::Executable.execute_command('node', ['-p',
"require.resolve(
'#{script}',
{paths: [process.argv[1]]},
)", __dir__]).strip
end
node_require('react-native/scripts/react_native_pods.rb')
node_require('react-native-permissions/scripts/setup.rb')
setup_permissions([
'Camera',
])
Info.plist
Code
<key>NSCameraUsageDescription</key>
<string>카메라를 사용하여 사진을 촬영합니다.</string>
React Native 개발 환경 설정 안내
- AndroidManifest.xml에
CAMERA권한을 반드시 선언해야 카메라 접근이 가능합니다. - iOS Info.plist에
NSCameraUsageDescription을 추가해야 합니다. - iOS에서는 HTTPS 환경에서만 카메라 접근이 가능합니다.
선택한 설정에 맞는 eKYC 연동 코드예요.
React Native - CLI
import React, { Component } from 'react';
import {
Alert,
StyleSheet,
Text,
PermissionsAndroid,
Platform,
SafeAreaView,
View,
} from 'react-native';
import {
PERMISSIONS,
RESULTS,
requestMultiple,
checkMultiple,
} from 'react-native-permissions';
import { WebView } from 'react-native-webview';
export default class SampleApp extends Component {
constructor(props) {
super(props);
this.appWebview = null;
/* 고객정보를 이미 전달했는지 기억해요. 웹 저장소가 아니라 앱이 직접 관리합니다.
WebView 를 재사용해 새 KYC 를 시작할 때는 이 값을 false 로 되돌리세요. */
this.kycParamSent = false;
this.domain = 'https://kyc.useb.co.kr/auth';
this.state = {
permissionsGranted: false,
deliveryFailed: false,
kycClosed: false,
indexPage: { uri: this.domain + '?ver=1' },
};
this.handleCameraPermission();
}
handleCameraPermission = async () => {
if (Platform.OS === 'android') {
// Calling the permission function
const result = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.CAMERA,
]);
if (result['android.permission.CAMERA'] === 'granted') {
this.setPermissionsGranted(true);
} else {
this.setPermissionsGranted(false);
}
} else if (Platform.OS === 'ios') {
const res = await checkMultiple([PERMISSIONS.IOS.CAMERA]);
if (res[PERMISSIONS.IOS.CAMERA] === RESULTS.GRANTED) {
this.setPermissionsGranted(true);
} else {
console.log('Need Permission');
const res2 = await requestMultiple([PERMISSIONS.IOS.CAMERA]);
console.log('Permission Status' + JSON.stringify(res2));
if (res2[PERMISSIONS.IOS.CAMERA] === RESULTS.GRANTED) {
this.setPermissionsGranted(true);
} else {
this.setPermissionsGranted(false);
}
}
}
};
setPermissionsGranted = value => {
console.log('setPermissionsGranted', value);
this.setState({ permissionsGranted: value });
};
doneProcessHandler = msgData => {
if (msgData?.result) {
const resultData = msgData.result;
switch (resultData) {
case 'success':
console.log('success - KYC 작업이 성공했습니다.');
break;
case 'failed':
console.log('failed - KYC 작업이 실패했습니다.');
break;
case 'complete':
console.log('complete - KYC가 완료되었습니다.');
this.closeKycView();
break;
case 'close':
console.log('close - KYC가 완료되지 않았습니다.');
this.closeKycView();
break;
case 'param_delivered':
// 전달 성공 알림이에요. KYC 결과가 아니므로 화면을 닫지 않아요.
console.log('param_delivered - WebView 에 고객정보를 전달했습니다.');
break;
case 'param_delivery_failed':
// 웹의 alcherakycreceive 준비 대기가 초과된 경우예요. 앱에서 실행한 JavaScript 가 보냅니다.
console.error('param_delivery_failed - WebView 에 고객정보를 전달하지 못했습니다. (alcherakycreceive 준비 대기 초과)');
this.handleParamDeliveryFailure();
break;
default:
console.log('알 수 없는 결과:', resultData);
break;
}
}
};
/**
* 화면에서 post를 던지면 react-native에서 받음(Throw a post on the screen, you will receive it from react-native)
*/
onReceivedWebViewMessage = event => {
const decodedMsg = decodeURIComponent(Base64.atob(event.nativeEvent.data));
let msgData;
try {
msgData = JSON.parse(decodedMsg) || {};
this.doneProcessHandler(msgData);
} catch (error) {
console.error(error);
return;
}
};
onWebViewLoadEnd = () => {
console.log('onWebViewLoadEnd');
// onLoadEnd 는 리다이렉트나 KYC 진행 중에도 다시 호출되므로 최초 1회만 전달해요.
if (this.kycParamSent) return;
this.kycParamSent = true;
const customer_id = '2'; // 2=ocr, 3=ocr_face, 4=ocr_face_liveness, 5=all, 6=account
let params = {
customer_id: "<고객사_id>", // number
id: "<client_id>", // string
key: "<client_secret>", // string
};
// Admin: [기타 설정] → [개인정보 옵션] → [사용자 필수 정보 수신 "사용 중"]
params.name = "홍길동";
params.birthday = "1984-11-23";
params.phone_number = "01012345678";
params.email = "test@test.com";
let encodedParams = Base64.btoa(encodeURIComponent(JSON.stringify(params)));
this.sendWebViewParams(encodedParams);
};
/* 고객정보 전달 실패를 사용자에게 알려요.
실제 앱에서는 [확인] 시 KYC 화면을 닫고 재시도 경로를 제공해주세요. */
handleParamDeliveryFailure = () => {
Alert.alert('고객정보 전달 실패', 'WebView 에 고객정보를 전달하지 못했습니다.', [
{ text: '확인', onPress: () => this.setState({ deliveryFailed: true }) },
]);
};
/* KYC 화면을 닫아요. 이 샘플은 WebView 가 유일한 화면이라 언마운트만 합니다.
실제 앱에서는 이 자리에 자사 화면 전환 코드(navigation.goBack 등)를 넣어주세요. */
closeKycView = () => {
this.setState({ kycClosed: true });
};
/**
* 웹의 alcherakycreceive() 함수를 호출하는 스크립트를 만들어요.
* 함수가 아직 준비되지 않았을 수 있어, 준비될 때까지 기다렸다가 준비되는 즉시 한 번 호출해 데이터 유실을 막아요.
* onLoadEnd 는 리다이렉트/재로드 시 여러 번 호출될 수 있어, kycParamSent 플래그로 화면당 1회만 전달해요.
* 최대 5초를 기다려도 함수가 준비되지 않으면 조용히 끝내지 않고,
* 콘솔 에러를 남기고 앱에 param_delivery_failed 를 알려요.
*/
buildKycInjection = param => `
(function () {
var p = '${param}';
var t0 = Date.now();
var limit = 5000;
var n = 0;
(function call() {
if (typeof window.alcherakycreceive === 'function') {
window.alcherakycreceive(p);
console.log('[KYC] alcherakycreceive 전달 (' + n + '회 대기 후)');
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(
btoa(encodeURIComponent(JSON.stringify({ result: 'param_delivered' })))
);
}
} else if (Date.now() - t0 < limit) {
n++;
setTimeout(call, 100);
} else {
console.error('[KYC] alcherakycreceive 미준비 - 고객정보 전달 실패');
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(
btoa(encodeURIComponent(JSON.stringify({ result: 'param_delivery_failed' })))
);
}
}
})();
})();
true;
`;
/**
* 앱에서 웹으로 고객 정보를 전달해요(Send customer info from app to web)
*/
sendWebViewParams = message => {
console.log('sendWebViewParams', message);
// console.log("sendWebViewParams (decoded)", decodeURIComponent(Base64.atob(message)));
this.appWebview?.injectJavaScript(this.buildKycInjection(message));
};
render() {
return (
<SafeAreaView style={styles.container}>
{this.state.permissionsGranted &&
!this.state.deliveryFailed &&
!this.state.kycClosed && (
<WebView
style={styles.webview}
source={this.state.indexPage}
originWhitelist={['*']}
ref={webview => (this.appWebview = webview)}
javaScriptEnabled={true}
useWebKit={true}
mediaPlaybackRequiresUserAction={false}
domStorageEnabled={true}
allowsInlineMediaPlayback={true}
startInLoadingState={true}
allowUniversalAccessFromFileURLs={true}
onMessage={this.onReceivedWebViewMessage}
onLoadEnd={this.onWebViewLoadEnd}
/>
)}
{!this.state.permissionsGranted && (
<Text>
카메라/갤러리 접근 권한이 없습니다. 권한 허용 후 이용해주세요. // no
access authority for camera and gallery. check allowance of
authortiy.
</Text>
)}
{this.state.kycClosed && (
<View style={styles.failure}>
{/* KYC 화면을 닫은 상태예요. 실제 앱에서는 자사 화면으로 전환하면 됩니다. */}
<Text style={styles.failureText}>KYC가 종료되었습니다.</Text>
</View>
)}
{this.state.deliveryFailed && (
<View style={styles.failure}>
<Text style={styles.failureText}>
WebView 에 고객정보를 전달하지 못했습니다.
</Text>
</View>
)}
</SafeAreaView>
);
}
}
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const Base64 = {
btoa: (input = '') => {
let str = input;
let output = '';
for (
let block = 0, charCode, i = 0, map = chars;
str.charAt(i | 0) || ((map = '='), i % 1);
output += map.charAt(63 & (block >> (8 - (i % 1) * 8)))
) {
charCode = str.charCodeAt((i += 3 / 4));
if (charCode > 0xff) {
throw new Error(
"'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.",
);
}
block = (block << 8) | charCode;
}
return output;
},
atob: (input = '') => {
let str = input.replace(/[=]+$/, '');
let output = '';
if (str.length % 4 == 1) {
throw new Error(
"'atob' failed: The string to be decoded is not correctly encoded.",
);
}
for (
let bc = 0, bs = 0, buffer, i = 0;
(buffer = str.charAt(i++));
~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
: 0
) {
buffer = chars.indexOf(buffer);
}
return output;
},
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ffffff',
},
webview: {
flex: 1,
},
failure: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#ffffff',
padding: 24,
},
failureText: {
color: '#111111',
fontSize: 16,
textAlign: 'center',
},
});
펼치기