React Native WebView 안드로이드 백버튼 처리
React Native 를 웹뷰 컨테이너로 사용해 웹앱을 구현하고 있다.
웹뷰를 사용한 하이브리드앱 구현 시 주의해야 할 점 중 하나는 안드로이드 기기의 백버튼(뒤로가기 버튼)에 대한 처리다. 사람들은 이전 웹페이지로 돌아가길 기대하며 백버튼을 누르지만, React Native 는 웹뷰 내에서의 이전 페이지로 이동하는 것이 아니라 React Native 상의 이전 화면으로 돌아가기 때문이다.
만약 웹뷰 화면이 해당 앱의 첫 화면이라면, 백버튼을 눌렀을 때 이전 웹페이지로 가는 것이 아니라 앱이 종료되는 것을 경험할 수 있다. 당연하게도 그것은 개발자가 의도한 사항도, 사용자가 의도한 사항도 아니다.
그러면 웹뷰가 있는 화면에서 백버튼을 눌렀을 때 React Native 의 이전 화면이 아닌 웹뷰의 이전 웹페이지가로 가도록 코드를 수정해보자.
1. onNavigationStateChange
React Native WebView 를 사용하고 있다면 onNavigationStateChange
콜백과 BackHandler
API를 사용해 이전 웹페이지로 이동하도록 백버튼의 기능을 조정할 수 있다.
const HomeScreen = (props) => {
const ref = useRef();
const [navState, setNavState] = useState();
useEffect(() => {
const onPress = () => {
if (navState.canGoBack) {
// 뒤로 갈 수 있는 상태라면 이전 웹페이지로 이동한다
ref.current.goBack();
// 기본 뒤로가기 동작을 수행하지 않을 거라면 true 를 리턴한다.
return true;
} else {
// 뒤로 갈 수 없는 상태라면
// 다른 원하는 행동을 하면 된다
console.log("do something");
// 기본 뒤로가기 동작을 수행하지 않을 거라면 true 가 아닌 값을 리턴한다.
return false;
}
};
// 안드로이드 백버튼이 눌렸을 때 이벤트 리스너를 등록한다.
BackHandler.addEventListener("hardwareBackPress", onPress);
return () => {
BackHandler.removeEventListener("hardwareBackPress", onPress);
};
}, [navState.canGoBack]);
return (
<WebView
// ...
ref={ref}
// 웹뷰의
onNavigationStateChange={setNavState}
/>
);
};
onNavigationStateChange
콜백의 첫 번째 인자(navState
)는 웹뷰의 현재 상태가 담긴 객체인데, 해당 객체의 canGoBack
값은 현재 웹뷰에서 뒤로가기가 가능한 상태인지를 알려준다 (예를 들어 첫페이지에서는 뒤로 갈 수 없을 것이다. 그런 경우에는 false
값을 가진다).
뒤로 갈 수 있다면 ref.current.goBack()
메서드를 사용해 이전 페이지로 돌아가고, 그렇지 않다면 필요한 다른 작업을 하면 되겠다.
2. HTML5 History API 대응
하지만 onNavigationStateChange
콜백에 문제가 있으니, 바로 pushState()
, replaceState()
등 HTML5 History API로 추가된 기능으로 페이지 이동을 하면 동작하지 않는다는 것이다. 이는 canGoBack
값이 최신화되지 않는다는 뜻이고, 위에서 작성한 코드가 정상적으로 동작할 수 없다는 뜻이다.
물론 여기서 주저앉을 수는 없다. 이제 우리는 onMessage
콜백과 webview.injectJavaScript()
메서드를 통해 pushState()
, replaceState()
동작 시 웹뷰의 상태를 받아오도록 할 것이다.
2.1. webview.injectJavaScript()
webview.injectJavaScript()
메서드는 웹뷰에 자바스크립트 코드를 삽입할 수 있도록 해주는 메서드이다. 해당 메서드를 통해 pushState()
와 replaceState()
메서드를 래핑해줄 것이다.
const INJECTED_CODE = `
(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.replaceState = wrap(history.replaceState);
window.addEventListener('popstate', function() {
window.ReactNativeWebView.postMessage('navigationStateChange');
});
})();
true;
`;
const HomeScreen = (props) => {
// ...
return (
<WebView
// ...
ref={ref}
onLoadStart={() => ref.current.injectJavaScript(INJECTED_CODE)}
onNavigationStateChange={setNavState}
/>
);
};
2.1.1. injectedJavaScript
props 는 안 되나?
React Native WebView 에는 webview.injectJavaScript()
메서드와 비슷한 기능을 하는 injectedJavaScript
prop도 존재한다. 하지만 결정적인 차이점이 존재하는데, webview.injectJavaScript()
는 우리가 원하는 때에만 골라서 코드를 삽입할 수 있지만, injectedJavaScript
는 onLoad
이벤트가 발생할 때마다 코드를 삽입된다는 것이다.
onLoadStart
이벤트는 실제 페이지 이동이 일어날 때만 발생하는데 반해 onLoad
이벤트는 History API 등으로 로딩이 발생할 때도 발생한다. 때문에 같은 코드가 중복적으로 삽입될 수 있고, 우리는 그러한 현상을 피하기 위해 onLoadStart
와 injectJavaScript()
메서드를 조합해서 코드를 삽입한다.
(단, injectedJavaScriptBeforeContentLoaded
prop 을 사용하면 위 코드와 거의 동일한 효과를 낼 수 있다.)