본문으로 건너뛰기
Tech Blog

앱 안에 심은 WebView와 Bridge

글 복사 완료!

웹팀은 당일 배포하는데 앱은 심사로 며칠을 기다리던 시절, 하이브리드 앱이 나온 이유예요.

·7분·

같은 기능을 iOS는 Swift로, Android는 Kotlin으로 두 번 만들어본 적 있으세요? 저는 작은 팀에서 이 구조를 돌리다가 버전 싱크가 어긋나는 걸 여러 번 봤어요. 웹 팀은 수정본을 당일에 배포하는데, 앱은 스토어 심사를 기다리면서 며칠이 흘러가죠. 이 답답함이 하이브리드 앱이라는 아이디어의 출발점이에요.

왜 하이브리드였나

네이티브로 두 플랫폼을 따로 개발하는 비용은 생각보다 큽니다. iOS 팀과 Android 팀을 따로 꾸려야 하고, 같은 기능을 두 번 설계해야 하거든요. 스타트업이나 작은 조직에서 이 구조를 감당하기는 꽤 버겁죠.

여기에 또 하나, 스토어 심사 병목이 있어요. 긴급 버그 하나를 고쳐도 심사에 며칠이 걸립니다. 그 사이에 유저는 깨진 화면을 보고 있죠. 웹은 푸시 한 번이면 끝나는데, 앱은 그렇지 않아요.

그래서 자연스럽게 떠오른 발상이 있어요. "앱 껍데기만 네이티브로 만들고, 안에는 웹 기술을 넣으면 어떨까?" 이 발상이 WebView와 Bridge, 나중엔 CodePush까지 이어지는 이야기의 시작점이에요.

앱 안에 브라우저를 심는다

WebView는 네이티브 앱 안에 박힌 브라우저 엔진이에요. iOS에서는 WKWebView, Android에서는 WebView라는 이름으로 제공되죠. 쉽게 말하면 앱의 어떤 영역을 "작은 사파리" 또는 "작은 크롬"으로 비워두고, 거기에 웹 페이지를 띄우는 거예요.

실무에서 WebView를 쓰는 자리는 생각보다 많아요. 약관 페이지, 공지사항, 이벤트 랜딩, 결제 PG사 페이지 같은 것들이죠. 웹팀이 이미 만들어둔 페이지를 그대로 가져다 붙이면 앱 빌드 없이도 콘텐츠를 매일 갈아끼울 수 있거든요.

하이브리드 앱이라는 개념의 1세대는 아예 앱 전체를 WebView 한 장으로 채우기도 했어요. CordovaIonic이 대표적이었죠. 앱 아이콘만 네이티브고, 그 안은 전부 웹이에요.

다만 WebView에는 몇 가지 한계가 따라붙어요. 스크롤과 애니메이션이 네이티브 대비 눈에 띄게 뻑뻑하고, iOS에서는 Apple이 WebKit 외 엔진을 오래 금지해왔어요. 그리고 WebView 안의 쿠키 저장소가 앱의 네트워크 스택과 분리돼 있어서 로그인 세션을 양쪽에서 맞추기도 까다롭죠. 네이티브 스와이프 뒤로가기와 웹의 가로 스크롤이 부딪히는 제스처 충돌도 흔한 골칫거리예요.

웹뷰에서 네이티브 기능을 호출하기

WebView로 페이지를 띄우는 것까지는 쉬워요. 문제는 그 안에서 카메라를 켜거나 푸시 토큰을 꺼내는 등의 네이티브 기능이 필요해질 때예요. WebView 안의 JS는 브라우저 샌드박스에 갇혀 있어서 기기의 능력을 직접 쓸 수 없거든요.

Bridge는 이 사이를 뚫어주는 통로예요. WebView 안의 JS가 네이티브로 메시지를 보내고, 네이티브가 결과를 다시 JS로 돌려주는 양방향 채널이죠.

