
React Native WebView Bridge 통신 안정성 개선하기
Handshake 패턴을 활용하여 WebView와 네이티브 앱 간 메시지 유실 없이 안정적인 브리지 통신을 구현하는 방법을 소개합니다.
시작하며
React Native에서 WebView를 활용하여 웹과 앱 간 브리지 메시지를 주고받는 과정에서, 특정 디바이스나 특정 페이지에서 메시지가 간헐적으로 전달되지 않는 문제가 발생하는 경우가 있습니다.
앱에서는 분명 메시지를 전송했지만 웹에서는 이를 전혀 감지하지 못하는 형태로 나타나며, 재현이 어려워 실무에서 상당한 디버깅 비용을 초래하는 문제입니다.
특히 WebView의 onLoad, injectedJavaScript, injectedJavaScriptBeforeContentLoaded, injectJavaScript, postMessage와 같은 다양한 초기화 관련 Props들은 “웹이 준비되었다”는 명확한 보장을 제공하지 않습니다.
최근에는 Next.js와 같은 프레임워크가 보편화되면서 hydration, JS 번들 로딩, lazy loading 등의 초기 렌더링 과정이 복잡해졌고, 이러한 이유로 WebView 내부 JavaScript 실행 시점이 불안정해져 브리지 메시지가 사라지는 문제가 실무에서 더욱 자주 관찰되고 있습니다.
본 글에서는 이러한 메시지 누락 문제의 원인을 분석하고, 메시지 손실을 근본적으로 제거할 수 있는 Handshake 기반 통신 구조를 소개합니다.
문제 상황
기존 구현 방식
일반적으로 React Native WebView에서 앱은 다음과 같은 Props 또는 메서드를 활용해 웹으로 메시지를 전달합니다.
- onLoad
- injectJavaScript
- injectedJavaScript
- injectedJavaScriptBeforeContentLoaded
- postMessage
이 방식들은 모두 "웹이 준비되었다고 가정한 상태에서" 메시지를 전송합니다.
다음은 대표적인 예시 코드입니다.
import React, { useRef } from 'react'
import { WebView } from 'react-native-webview'
export default function App() {
const webViewRef = useRef<WebView>(null)
const sendMessageToWeb = () => {
// 앱에서 웹으로 메시지 전송
webViewRef.current?.injectJavaScript(`
window.postMessage({ type: 'USER_INFO', data: { id: 123, name: 'John' } });
true;
`)
}
return (
<WebView
ref={webViewRef}
source={{ uri: 'https://your-web-app.com' }}
onLoad={() => {
// 페이지 로드 완료 시 메시지 전송
sendMessageToWeb()
}}
/>
)
}useEffect(() => {
// 웹에서 앱으로부터 메시지 수신
const handleMessage = (event: MessageEvent) => {
if (event.data.type === 'USER_INFO') {
console.log('Received user info:', event.data.data)
setUserInfo(event.data.data)
}
}
window.addEventListener('message', handleMessage)
return () => window.removeEventListener('message', handleMessage)
}, [])실제로 발생하는 문제
위 코드는 간단한 페이지에서는 잘 동작하지만, 다음과 같은 상황에서 문제가 발생합니다:
-
특정 Android 기기에서 간헐적 통신 실패
- WebView의
onLoad이벤트가 발생했지만 웹의 JavaScript가 완전히 준비되지 않은 상태 - 특히 저사양 기기에서 스크립트 초기화가 느릴 때 발생
- WebView의
-
Next.js 같은 프레임워크에서의 타이밍 이슈
- Next.js의 hydration 과정이 완료되기 전에 메시지 전송
- React의
useEffect가 실행되기 전에 메시지가 도착 - CSR 페이지에서 번들 로딩이 느린 경우
-
네트워크 상태에 따른 불안정성
- 느린 네트워크 환경에서 JavaScript 번들 로딩 지연
- Lazy loading으로 인한 컴포넌트 초기화 지연
문제의 근본 원인
핵심은 WebView의 onLoad 이벤트와 웹의 JavaScript 준비 완료 시점이 일치하지 않는다는 것입니다.
Timeline of events:
┌──────────────────────────────────────────────────────────────────┐
│ WebView onLoad 발생 │
│ ↓ │
│ injectJavaScript 실행 ❌ 웹이 준비되지 않음 │
│ ↓ │
│ HTML 파싱 완료 │
│ ↓ │
│ JavaScript 번들 다운로드 │
│ ↓ │
│ React hydration │
│ ↓ │
│ useEffect 실행 (message listener 등록) ✅ 이제야 준비 완료 │
└──────────────────────────────────────────────────────────────────┘
이 타이밍 차이로 인해 앱에서 보낸 메시지가 웹에서 수신되지 못하고 유실됩니다.
일반적인 해결 시도: Timeout
이 문제를 해결하기 위해 많은 개발자들이 setTimeout을 사용하여 메시지 전송을 지연시키는 방법을 시도합니다:
export default function App() {
const webViewRef = useRef<WebView>(null)
const sendMessageToWeb = () => {
webViewRef.current?.injectJavaScript(`
window.postMessage({ type: 'USER_INFO', data: { id: 123, name: 'John' } });
true;
`)
}
return (
<WebView
ref={webViewRef}
source={{ uri: 'https://your-web-app.com' }}
onLoad={() => {
// 웹이 준비될 시간을 주기 위해 지연
setTimeout(() => {
sendMessageToWeb()
}, 1000) // 1초 대기
}}
/>
)
}위 구현은 단순한 웹페이지에서는 정상적으로 작동하지만, 다음과 같은 경우 메시지가 유실될 가능성이 높습니다.
-
Android 특정 기기에서의 간헐적 통신 실패 WebView의 onLoad는 발생했지만 웹의 JavaScript가 완전히 준비되지 않은 상태입니다.
-
Next.js 기반 웹에서의 초기화 지연 hydration 또는 CSR 환경에서 useEffect가 실행되기 전에 메시지가 도착할 수 있습니다.
-
네트워크 · 번들 로딩 지연 JS 번들 다운로드가 늦을 경우 이벤트 리스너는 더 늦게 등록됩니다.
해결 방법: Handshake 패턴
핵심 개념
문제를 해결하기 위한 핵심 아이디어는 간단합니다:
웹이 준비되면 먼저 앱에게 알려주고, 앱은 그 신호를 받은 후에 메시지를 보낸다.
이는 네트워크 통신에서 사용되는 Handshake(핸드셰이크) 패턴과 유사합니다. TCP의 3-way handshake처럼, 웹과 앱이 서로 준비 상태를 확인한 후 통신을 시작하는 것입니다.
기존 방식 (불안정):
App → Web: 메시지 전송 (웹이 준비되지 않았을 수 있음)
Handshake 방식 (안정):
Web → App: "준비 완료!" (READY 메시지)
App → Web: "확인! 메시지 보낼게" (실제 데이터 전송)
구현 방법
1. 웹에서 준비 완료 신호 보내기
import { useEffect, useCallback } from 'react'
interface WebViewMessage {
type: string
data?: any
}
export function useWebViewBridge() {
const isWebView = typeof window !== 'undefined' &&
window.ReactNativeWebView !== undefined
// 앱으로 메시지 전송하는 함수
const sendToApp = useCallback((message: WebViewMessage) => {
if (isWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify(message))
}
}, [isWebView])
// 앱으로부터 메시지 수신
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
try {
const message = JSON.parse(event.data)
console.log('[Web] Received from app:', message)
// 여기서 메시지 타입별로 처리
switch (message.type) {
case 'USER_INFO':
// 사용자 정보 처리
break
case 'NAVIGATION':
// 네비게이션 처리
break
}
} catch (error) {
console.error('[Web] Failed to parse message:', error)
}
}
if (isWebView) {
window.addEventListener('message', handleMessage)
// ✅ 중요: 웹이 준비되었음을 앱에 알림
sendToApp({ type: 'WEB_READY' })
return () => window.removeEventListener('message', handleMessage)
}
}, [isWebView, sendToApp])
return { sendToApp, isWebView }
}2. 앱에서 준비 신호 대기 후 메시지 전송
import React, { useRef, useState, useCallback } from 'react'
import { WebView } from 'react-native-webview'
import type { WebViewMessageEvent } from 'react-native-webview'
export default function App() {
const webViewRef = useRef<WebView>(null)
const [isWebReady, setIsWebReady] = useState(false)
const pendingMessages = useRef<any[]>([])
// 웹으로 메시지 전송
const sendToWeb = useCallback((message: any) => {
const script = `
window.postMessage(${JSON.stringify(message)});
true;
`
if (isWebReady) {
// 웹이 준비되었으면 즉시 전송
webViewRef.current?.injectJavaScript(script)
} else {
// 웹이 준비되지 않았으면 대기열에 추가
pendingMessages.current.push(message)
}
}, [isWebReady])
// 웹으로부터 메시지 수신
const handleWebViewMessage = useCallback((event: WebViewMessageEvent) => {
try {
const message = JSON.parse(event.nativeEvent.data)
console.log('[App] Received from web:', message)
if (message.type === 'WEB_READY') {
// ✅ 웹이 준비 완료됨
console.log('[App] Web is ready!')
setIsWebReady(true)
// 대기 중이던 메시지들을 모두 전송
pendingMessages.current.forEach((pendingMsg) => {
const script = `
window.postMessage(${JSON.stringify(pendingMsg)});
true;
`
webViewRef.current?.injectJavaScript(script)
})
// 대기열 초기화
pendingMessages.current = []
}
} catch (error) {
console.error('[App] Failed to parse message:', error)
}
}, [])
// 예시: 사용자 정보 전송 버튼
const handleSendUserInfo = () => {
sendToWeb({
type: 'USER_INFO',
data: { id: 123, name: 'John', email: 'john@example.com' }
})
}
return (
<WebView
ref={webViewRef}
source={{ uri: 'https://your-web-app.com' }}
onMessage={handleWebViewMessage}
/>
)
}개선된 구조의 장점
-
타이밍 이슈 완전 해결
- 웹이 완전히 준비된 후에만 메시지 전송
- React의
useEffect실행 보장
-
메시지 유실 방지
- 웹 준비 전 메시지는 대기열에 보관
- 준비 완료 후 순차적으로 전송
-
디버깅 용이
- 준비 상태를 명확히 추적 가능
- 각 단계별 로그 확인 가능
-
코드 단순화
- 다양한 Props(
injectJavaScript,injectedJavaScript,postMessage등)를onMessage중심의 양방향 통신으로 통일 - 일관된 메시지 전송 패턴으로 유지보수 용이
- 다양한 Props(
마무리
React Native WebView와 웹 간 통신은 초기화 단계의 타이밍이 일치하지 않을 때 쉽게 불안정해질 수 있습니다. 특히 Next.js와 같이 초기 렌더링 구조가 복잡한 프레임워크에서는 이러한 현상이 더 빈번하게 발생합니다.
Handshake 패턴을 적용하면 웹이 완전히 준비된 상태에서만 메시지 전송이 이루어지므로, 메시지 유실 문제를 근본적으로 해결할 수 있습니다.
본 글에서는 설명을 위해 간단한 큐(queue) 구조를 사용했지만, 실제 프로젝트에서는 앱의 특성에 따라 큐가 불필요할 수도 있습니다. 예를 들어, 웹이 준비된 후에만 메시지를 보내는 단순한 흐름이라면 준비 상태 플래그만으로도 충분합니다. 반면, 초기화 과정에서 여러 메시지를 순차적으로 전달해야 하는 경우에는 큐를 활용하여 메시지 순서를 보장하는 것이 유용합니다.
WebView 기반 하이브리드 앱을 개발하거나 운영하고 계시다면, 본 패턴을 적용하여 통신 안정성을 강화해보시기 바랍니다.