웹뷰(WebView)는 앱 화면에 끼워 넣는 "주소창 없는 브라우저 한 장"이다. iOS는 WKWebView, Android는 WebView를 쓴다. 화면 대부분을 웹으로 그리는 하이브리드 앱은 웹을 배포하면 앱도 바뀐다는 장점이 있지만, 푸시 알림 수신이나 앱 리뷰 팝업, 다른 앱 열기처럼 웹이 못 하는 일은 네이티브가 해야 한다. 웹 기술로 쓰되 웹뷰 없이 네이티브 화면으로 그리는 Lynx 같은 프레임워크는 다른 길이다. 웹뷰 안의 JS(JavaScript)와 바깥의 네이티브 코드는 변수도 함수도 공유하지 않으므로, 플랫폼이 열어 둔 통로로만 말을 주고받는다.
통로 네 가지
| 통로 | 방향 | 하는 일 |
|---|---|---|
| User-Agent 표식 | 앱 → 웹·서버 | "앱 안에서 열렸다"는 신분 표시 |
| 메시지 핸들러 | 웹 → 앱 | 웹이 네이티브 기능을 부탁 |
| 스크립트 실행 | 앱 → 웹 | 네이티브가 웹에 값을 주입 |
| URL(딥링크·쿼리) | 양쪽 | 화면 이동 자체가 메시지 |
User-Agent 표식. iOS는 WKWebViewConfiguration.applicationNameForUserAgent로 기본 UA(User-Agent) 뒤에 MyApp/ios/4.2.0 같은 토큰을 붙일 수 있다. 웹뷰가 보내는 모든 요청에 붙으므로 웹과 서버가 같은 정규식으로 앱 여부를 판단한다. 다만 서버 렌더링 시점에는 클라이언트 전용 정보(브리지 존재 여부 등)를 알 수 없고, 포맷을 바꾸면 판별 코드를 모든 곳에서 함께 고쳐야 한다.
웹 → 앱. iOS는 WKUserContentController에 이름을 붙여 WKScriptMessageHandler를 등록하면, 웹에서 window.webkit.messageHandlers.<이름>.postMessage(값)으로 보낼 수 있다. 답을 받아야 하면 프로미스로 돌려주는 WKScriptMessageHandlerWithReply가 있다. Android는 2026년 현재 WebViewCompat.addWebMessageListener를 권한다. 허용할 출처 목록을 정해 두어 다른 사이트나 끼어든 iframe이 네이티브 코드를 부르지 못하게 할 수 있기 때문이다. 오래된 addJavascriptInterface는 출처 검사가 없고 모든 프레임에 노출돼 권하지 않는다.
// 웹 쪽: 앱이면 네이티브로, 아니면 웹 SDK로
if (window.webkit?.messageHandlers?.analytics) {
window.webkit.messageHandlers.analytics.postMessage({ event, params })
} else {
webAnalytics.track(event, params)
}이벤트를 정의하는 쪽을 웹 한 곳에 두고 네이티브는 배달만 하면, 이벤트를 바꿀 때 앱 심사 없이 웹 배포로 끝난다.
앱 → 웹. evaluateJavaScript(iOS)·evaluateJavascript(Android)로 웹뷰 안에서 JS 문자열을 실행하고 결과값도 받아 온다. 페이지가 로드될 때마다 자동 실행할 스크립트는 iOS의 WKUserScript로 심는다. 문자열을 이어 붙여 코드를 만들 때는 값을 JSON으로 직렬화해 따옴표 깨짐과 주입을 막는다.
URL. 앱 밖에서 들어오는 링크는 iOS 유니버설 링크(Universal Links)와 Android 앱 링크(App Links), 또는 myapp:// 같은 커스텀 스킴으로 앱을 연다. 앱은 받은 URL을 웹 주소로 바꿔 웹뷰에 로드한다. 푸시 알림을 탭했을 때도 링크에 쿼리를 붙여 넘기면 웹이 그걸 읽고 처리한다. 반대로 웹뷰가 이동하려는 URL은 네이티브가 이동 직전에 검사해 외부 앱이나 시스템 브라우저로 돌릴 수 있다.
React Native WebView와 메시지 설계
React Native의 react-native-webview에서는 웹이 window.ReactNativeWebView.postMessage(문자열)로 보내고 네이티브가 onMessage의 event.nativeEvent.data로 받는다. 인자는 문자열 하나뿐이라 객체는 JSON으로 직렬화한다. onMessage를 설정하지 않으면 이 함수가 웹에 주입되지 않는다. 반대 방향은 injectJavaScript로 웹뷰 안에서 코드를 실행한다.
type BridgeMessage =
| { type: 'REQUEST_BIOMETRIC_AUTH'; requestId: string }
| { type: 'GET_APP_VERSION'; requestId: string }
| { type: 'AUTH_RESULT'; requestId: string; success: boolean }
const allowed = new Set(['REQUEST_BIOMETRIC_AUTH', 'GET_APP_VERSION'])
// onMessage 안: 허용 목록에 없는 type은 버린다
const msg = JSON.parse(event.nativeEvent.data)
if (!allowed.has(msg.type)) return- 메시지는
type이 붙은 스키마로 정하고, 요청마다requestId를 붙여 비동기 응답을 요청과 잇는다 - 받은
type을 그대로 네이티브 함수 이름처럼 호출하지 않는다. 허용 목록에 있는 것만 처리하고 필드도 검증한다 - 웹 페이지에는 꼭 필요한 메서드와 데이터만 노출한다. 페이지에 XSS가 있으면 공격자 스크립트가 같은 브리지를 쓸 수 있으니, 리프레시 토큰이나 PIN 같은 비밀은 가능하면 넘기지 않고 민감한 작업 전에는 메시지를 검증한다
이름이 같은 브라우저 표준 window.postMessage는 iframe·팝업 같은 창끼리 주고받는 API라 성격이 다르다. 보낼 때 둘째 인자 targetOrigin을 *로 두면 그 창이 다른 사이트로 이동했을 때 데이터를 엉뚱한 곳에 넘길 수 있다. 그래서 정확한 출처를 적고, 받는 쪽은 event.origin을 확인한 뒤 데이터 형식까지 검증한다.
두 세계의 시차
- SPA 화면 전환을 앱은 모른다. 웹뷰가 아는 건 문서를 새로 받는 진짜 이동뿐이다.
history.pushState로 주소만 바꾸는 SPA(Single-Page Application) 이동은 신호가 오지 않는다. 앱이 경로를 알아야 하면 웹이 메시지 핸들러로 직접 알리게 한다.pushState를 덮어쓰는 몽키패치는replaceState를 놓치거나 라우터 구현이 바뀌면 조용히 깨진다 - 앱은 웹이 준비됐는지 모른다. 로드 완료 콜백은 HTML 파싱 직후라, React가 전역 함수를 붙이기 전일 수 있다. 폴링과 재시도로 버틸 수도 있지만, 정석은 웹이 준비됐을 때 "ready" 메시지를 먼저 보내고 앱이 그때 값을 넣는 웹 주도 핸드셰이크다. 받은 값은 웹이 저장해 두면 다음 기회에 복구할 수 있다
앱 배포는 심사와 사용자 업데이트를 기다려야 하고 웹 배포는 즉시라서, 빈틈은 웹 쪽에서 메우는 쪽으로 자연스럽게 기운다. 웹이 저장한 값은 웹뷰의 웹 저장소에 있어서 네이티브 저장소와 자동으로 공유되지 않는다 → 모바일 네이티브 저장소
safe area
노치와 홈 인디케이터를 피하는 방법은 둘이다. 네이티브가 웹뷰 자체를 safe area 안으로 줄이거나, 웹뷰를 화면 전체에 깔고 웹이 CSS로 피한다. 후자는 <meta name="viewport" content="width=device-width, viewport-fit=cover">로 화면 끝까지 그리게 한 뒤 env(safe-area-inset-*)로 여백을 준다.
.bottom-bar {
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
}웹뷰 디버깅은 iOS에서 isInspectable을 켜면 Safari 웹 인스펙터가 붙는다. 운영 빌드에서는 끄는 것이 안전하다. 출처 개념은 동일 출처 정책, 서버 렌더링과 클라이언트 값의 어긋남은 하이드레이션을 본다.
출처: Apple — WKScriptMessageHandler · Apple — applicationNameForUserAgent · Apple — evaluateJavaScript · Android — Access native APIs with JavaScript bridge · MDN — env() · WebKit — Designing Websites for iPhone X · react-native-webview — Communicating between JS and Native · MDN — Window.postMessage() 보안