[WebView] 카메라 호출 및 QR 스캐너 추가

2023. 12. 22. 10:26WebView

웹에서 네이티브 카메라 모듈을 사용하기 위해서 웹뷰와의 통신을 한다.

웹 ( 카메라 호출 요청 ) -> 웹뷰 카메라 호출 과정 자체는 단순하나 문제가 있다.

이미 떠있는 웹뷰에서 카메라를 호출할 경우 스캔 이후에 다시 웹뷰가 초기화 된다는 것

그래서 생각해낸 방법이 웹뷰는 그대로 로드하되 오버레이 형태로 카메라 모듈을 띄우는것

		<RNSafeAreaView style={{ flex: 1, backgroundColor: 'white' }}>
			<RNStatusBar backgroundColor="white" barStyle="dark-content" />
			{injectJavaScriptCode && fcmToken && (
				<>
					<WebView
						ref={webViewRef}
						source={{ uri: CIC_APP_SERVICE_FRONTEND }}
						javaScriptEnabled={true}
						geolocationEnabled={true}
						sharedCookiesEnabled={true}
						thirdPartyCookiesEnabled={true}
						domStorageEnabled={true}
						mixedContentMode="compatibility"
						originWhitelist={['*']}
						webviewDebuggingEnabled={true}
						onMessage={handleIncomingMessage}
						onShouldStartLoadWithRequest={handleWebRequest}
						injectedJavaScript={injectJavaScriptCode}
						injectedJavaScriptBeforeContentLoaded={
							injectFcmJavaScriptCode
						}
						decelerationRate={'normal'}
					/>
					{isScannerOpen && (
						<QRScanner
							onQRCodeScanned={onQRCodeScanned}
							onBackPress={onBackPress}
						/>
					)}
				</>
			)}
		</RNSafeAreaView>

위와 같이 카메라 호출을 받으면 플래그를 참조해서 카메라를 호출한다.

순서대로 보자면, 웹에서 네이티브쪽으로 카메라를 호출하는 로직은 아래와 같다.

	const [isLoadingCamera, setIsLoadingCamera] = useState(false)

	useEffect(() => {
		if (window.ReactNativeWebView) {
			function handleRequestCamera(): void {
				setIsLoadingCamera(!isLoadingCamera)
				const message = {
					action: 'OPEN_CAMERA_FOR_QR'
				}

				window.ReactNativeWebView.postMessage(JSON.stringify(message))
			}

			function handleMessage(event: MessageEvent): void {
				const data = event.data
				try {
					const parsedData = JSON.parse(data)
					if (parsedData) {
						router.push(parsedData)
					}
				} catch (error) {
					console.error('Error parsing message data', error)
				}
			}

			// @ts-ignore
			document.addEventListener('message', handleMessage)
			window.addEventListener('message', handleMessage)

			handleRequestCamera()

			return () => {
				// @ts-ignore
				document.removeEventListener('message', handleMessage)
				window.removeEventListener('message', handleMessage)
			}
		}
	}, [])

OPEN_CAMERA_FOR_QR 액션을 가진 메세지를 네이티브 쪽으로 보내고,

네이티브 웹뷰에서 onMessage={handleIncomingMessage} 메세지를 통해서 들어온 OPEN_CAMERA_FOR_QR 액션을 받으면 카메라 모듈을 띄울 플래그를 변경한다.

 카메라 모듈은 react-native-vision-camera을 사용했고, 관련 설정은 공식문서를 참조하면 된다.

QR 스캔 관련한 풀코드는 아래와 같다.

import React, { useState, useEffect, useMemo, useRef } from 'react';
import {
	View,
	Text,
	StyleSheet,
	BackHandler,
	Dimensions,
	Alert,
	Linking,
	Platform,
} from 'react-native';
import {
	Camera,
	useCameraDevice,
	CameraPermissionStatus,
	useCodeScanner,
} from 'react-native-vision-camera';
import { openSettings } from 'react-native-permissions';

const screen = Dimensions.get('window');
const qrSize = screen.width * 0.75;

type QRScannerProps = {
	onQRCodeScanned: (data: string) => void;
	onBackPress: () => void;
};

