하이브리드 앱 브릿지(Bridge) 리팩토링 후기

네이티브 로 하이브리드 앱을 개발하게된 배경

하이브리드 앱을 만드는 프레임 워크는 현재 많습니다. React Native, flutter, XAMARIN, NativeScript, 여기에 Apache Cordova 계열로 가면 더 많아 집니다. 이런 계열의 프레임 워크로 하이브리드 앱을 만들게 되면, 기본적으로 플러그인 형태로 웹뷰와 네이티브 디바이스 를 연결 해서 접근할 수 있게 지원해 줍니다. 그렇기 때문에 사실 프레임 워크를 사용하면 웹뷰와 네이티브를 연결해주는 브릿지를 알 필요가 없습니다. 만들 필요도 없습니다. 플러그인으로 제공 하기 때문에 쓰기만 하면 됩니다.

웹뷰와 네이티브를 연결하는 방법

웹뷰와 네이티브를 연결하는 방법은 대부분 비슷합니다.

현재 구현된 코드의 문제점

입사했을 때 회사는 React Native 에 Next.js 구조의 하이브리드 앱이었습니다. 그리고 입사했을 때 구현되어있던 브릿지 코드를 분석하다 보니 다음과 같은 문제가 발견되었습니다.

useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
inAppBridge?.requestCamera(IMAGE_FILE_ID);
},
[inAppBridge],
<Styles.HiddenInput type=”file” id={IMAGE_FILE_ID} accept=”image/*” onChange={handlePostReviewImage} />
webViewRef?.current?.injectJavaScript(`document.getElementById(‘${targetId}’).click()`);
if (isInApp) {
inAppBridge?.requestMallLogin(`${BASE_URL}${router.asPath}`);
}

리팩토링의 과정

웹뷰에서 네이티브로 데이터 전달 과정

ANDROID 와 IOS 는 브릿지 만들 때 OS 가 다르다 보니 브릿지에서 이 다른 점을 맞추어 줘야 합니다.

// IOS
window.webkit.messageHandlers.jsToNative.postMessage(
{
action: eventName, // event 타입 이름
param: {…param}, // event 마다 전달할 param 정보
callback: callbackName // window.nativeCallback 에서 전달 받을 콜백 이름
}
)
// ANDROID
window.bridgeHandler.jsToNative(eventName, JSON.stringify(param), callbackName)
WEBVIEW => ANDROID: 보낼때 String 으로만 전달됨
WEBVIEW => IOS: 보낼때 객체 상태로 전달됨
WEBVIEW <= ANDROID: 받을때 객체로 전달 가능
WEBVIEW <= IOS: 받을때 String 으로 받아짐
WEBVIEW => ANDROID: 보낼때 String 으로만 전달됨
WEBVIEW => IOS: 보낼때 객체 상태로 받아짐
WEBVIEW <= ANDROID: 받을때 String 으로 받음
WEBVIEW <= IOS: 받을때 String 으로 받음
getCurrentPosition(){
return setCallbackStack(‘getCurrentPosition’, callbackName =>{
if (platformType === platformDetail.IOS_APP) return window.webkit.messageHandlers.jsToNative.postMessage({action: ‘getCurrentPosition’, param: {}, callback: callbackName})
if (platformType === platformDetail.ANDROID_APP) return window.bridgeHandler.jsToNative(‘getCurrentPosition’, JSON.stringify({}), callbackName)return console.error(‘not platformType’, platformType)
})
},
const setCallbackStack = (eventName, callback) => {
return new Promise((resolve) => {
// 콜백이름을 uuid 형태로 생성한다.
const callbackName = generateCallbackName(eventName)
// 여기서 실행될 콜백을 등록한다. 네이티브에서 호출해 줄때 Promise가 동작한다.
window.__Bridge__.__CALLBACKSTACK__[callbackName] = (res) => resolve(res)
// 생성된 콜백이름을 인자로 받은 콜백에 전달한다.
callback(callbackName)
})
}
inAppBridge?.getCurrentPosition().then((res: LocationBridgeModel) => getLocation(res))

네이티브에서 웹뷰로 데이터 전달 과정

window.nativeCallback = (callbackName, paramJSON) => {
const param = JSON.parse(paramJSON)
window.__Bridge__.__CALLBACKSTACK__[callbackName](param)
// 콜백 함수 삭제
delete window.__Bridge__.__CALLBACKSTACK__[callbackName]
}

좀 더 개선해 나가야 할 방향

이 방식의 문제점은 반드시 네이티브 가 브릿지 호출 시 반드시 콜백을 호출해 줘야 한다는 것입니다. 물론 이 방법도 브릿지 호출시 콜백 함수 이름을 넘긴경우와 아닌 경우로 나누어서 보내면 되긴 하지만 네이티브에서 한번이라도 콜백을 호출하지 않으면 메모리 릭이 될 가능성이 높아지게 됩니다. 현재는 이 케이스가 없도록 네이티브 쪽에서 반드시 호출 하도록 하기로 해서 문제를 피해가고 있습니다.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store