-
React Native Expo에서 WebView와 Geolocation API 통신 문제 해결하기React Native 2024. 10. 11. 18:43
최근 프로젝트에서 Geolocation API를 사용하여
사용자의 위치 정보를 가져와 위치 기반 서비스를 제공하였습니다.
해당 서비스의 모바일 사용자가 증가함에 따라
React Native Expo와 WebView를 이용한 간단한 앱을 만들어 보았습니다.이 글에서는 React Native Expo를 사용하여
WebView와 Geolocation API를 통합하는 과정에서 발생한 문제와
이를 해결하기 위한 두 가지 방법을 소개하려 합니다.| 구현
해당 프로젝트에서 사용자가 위치 동의를 하지 않은 상태일 때,
웹과 앱에 따라 다음과 같은 각각 다른 경고창을 보여주도록 구현되어 있었습니다.이때 앱에서 사용자가 바로 설정창으로 이동할 수 있도록 하기 위해
“설정 가기” 버튼을 누르면 웹에서 postMessage를 사용하여
다음과 같이 앱에 메시지를 보낸 다음// 웹 코드 // 웹 코드에서 React Native WebView로 메시지 전송 window.ReactNativeWebView.postMessage(JSON.stringify({type: "OPEN_SETTING"}))
아래 코드처럼 앱의 WebView에서 onMessage를 통해 데이터를 확인한 후
React Native의 Linking을 통해 설정창으로 이동할 수 있도록 하였습니다.// 앱 코드 // React Native 앱에서 메시지 수신 및 처리 const handleMessage = (event: WebViewMessageEvent) => { const message = JSON.parse(event.nativeEvent.data); if (message.type === "OPEN_SETTING") { Linking.openSettings(); } }; <WebView onMessage={handleMessage} ...다른것들 />
이때 데이터는 문자열만 주고받을 수 있기 때문에
JSON.stringify를 사용해 JSON형태의 문자열로 바꿔서 보내주고,JSON.parse로 자바스크립트의 객체 형식으로 바꿔 사용해 줘야 합니다.
-> 참고
https://archive.reactnative.dev/docs/0.54/webview#onmessage
https://reactnative.dev/docs/linking
| 문제
이때 문제가 하나 있었습니다.
사용자가 설정창으로 이동하여 위치 권한을 허용한 후 앱으로 돌아오면,
권한 상태가 즉시 반영되지 않고 여전히 거부된 상태로 인식되어 경고창이 다시 나타났습니다.
이를 해결하기 위해 설정창으로 돌아올 때 웹뷰를 새로고침 하는 기능을 추가할 필요가 생겼습니다.| 방법 1: 앱 상태 변화를 감지해서 웹뷰 새로고침
먼저 설정창에서 돌아오면 앱을 새로고침 할 수 있도록 적용해 보기로 했습니다.
우선 앱의 상태 변화를 저장할 useState 상태를 만들어 주고,
다음과 같이 앱이 백그라운드에 있거나 활성화되지 않은 상태에서 돌아오면 새로고침 되도록 하였습니다.// 앱 코드 const [appState, setAppState] = useState(AppState.currentState); useEffect(() => { const handleChange = (nextAppState) => { if (appState.match(/inactive|background/) && nextAppState === "active") { // 앱이 다시 활성화되면 웹뷰 새로고침 if (webViewRef.current) { webViewRef.current.reload(); } } setAppState(nextAppState); }; const subscription = AppState.addEventListener("change", handleChange); return () => { subscription.remove(); }; }, [appState]);
여기서 앱의 상태인 AppState.currentState는 외부 값이기 때문에
useState훅으로 상태를 따로 만들어 줘야 자동으로 리렌더링 되며 값이 바뀌는 것을 확인할 수 있습니다.이제 앱의 설정으로 이동했다가 돌아올 경우에만 새로고침 되도록
fromSettings라는 false값을 가진 상태를 하나 만들어 주고
설정창으로 이동하는 버튼을 눌렀을 때 해당 값을 true로 바꾸면서,
앱이 활성화될 때 이 값을 확인한 다음 새로고침시켜주는 방식으로 바꿨습니다.// 앱 코드 const [appState, setAppState] = useState<AppStateStatus>( AppState.currentState ); const [fromSettings, setFromSettings] = useState(false); useEffect(() => { const handleChange = (nextAppState: AppStateStatus) => { if (appState.match(/inactive|background/) && nextAppState === "active") { if (fromSettings) { // 설정창에서 돌아온 경우 setFromSettings(false); if (webViewRef.current) { webViewRef.current.reload(); } } } setAppState(nextAppState); }; const subscription = AppState.addEventListener("change", handleChange); return () => { subscription.remove(); }; }, [appState, fromSettings]);
이런 식으로 하면서 어느 정도는 해결된 거 같았습니다.
하지만 새로고침이 무조건 돼 야하기 때문에 사용자 경험 측면에서 좋지 않을 거라고 판단하였습니다.
또한, 권한 관리를 좀 더 세밀하게 제어하기 어려웠고,
모바일 측면에서 좀 더 세밀한 위치 기반 서비스를 제공하기 위해 다른 방법이 필요했습니다.| 방법 2: 기능 구분
이에 저는 웹에서는 기존의 브라우저의 Geolocation API를 계속 사용하고,
모바일에서는 Expo의 Location 라이브러리를 사용해 보기로 결정하였습니다.사용 방법은 아래 링크에 나와있습니다.
https://docs.expo.dev/versions/latest/sdk/location/
우선 웹 코드에서 리액트 네이티브 앱으로 접속했을 때 여부를 확인하고,
위치 정보 요청이 들어오면 다른 로직이 실행되도록 하였습니다.이번에도 ReactNative WebView의 postMessage를 사용하여
웹뷰로 들어왔을 때 앱에 알리는 형식으로 구현했습니다.// 웹 코드 // 리액트 네이티브 웹뷰로 들어왔는지 확인 const isReactNativeWebView = typeof window != "undefined" && window.ReactNativeWebView != null; if(isReactNativeWebView) { // 리액트 네이티브 앱이면 postMessage로 전송 window.ReactNativeWebView.postMessage(JSON.stringify({type: "GPS_PERMISSIONS"})); return; } else { // 아니면 그대로 if (navigator.geolocation) { const watchId = navigator.geolocation.watchPosition( ...코드 ); return () => { navigator.geolocation.clearWatch(watchId); } } }
이후 앱 코드에서 이를 웹뷰의 onMessage로 확인하고
expo의 Location을 통해 사용자의 위치 정보를 받도록 구현하였습니다.// 앱 코드 const handleMessage = async (event: WebViewMessageEvent) => { const message = JSON.parse(event.nativeEvent.data); if (message.type === "GPS_PERMISSIONS") { ...여기다 위치 정보 받는 코드 } }; <WebView onMessage={handleMessage} ...다른것들 />
그다음 기존에 웹에서 위치 동의가 안 돼있으면 띄웠던 경고창을
앱에서 위치동의 여부를 확인하고, 앱에서 경고창을 띄우도록 하였습니다.먼저 기기에 위치 서비스 허용 여부를 확인하는 함수를 만들어 주고,
허용 안 해놨으면 설정으로 이동할 수 있도록 React Native의 Alert를 사용하여 경고창을 만들어 줍니다.Location의 hasServicesEnabledAsync를 사용하면 알 수 있습니다.
// 앱 코드 const checkLocation = async () => { // 위치 서비스 허용 여부 확인 const isEnabled = await Location.hasServicesEnabledAsync(); if (!isEnabled) { Alert.alert( "위치 서비스 사용", '위치 서비스를 사용할 수 없습니다. "기기의 설정 > 개인 정보 보호" 에서 위치서비스를 켜주세요.', [ { text: "취소", style: "cancel" }, { text: "설정으로 이동", onPress: () => { Linking.openSettings(); }, }, ], { cancelable: false } ); return false; } return true; };
그다음 앱의 위치 정보 접근 권한을 요청하는 함수를 만들어 주고,
마찬가지로 동의가 거부된 상태면 설정으로 이동할 수 있도록 합니다.Location의 requestForegroundPermissionsAsync를 사용해 요청할 수 있습니다.
// 앱 코드 const requestPermissions = async () => { // 앱의 위치 접근 권한 요청 const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== "granted") { Alert.alert( "위치 정보 접근 거부", "위치 권한이 필요합니다.", [ { text: "취소", style: "cancel" }, { text: "설정으로 이동", onPress: () => { Linking.openSettings(); }, }, ], { cancelable: false } ); return false; } return true; };
마지막으로 Location을 사용하여 사용자의 위치를 가져오는 함수를 만들어 주고,
위치 정보를 받은 후 webview의 postMessage로 웹에 위치 정보를 보내줍니다.
저는 사용자의 위치를 실시간으로 1m 간격으로 확인하기 위해 다음과 같은 설정으로 만들었습니다.Location의 watchPositionAsync으로 사용자의 위치를 추적할 수 있습니다.
// 앱 코드 const getLocation = async () => { try { setGpsLoading(true); let location = await Location.watchPositionAsync( { distanceInterval: 1, }, (location) => { const { latitude, longitude } = location.coords; webViewRef.current?.postMessage( JSON.stringify({ latitude, longitude }) ); } ); } catch (error) { console.error(error); } finally { setGpsLoading(false); } };
이제 해당 함수들을 아까 위에서 만든 handleMessage에서 실행시킵니다.
// 앱 코드 const handleMessage = async (event: WebViewMessageEvent) => { const message = event.nativeEvent.data; if (message.type === "GPS_PERMISSIONS") { // 기기 위치 서비스 허용 여부 확인 const servicesEnabled = await checkLocation(); if (!servicesEnabled) return; // 앱 위치 정보 접근 권한 요청, 확인 const permissionsGranted = await requestPermissions(); if (!permissionsGranted) return; // 사용자 위치 정보 수집 및 웹으로 전송 getLocation(); } };
-> 참고
https://reactnative.dev/docs/alert
이제 웹 코드에서 이를 확인하고 원하는 로직을 만들어 주면 됩니다.
앱에서 보낸 메시지를 웹에서 확인하기 위해
최상위 객체에 message 이벤트를 달아주면 됩니다.message를 통해 가져온 데이터를 JSON.parse를 통해 다시 자바스크립트 객체로 만들어 주고,
이를 확인하고 코드를 작성해 줍니다.여기서 주의할 점은 IOS와 Android의 최상위 객체가 다르기 때문에 각각 달아줘야 합니다.
useEffect(() => { const handleMessage = (event: any) => { const data = JSON.parse(event.data); if (data.latitude && data.longitude) { ...여기에 코드 } }; // IOS window.addEventListener("message", handleMessage); // Android document.addEventListener("message", handleMessage); return () => { window.removeEventListener("message", handleMessage); document.removeEventListener("message", handleMessage); }; }, []);
이런 식으로 앱에서는 expo의 Location 라이브러리를 통해
사용자의 위치 정보를 가져오도록 만들었습니다.expo의 Location에서는 위치 업데이트 빈도 세밀하게 설정, 배터리 절약, 백그라운드 동작 등
다양한 기능을 제공하기 때문에 더 풍부한 기능을 가진 앱을 만들 수 있을 거 같습니다.