const QRScanner: React.FC<QRScannerProps> = ({
	onQRCodeScanned,
	onBackPress,
}) => {
	const [cameraPermission, setCameraPermission] =
		useState<CameraPermissionStatus>();

	const camera = useRef(null);
	const isProcessing = useRef(false);
	const device = useCameraDevice('back');

	useEffect(() => {
		const backHandler = BackHandler.addEventListener(
			'hardwareBackPress',
			() => {
				onBackPress();
				return true;
			},
		);

		return () => backHandler.remove();
	}, [onBackPress]);

	const openAppSettings = (): void => {
		if (Platform.OS === 'android') {
			openSettings();
		} else {
			// 시도
			Linking.openSettings();
			// 안될경우 시도
			// Linking.openURL('app-settings:');
		}
	};

	const showPermissionAlert = (): void => {
		Alert.alert(
			'권한 필요',
			'QR 스캔을 하기 위해서는 카메라 권한이 필요합니다. 설정에서 권한을 허용해주세요.',
			[
				{ text: '취소', style: 'cancel' },
				{ text: '설정으로 이동', onPress: openAppSettings },
			],
			{ cancelable: false },
		);
	};

	useEffect(() => {
		const checkPermissionAndRedirect = async (): Promise<void> => {
			const status = await Camera.getCameraPermissionStatus();
			console.log('카메라 권한 상태...', status);
			if (status !== 'granted') {
				showPermissionAlert();
			}
		};

		checkPermissionAndRedirect();
	}, []);

	const codeScanner = useCodeScanner({
		codeTypes: ['qr', 'ean-13'],
		onCodeScanned: useMemo(() => {
			return codes => {
				if (codes.length > 0 && !isProcessing.current) {
					isProcessing.current = true;

					const scannedValue = codes[0].value as string;
					onQRCodeScanned(scannedValue);

					isProcessing.current = false;
				}
			};
		}, [onQRCodeScanned]),
	});

	if (device == null) return <Text>loading...</Text>;

	return (
		<View style={styles.container}>
			<Camera
				ref={camera}
				codeScanner={codeScanner}
				style={StyleSheet.absoluteFill}
				device={device}
				isActive={true}
			/>
			<View style={styles.overlay}>
				<View style={styles.background} />
				<View
					style={[styles.qrFrame, { width: qrSize, height: qrSize }]}
				/>
				<Text style={styles.instructionText}>
					QR코드를 프레임 안에 위치 시켜주세요.
				</Text>
			</View>
		</View>
	);
};

const styles = StyleSheet.create({
	container: {
		position: 'absolute',
		top: 0,
		left: 0,
		right: 0,
		bottom: 0,
		zIndex: 10,
	},
	preview: {
		flex: 1,
	},
	overlay: {
		position: 'absolute',
		top: 0,
		bottom: 0,
		left: 0,
		right: 0,
		alignItems: 'center',
		justifyContent: 'center',
		backgroundColor: 'transparent',
	},
	background: {
		position: 'absolute',
		top: 0,
		left: 0,
		right: 0,
		bottom: 0,
		backgroundColor: 'rgba(0,0,0,0.5)',
	},
	qrFrame: {
		width: qrSize,
		height: qrSize,
		alignItems: 'center',
		justifyContent: 'center',
		borderColor: 'white',
		borderWidth: 2,
		borderRadius: 20,
		backgroundColor: 'transparent',
	},
	instructionText: {
		position: 'absolute',
		width: '100%',
		bottom: qrSize * 0.5,
		textAlign: 'center',
		color: 'white',
		fontSize: 16,
	},
});

export default QRScanner;

카메라 스타일을 커스텀하고, back 버튼에 대한 핸들러가 없을 경우 앱이 백그라운드 상태로 내려가니 핸들러 처리도 추가했다. 또, 카메라에 대한 접근 권한이 없을 경우 추가 권한을 받기 위한 알럿과, 권한이 설정 안되어 있을경우 앱의 설정 페이지로 이동하는 로직도 추가했다.

https://react-native-vision-camera.com/docs/guides