React-Native와 React를 이용해서 웹뷰 어플을 개발하고 있었다. 웹 뷰 어플은 안드로이드의 물리적 뒤로가기 버튼을 누르더라도 웹뷰 웹 내의 페이지 뒤로가기가 작동하지 않기 때문에, 이와 관련된 작업을 하였었다. 또한, 메인 화면에서 뒤로가기 버튼을 누르면 토스트를 띄워주고, 2초 내로 한번 더 뒤로가기 버튼을 누르면 어플을 종료시키는 로직도 구현하였었다.
import React, { useEffect, useRef, useState } from 'react';
import { BackHandler, View, ToastAndroid } from 'react-native';
import WebView from 'react-native-webview';
import * as SplashScreen from 'expo-splash-screen';
export default function Native() {
const webViewRef = useRef<WebView>(null);
const [isCanGoBack, setIsCanGoBack] = useState(false);
const [lastBackPressed, setLastBackPressed] = useState(0);
SplashScreen.preventAutoHideAsync();
const onPressHardwareBackButton = () => {
const now = Date.now();
if (webViewRef.current && isCanGoBack) {
webViewRef.current.goBack();
return true;
} else if (now - lastBackPressed <= 2000) {
BackHandler.exitApp();
return true;
} else {
ToastAndroid.show('뒤로가기 버튼을 한번 더 누르면 종료됩니다.', ToastAndroid.SHORT);
setLastBackPressed(now);
return true;
}
};
useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', onPressHardwareBackButton);
return () => {
BackHandler.removeEventListener('hardwareBackPress', onPressHardwareBackButton);
};
}, [isCanGoBack, lastBackPressed]);
const handleLoad = () => {
setTimeout(() => {
SplashScreen.hideAsync();
}, 1000);
};
return (
<View style={{ flex: 1 }}>
<WebView
textZoom={100}
style={{ margin: 0, padding: 0 }}
ref={webViewRef}
javaScriptEnabled={true}
allowsBackForwardNavigationGestures={true}
source={{ uri: 'https://today-s-horoscope.vercel.app/' }}
onLoadEnd={handleLoad}
injectedJavaScript={`
(function() {
function wrap(fn) {
return function wrapper() {
var res = fn.apply(this, arguments);
window.ReactNativeWebView.postMessage('navigationStateChange');
return res;
}
}
history.pushState = wrap(history.pushState);
history.replaceInstance = wrap(history.replaceState);
window.addEventListener('popstate', function() {
window.ReactNativeWebView.postMessage('navigationStateChange');
});
})();
true;
`}
onMessage={({ nativeEvent: state }) => {
if (state.data === 'navigationStateChange') {
setIsCanGoBack(state.canGoBack);
}
}}
/>
</View>
);
}
아래의 블로그를 참고하여 구현하였다.
https://ricale.kr/blog/posts/210821-react-native-webview-android-back-button/
내부 웹 페이지의 뒤로 가기 UI와 Android의 하드웨어 뒤로 가기 버튼 간의 상태 불일치 문제가 발생했다. 사용자가 웹뷰 내부의 뒤로 가기 버튼을 통해 페이지를 변경한 후 Android의 뒤로 가기 버튼을 누르면, 예상과 달리 이전 페이지로 이동하지 않고 웹뷰 내의 최초 페이지로 이동해야 하는 상황에서도 이전 페이지로 돌아가 버리는 문제가 발생했다.
이 문제의 주요 원인은 Android 시스템의 뒤로 가기 버튼이 WebView의 네비게이션 상태 변화를 인식하지 못하여 WebView 내부에서 발생한 네비게이션 이벤트를 외부 Android 뒤로 가기 동작과 동기화하지 못했기 때문이다. 결과적으로, WebView의 canGoBack() 상태와 실제 웹 페이지의 위치가 일치하지 않는 상황이 발생한 것이다.
문제를 해결하기 위해 먼저 WebView 컴포넌트의 onNavigationStateChange 이벤트를 활용하여 현재 URL을 상태로 저장하고 관리하는 방법을 적용했다. 이를 통해 WebView에서 발생하는 모든 페이지 변경을 실시간으로 추적할 수 있게 되었다. 또한, WebView의 현재 URL 상태를 기반으로 하드웨어 뒤로 가기 버튼의 동작을 조건적으로 처리하여, 사용자가 최상위 페이지에 있을 때만 앱을 종료하거나 추가적인 토스트를 표시하도록 로직을 구현했다.
import React, { useEffect, useRef, useState } from 'react';
import { BackHandler, View, ToastAndroid } from 'react-native';
import WebView from 'react-native-webview';
import * as SplashScreen from 'expo-splash-screen';
type WebViewNavigation = {
url: string;
title: string;
loading: boolean;
canGoBack: boolean;
canGoForward: boolean;
};
export default function Native() {
const webViewRef = useRef<WebView>(null);
const [currentUrl, setCurrentUrl] = useState('');
const [lastBackPressed, setLastBackPressed] = useState(0);
SplashScreen.preventAutoHideAsync();
const onPressHardwareBackButton = () => {
const now = Date.now();
if (currentUrl === 'https://today-s-horoscope.vercel.app/') {
if (now - lastBackPressed <= 2000) {
BackHandler.exitApp();
} else {
ToastAndroid.show('뒤로가기 버튼을 한번 더 누르면 종료됩니다.', ToastAndroid.SHORT);
setLastBackPressed(now);
}
return true;
} else if (webViewRef.current) {
webViewRef.current.goBack();
return true;
}
return false;
};
useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', onPressHardwareBackButton);
return () => {
BackHandler.removeEventListener('hardwareBackPress', onPressHardwareBackButton);
};
}, [currentUrl, lastBackPressed]);
const handleNavigationStateChange = (navState: WebViewNavigation) => {
setCurrentUrl(navState.url);
};
const handleLoad = () => {
setTimeout(() => {
SplashScreen.hideAsync();
}, 1000);
};
return (
<View style={{ flex: 1 }}>
<WebView
style={{ flex: 1 }}
source={{ uri: 'https://today-s-horoscope.vercel.app/' }}
onNavigationStateChange={handleNavigationStateChange}
ref={webViewRef}
onLoadEnd={handleLoad}
/>
</View>
);
}
이러한 접근 방식을 통해 WebView를 사용하는 React Native 애플리케이션에서 더 나은 UX를 제공할 수 있게 되었다. URL 상태 관리를 통한 정확한 페이지 위치 추적과 조건적인 뒤로 가기 처리는 WebView를 사용하는 모든 모바일 애플리케이션 개발자에게 유용한 기법으로, 사용자의 기대에 부합하는 일관된 동작을 보장한다. 이 방법을 통해 개발자는 내부 웹뷰와 외부 앱 간의 상호작용을 효과적으로 관리할 수 있으며, 이는 최종적으로 앱의 전반적인 품질과 사용자 만족도를 향상시킬 것이라 기대한다.