CMC 17기 활동의 목표는 "APP 출시"였습니다. 저희 팀은 React 기술 스택을 활용하여 유지보수성이 높은 앱을 효율적으로 개발하고자 했고, 그 첫 번째 관문은 바로 React Native와 WebView 간의 통신, 즉 '브릿지(Bridge)' 환경을 설정하는 것이었습니다.

🤔 왜 WebView를 선택했을까?
React로 만든 웹 프로젝트가 이미 있다면, WebView는 가장 빠르게 앱을 만들 수 있는 매력적인 선택지입니다. WebView는 앱 내부에 웹 브라우저를 띄워 특정 URL의 웹 페이지를 그대로 보여주는 방식입니다. 덕분에 기존 웹 코드를 재사용하여 iOS와 Android 앱을 동시에 대응할 수 있습니다.
하지만 여기서 한 가지 중요한 질문이 생깁니다. "앱의 네이티브 기능과 WebView 안의 웹은 어떻게 서로 소통할 수 있을까?"
😫 기존 방식의 문제점: postMessage와 onMessage
React Native와 WebView는 기본적으로 postMessage와 onMessage를 통해 통신합니다. 한쪽에서 메시지를 이벤트로 보내면(post), 다른 쪽에서 그 이벤트를 받아(on) 처리하는 구조입니다.
간단한 코드로 살펴보겠습니다.
1. React Native에서 WebView 설정 및 메시지 수신
import React, { useRef } from 'react';
import { WebView } from 'react-native-webview';
function MyWebComponent() {
const webviewRef = useRef(null);
// Web(React)로부터 메시지 수신
const handleOnMessage = (event) => {
const data = JSON.parse(event.nativeEvent.data);
// 수신한 데이터로 네이티브 로직 처리...
console.log('Received from web:', data);
};
return (
<WebView
ref={webviewRef}
source={{ uri: 'YOUR_WEB_URL' }}
onMessage={handleOnMessage}
/>
);
}
2. 양방향 메시지 전송
// [RN -> Web] WebView로 메시지 보내기
webviewRef.current.postMessage(JSON.stringify({ message: 'Hello from RN!' }));
// [Web -> RN] React Native로 메시지 보내기
// window.ReactNativeWebView 객체는 RN WebView 환경에서 자동으로 주입됩니다.
window.ReactNativeWebView.postMessage(JSON.stringify({ message: 'Hello from Web!' }));
이 방식은 간단한 통신에는 유용하지만, 프로젝트가 복잡해지면 두 가지 명확한 한계에 부딪혔습니다.
- 타입 안정성(Type-Safe) 부족: postMessage는 모든 데이터를 문자열(JSON.stringify)로 주고받습니다. 이로 인해 어떤 데이터를 어떤 형식으로 보내는지 양쪽(앱과 웹)에서 명시적으로 알기 어렵습니다. 만약 한쪽에서 데이터 구조를 변경하면, 다른 쪽에서는 런타임 에러가 발생하기 전까지 문제를 인지하기 힘듭니다.
- 선언적 프로그래밍의 부재: 이벤트를 수동으로 등록하고, 받은 데이터를 직접 파싱하는 방식은 명령형에 가깝습니다. 이는 선언적 프로그래밍을 추구하는 React의 철학과 다소 거리가 멀게 느껴졌습니다.
✅ 해결책: webview-bridge 라이브러리의 등장
이러한 문제를 해결하기 위해 저희는 webview-bridge 라이브러리를 도입했습니다.
이 라이브러리는 번거로운 postMessage와 onMessage 처리 로직을 내부적으로 추상화합니다. 개발자는 실제 함수나 변수를 선언하고 가져다 쓰는 것처럼 직관적으로 브릿지를 사용할 수 있습니다.
꿈꾸는 이상적인 아키텍처
webview-bridge를 통해 저희가 그리고자 했던 이상적인 모노레포 구조는 다음과 같습니다. 중앙에서 타입을 관리하여 앱과 웹 양쪽에서 동일한 타입 정의를 참조하게 만드는 것입니다.
/monorepo
├── /packages
│ └── /types
│ └── bridge.ts // 👈 브릿지가 참조할 모든 타입을 이곳에서 정의 (The Contract)
├── /apps
│ ├── /react-native
│ │ └── bridge.ts // (original-bridge 인스턴스 생성, packages/types 참조)
│ └── /react
│ └── bridge.ts // (linked-bridge 인스턴스 생성, packages/types 참조)
이렇게 구성하면 어떤 장점이 있을까요?
- 명확한 계약: packages/types/bridge.ts 파일이 앱과 웹 간의 통신 '계약서' 역할을 합니다.
- 타입 안정성: 양쪽 프로젝트가 동일한 타입을 바라보므로, 데이터 구조가 변경되면 컴파일 시점에 오류를 잡아낼 수 있습니다.
- 뛰어난 확장성: 새로운 통신 함수를 추가할 때, 중앙 타입 정의 파일에 한 줄 추가하고 각 프로젝트에서 구현만 하면 되므로 관리가 매우 용이합니다.
물론 현실적인 문제도 있었습니다. 저희는 프로젝트 초기 의존성 문제로 인해 현재는 packages의 타입을 웹 프로젝트에 복사하여 사용하고 있지만, 위 구조는 webview-bridge를 통해 달성하고자 하는 가장 이상적인 목표입니다.
결론
webview-bridge 라이브러리를 사용함으로써 팀원들과 협업할 때 브릿지 함수의 역할과 데이터 구조를 훨씬 명확하게 파악하고, 안전하게 기능을 확장할 수 있었습니다.
React Native와 WebView로 프로젝트를 진행하면서 타입 안정성과 유지보수성 때문에 고민해 본 개발자라면, 한 번쯤 사용해 보시길 추천합니다.
'개발' 카테고리의 다른 글
| 6주 만에 앱 출시, Axios Interceptor를 활용한 토큰 관리 (0) | 2025.09.13 |
|---|---|
| Ref를 이용해 전역 모달 시스템 구축하기 (0) | 2025.03.24 |
| TanStack의 QueryClient를 이용해 Cache 데이터 활용하기 (0) | 2025.03.23 |
| 재귀 호출을 통한 데이터 구조 변환 (0) | 2025.03.09 |