RN ↔ WebView 통신 방식과 웹뷰 스택 패턴

1. RN ↔ WebView 통신 방식

RN은 네이티브 런타임, WebView는 브라우저 엔진이라 실행 환경이 다르다. 그래서 둘은 문자열 메시지를 주고받는 방식으로만 소통한다. 방향은 두 가지다.

웹에서 react-native-webview가 주입해 주는 전역 객체로 메시지를 보내고, RN은 onMessage로 받는다.

// RN
<WebView onMessage={({ nativeEvent }) => {
  const { type } = JSON.parse(nativeEvent.data);
  if (type === "GO_BACK") navigation.goBack();
}} />
// 웹
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "GO_BACK" }));

window.ReactNativeWebView가 있는지(!!window.ReactNativeWebView)로 지금이 앱 웹뷰인지 그냥 모바일 웹인지 구분한다.

injectJavaScript로 웹에서 실행할 JS를 밀어 넣거나, webviewRef.postMessage로 보내 웹의 message 리스너로 받는다. 리스너를 붙이는 대상이 플랫폼마다 달라서(Android는 document, iOS는 window) 이 부분은 항상 신경 써야 한다.

규모가 커지면 보통 { type, payload } 같은 envelope을 정해 두고 양쪽에 얇은 브릿지를 둔다. 라우팅, 토큰 전달, 공유 같은 걸 이 채널 하나로 처리한다.

한계도 분명하다. 데이터가 전부 문자열(JSON)로 직렬화되고, 요청-응답이 아니라 이벤트를 흘려보내는 구조다. 큰 payload를 자주 보내면 그 자체가 부담이 된다.

2. “웹뷰 스택” 패턴

단일 웹뷰 안에서 SPA 라우팅으로 화면을 넘기면 push/pop 전환, 스와이프 뒤로가기, 헤더·탭바 연동 같은 네이티브다운 전환을 흉내 내기 어렵다. 그래서 네이티브 스택 네비게이터의 각 스크린에 웹뷰를 하나씩 얹는 구조를 쓴다.

NativeStack
 ├─ Screen A → WebView(/a)
 ├─ Screen B → WebView(/b)
 └─ Screen C → WebView(/c)

전환·제스처·헤더는 네이티브가, 콘텐츠는 웹이 맡는다. 웹뷰가 “이 URL로 push해 달라”고 메시지를 보내면 네이티브가 새 스크린(=새 웹뷰)을 스택에 쌓는다.

문제는 여기서 생긴다. 스택이 깊어질수록 살아있는 웹뷰 인스턴스가 계속 늘어난다. A→B→C→D로 들어가면 뒤에 쌓인 웹뷰들이 화면에 안 보여도 메모리에 그대로 남게 된다.

  1. 여러 웹뷰를 띄우면 생기는 메모리 문제

배경부터 보면, iOS의 WKWebView는 인스턴스마다 별도 콘텐츠 프로세스를 띄운다. 앱별 메모리 상한(jetsam)을 넘기면 OS가 앱을 통째로 종료시킨다. Android의 System WebView는 크로미움 기반이라 프로세스를 일부 공유하지만, 인스턴스마다 렌더 리소스가 쌓이고 메모리가 부족하면 프로세스가 회수된다. 웹뷰 3개는 브라우저 탭 3개와 비슷하다고 보면 된다.

메모리 문제가 잘 터지는 상황은 대체로 이렇다.

•	스택을 깊게 파고들어 웹뷰가 여러 개 동시에 떠 있을 때

•	한 화면에 웹뷰를 여러 개 배치할 때(예: 리스트 아이템마다 웹뷰)

•	이미지·미디어가 많은 무거운 페이지를 웹뷰로 띄울 때

•	postMessage로 큰 데이터를 반복 전송할 때

•	웹뷰를 오래 살려 두고 계속 JS를 주입할 때

iOS에서는 OOM 종료로, Android에서는 프로세스 회수로 나타난다.

  1. 개선 방법