// iOS WKWebView, 웹에서 네이티브로
window.webkit.messageHandlers.auth.postMessage({ type: 'GET_TOKEN' });
 
// Android, 웹에서 네이티브로
window.Android.getToken();
 
// 네이티브에서 웹으로
webView.evaluateJavascript("window.onToken('xxx')");

초창기에는 location.href = "myapp://action?data=..." 같은 URL scheme 해킹 방식도 많이 썼지만, 지금은 대부분 postMessage 기반으로 통일됐어요. MDN은 postMessage를 origin 검증을 갖춘 크로스 컨텍스트 통신 기본기로 설명하는데, WebView Bridge도 같은 기반 위에서 돌아가죠.

이 방식에는 몇 가지 비용이 따라와요. 메시지는 모두 문자열로 직렬화해서 주고받기 때문에 타입 안전성이 없어요. 웹은 항상 최신 배포인데 앱은 구버전이 섞여 있어서 버전 스큐도 흔하고요.

보안은 더 민감한 문제예요. Android의 addJavascriptInterface는 과거에 리플렉션을 통해 임의의 Java 메서드를 호출당하는 취약점이 있었어요. API 17부터는 @JavascriptInterface 애너테이션이 붙은 메서드만 노출되도록 제한이 생겼지만, 그 전 API 16 이하에서는 WebView에 로드되는 모든 프레임이 주입된 객체를 통해 앱 권한을 넘볼 수 있었죠.

"The addJavascriptInterface, postWebMessage, and postMessage methods can be leveraged by malicious actors to access, manipulate, or inject code they control into a WebView." - Android Developers

WebView 안에서 로드되는 콘텐츠가 전부 신뢰 가능하지 않다면, 이 통로들은 공격자의 진입점으로 바뀔 수 있다는 얘기예요. 그래서 모르는 출처를 WebView에 띄우지 않고, iframe을 포함한 모든 프레임의 origin을 검증하는 게 기본이에요.

React Native가 이어받은 Bridge 아이디어

Web Bridge 아이디어는 WebView만의 이야기가 아니에요. React Native의 초기 아키텍처도 같은 발상에서 출발했거든요. JS 엔진에서 돌아가는 UI 코드가 네이티브 컴포넌트를 "원격 조종"하는 구조죠.

다만 조종 대상이 달라요. WebView Bridge는 WebView 안의 웹 JS와 호스트 네이티브 사이를 잇는 통로고, React Native의 Bridge는 JSC나 Hermes 같은 자바스크립트 엔진과 네이티브 모듈 사이를 잇는 통로예요. 끝점은 다르지만 "JS는 샌드박스라 네이티브 능력을 빌려와야 한다"는 같은 문제를 풀어요.

React Native의 초기 Bridge는 메시지를 JSON으로 직렬화해서 비동기로 흘려보내는 구조였어요. 게다가 호출을 모아서 배치로 처리했죠. 스크롤처럼 높은 프레임레이트가 필요한 동작에서는 이 구조가 결국 병목이 됐어요. JS 스레드가 아무리 빨라도 메시지가 Bridge를 건너는 동안 16ms 안에 프레임이 끝나야 하는 약속을 놓치기 시작했거든요.

이 병목을 걷어내는 게 React Native의 New Architecture 이야기예요. 그건 다음 편에서 자세히 다룰게요.

다음 편 예고

여기까지가 하이브리드 앱의 1세대 이야기예요. 앱 안에 웹을 심고, Bridge로 네이티브와 이어붙이는 구조죠. Cordova부터 초기 React Native까지, 공통된 아이디어 위에 올라타 있어요.

다음 편에서는 이 Bridge를 어떻게 얇게 만들거나 아예 걷어내는지, 그리고 스토어 심사라는 또 다른 병목을 어떻게 우회해왔는지 살펴볼게요. React Native의 New Architecture, Flutter의 다른 접근, 그리고 CodePush가 그 이야기예요.

참고 자료