병원 상세 웹뷰 통신 및 크로스 브라우징
안녕하세요. 비브로스 웹프론트엔드팀 박서영입니다.
똑닥 앱은 네이티브 앱과 웹이 결합한 하이브리드앱으로 구성되어있습니다. 하이브리드 앱이란 빠른 실행 속도와 운영체제 및 디바이스 제어가 가능한 네이티브 앱의 장점, 강제 업데이트와 스토어 심사 없이 최신화면을 제공하는 웹의 장점을 합친 어플리케이션을 의미합니다.
현재 병원 상세와 이벤트/투표, 공지사항과 주소 찾기 등 똑닥 앱 내 다양한 화면이 웹뷰로 제공되고 있습니다. 이 중 병원 상세 화면을 통해 웹과 앱은 어떻게 통신하는지, 웹뷰 개발 시 고려해야 할 부분은 어떤 것들이 있는지, 그리고 기존 구현 방식에서 어떤 부분을 개선할 수 있는지 간략하게 살펴보겠습니다.
웹뷰와 앱은 어떻게 통신하는가?
웹 → 안드로이드
웹은 보안 등의 이유로 디바이스의 자원(주소록, 사진 등)이나 하드웨어(카메라, 마이크 등)에 자유롭게 접근할 수 없습니다. 그래서 네이티브 코드를 통해 해당 자원에 접근해야 하는데 앱의 네이티브 코드와 웹의 자바스크립트 코드 간의 인터페이스를 지원하는 것이 바로 JavaScript Interface 입니다.
public class WebViewInterface {
@JavascriptInterface
public void showSnackBar(String message) {
...
}
}
webView.addJavascriptInterface(new WebAppInterface(), "ddInterface")
앱에서는 웹뷰와 통신할 클래스를 만들고 웹뷰에서 호출할 메서드들을 정의합니다. 이 때, Javascript Interface 어노테이션 붙은 메서드만 자바스크립트의 접근이 허용됩니다. 그리고 addJavascriptInterface 메서드를 사용하여 클래스를 웹뷰에 등록하고 웹뷰에서 호출할 전역 변수명을 전달합니다.
const ToastButton = () => {
function onClickCreateButton () {
window.ddInterface.showSnackBar('등록이 완료되었습니다.');
}
return (
<button onClick={onClickCreateButton}>사용자 등록</button>
);
}
웹의 자바스크립트 코드는 window 객체의 ddInterface 프로퍼티를 통해 앱 메서드를 호출할 수 있습니다. window 객체 란 웹 브라우저 창을 나타내는 객체입니다. 브라우저 안의 요소들이 모두 포함되어있는 최상위 객체이기 때문에 어디서나 window.ddInterface에 접근하여 앱 메서드를 호출할 수 있게 됩니다.
웹 → iOS
override func viewDidLoad() {
let config = WKWebViewConfiguration()
let userContentController = WKUserContentController()
userContentController.add(self, name: "ddInterface")
config.userContentController = userContentController
...
}
extension WebViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.name == 'domainUrl',
let command = message.body.command else { return }
switch command {
case .showToast:
return onShowToast();
case ....
}
}
}
- WKWebViewConfiguration : 웹페이지 렌더링 속도, 미디어 재생 처리 방법 등 옵션 설정 객체
- WKUserContentController : 앱과 자바스크립트 코드 사이의 통신을 관리하는 객체
- WKScriptMessageHandler : 자바스크립트 코드로부터 메세지를 수신하기 위한 인터페이스
먼저 앱에서 WKUserContentController 객체의 add 메서드를 통해 자바스크립트 코드가 호출할 메시지 핸들러를 설치합니다. 그리고 WKWebViewConfiguration 객체의 userContentController 프로퍼티에 WKUserContentController를 할당하면, 자바스크립트에서 window.webkit.messageHanlders.{handlerName}.postMessage()
를 통해 메시지를 보낼 수 있습니다.
declare global {
interface Window {
webkit?: Webkit;
}
}
export interface Webkit {
messageHandlers: {
ddInterface: {
postMessage(message: any): void;
};
};
}
window.webkit.messageHandlers.ddInterface.postMessage({
command: 'showToast',
data: {
args: { ... },
},
});
기본적으로 스크립트는 동일 출처 정책에 의해 프로토콜, 도메인, 포트 번호가 다른 경우 통신이 불가능합니다. 그러나 window.postMessage 메서드는 (올바르게 사용할 경우) 이 제약 조건을 우회하여 출처가 다른 window와도 안전하게 cross-origin 통신할 수 있도록 도와줍니다. 그리고 자바스크립트에서 postMessage 메서드를 사용해 JSON 형식으로 메시지를 보내면, ios에서 WKScriptMessage 형식으로 자동 변환하여 메시지를 수신합니다.
앱 → 웹
반대로 앱에서 자바스크립트 코드를 호출해야 하는 경우도 있습니다. 예를 들어 병원 상세 페이지의 북마크(☆)는 로그인이 필요한 기능입니다. 비로그인 유저가 북마크 버튼을 클릭하면 자바스크립트에서는 앱의 로그인 화면을 호출합니다.
만약 유저가 로그인을 완료하고 돌아왔다면 웹뷰는 북마크 API를 조회해 북마크 여부를 표시해주어야 합니다. 그러나 유저가 로그인을 완료하고 돌아온 건지, 로그인 화면을 닫고 돌아온건지 웹뷰는 알 수가 없습니다. 따라서 앱에서 유저의 로그인 결과에 대한 커스텀 이벤트를 전달해주어야 하는데요.
먼저 웹에서 loginFinish 라는 이벤트 타입을 가진 커스텀 이벤트를 만들어 window 객체의 ddLoginFinish라는 프로퍼티에 할당합니다. 그리고 이벤트 리스너를 등록하는데 첫 번째 인자로는 트리거할 이벤트이름을, 두 번째 인자로는 이벤트가 트리거 되었을 때 실행시킬 메서드를 넘겨줍니다.
useEffect(()=>{
window.ddLoginFinish = new Event('loginFinish');
const afterLoginFinish = () => {
//...로그인 성공 후 실행할 로직
}
window.addEventListener('loginFinish', afterLoginFinish);
return () => {
window.removeEventListenter('loginFinish', afterLoginFinish);
}
},[])
앱에서는 WebView 클래스의 evaluateJavaScript라는 메서드를 사용하여 자바스크립트 코드를 실행시킬 수 있습니다. 유저가 로그인을 완료했다면, dispatchEvent라는 Web API를 사용하여 이전에 약속했던 로그인 완료 이벤트를 window 객 체로 발송하면 됩니다.
WebView.evaluateJavaScript(String, ValueCallback<String>
override fun onLoginFinish() {
webView.evaluateJavascript("(function() { window.dispatchEvent(ddLoginFinish); })();", null);
}
이미지 출처 - https://gradler.tistory.com/32
이미지 출처 - https://gradler.tistory.com/33
웹뷰 개발 시 고려해야할 부분은 무엇인가?
OS별 UX
window 객체에는 다양한 프로퍼티가 존재합니다. window의 navigator는 스크립트를 구동 중인 브라우저의 정보를 담고 있는 객체입니다. 그리고 navigator의 userAgent는 HTTP 요청 헤더의 프로퍼티 중 하나로 디바이스, 브라우저, OS 등 유저 식별 정보를 포함하고 있는 문자열입니다. 예를 들어 2019 Intel Macbook pro가 window.navigator.userAgent를 브라우저 콘솔에 출력하면 아래와 같이 표시됩니다.
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
userAgent를 통해 유저 정보를 파악하면, OS별 유저 특성을 반영하여 개발할 수 있습니다. 일반적으로 안드로이드 유저는 바텀시트나 팝업을 닫을 때 뒤로가기 버튼을 클릭하여 닫는 경향이 있습니다. 아쉽게도 자바스크립트 코드는 안드로이드 디바이스의 물리 버튼 클릭 여부를 감지할 수 없습니다. 그러나 브라우저에서 뒤로가기란 이전 페이지로 탐색하는 동작이고, URL의 변화는 자바스크립트 코드가 감지할 수 있습니다.
그래서 병원 상세의 바텀시트는 hash(#) 라우팅을 기반으로 구현되어있습니다. URL의 hash 값에 따라 해당하는 바텀시트를 호출하고, 일치하는 hash 값이 없으면 바텀시트는 닫히게 됩니다. URL에 따라 바텀시트가 동작하기 때문에 안드로이드 유저는 뒤로가기 버튼을 클릭하여 바텀시트를 닫을 수 있게 되었습니다.