4.1 스택 깊이 제한 + 스냅샷

화면에 안 보이는 뒤쪽 웹뷰는 정적 이미지로 대체하고 실제 인스턴스는 해제한다. 뒤로 돌아오면 다시 로드한다.

핵심은 depth(스택에서 밀린 정도)를 기준으로 임계치를 넘으면 스냅샷으로 바꿔 치는 것이다. 사용자 눈에는 화면이 그대로 있는 것처럼 보이지만, 살아있는 웹뷰는 항상 소수로 유지된다. 대신 복귀 시 재로드와 스크롤 위치 복원이 필요하다.

import { useRef, useState } from "react";
import { Image } from "react-native";
import { WebView } from "react-native-webview";
import { captureRef } from "react-native-view-shot";
import { useIsFocused } from "@react-navigation/native";

// depth: 이 스크린이 스택에서 뒤로 몇 칸 밀렸는지 (0이면 최상단)
function StackedWebView({ uri, depth }: { uri: string; depth: number }) {
  const viewRef = useRef(null);
  const [snapshot, setSnapshot] = useState<string | null>(null);
  const focused = useIsFocused();

  // 스택에서 2칸 이상 밀리면 스냅샷으로 대체하고 웹뷰 해제
  const shouldDetach = depth >= 2;

  const takeSnapshot = async () => {
    const uri = await captureRef(viewRef, { format: "png", quality: 0.8 });
    setSnapshot(uri);
  };

  if (shouldDetach && snapshot) {
    // 실제 WebView는 언마운트된 상태 → 메모리 반환
    return <Image source={{ uri: snapshot }} style={{ flex: 1 }} />;
  }

  return (
    <View ref={viewRef} collapsable={false} style={{ flex: 1 }}>
      <WebView
        source={{ uri }}
        // 화면을 벗어나기 직전에 스냅샷을 찍어 둔다
        onBlur={takeSnapshot}
      />
    </View>
  );
}

4.2 오프스크린 freeze / unmount

react-freeze나 네비게이션 옵션으로, 안 보이는 스크린의 렌더링을 멈추거나 언마운트한다. freeze는 상태를 보존한 채 렌더만 멈추고, 언마운트는 인스턴스를 제거해 메모리를 확실하게 돌려준다.

// (a) react-freeze: 포커스를 잃으면 렌더 정지, 상태는 유지
import { Freeze } from "react-freeze";
import { useIsFocused } from "@react-navigation/native";

function FrozenWebView({ uri }: { uri: string }) {
  const focused = useIsFocused();
  return (
    <Freeze freeze={!focused}>
      <WebView source={{ uri }} />
    </Freeze>
  );
}
// (b) 언마운트: 화면을 벗어나면 인스턴스 제거 → 메모리 반환
<Stack.Screen
  name="Detail"
  component={DetailWebView}
  options={{
    unmountOnBlur: true,      // 벗어나면 언마운트
    detachInactiveScreens: true, // 비활성 스크린 분리
  }}
/>

상태를 지켜야 하는 화면이면 freeze, 메모리를 확실히 비워야 하는 무거운 화면이면 언마운트를 쓴다. 화면 성격에 맞춰 고르면 된다.

  1. 정리

RN ↔ WebView 통신은 결국 postMessage(웹→앱)와 injectJavaScript/postMessage(앱→웹)를 축으로 한 문자열 메시지 패싱이다. 웹뷰 스택은 네이티브다운 전환을 얻는 좋은 패턴이지만, 스택이 깊어질수록 살아있는 인스턴스가 쌓여 메모리 문제로 직결된다. 대응의 요점은 동시에 떠 있는 웹뷰 수를 줄이는 것이고, 그 과정에서 세션 공유·상태 복원·히스토리 충돌·플랫폼 차이·백그라운드 회수까지 같이 설계해야 한다.

ㅇㅇ

글 목록으로