웹뷰 개발 과정에서 React Query로 상태 관리 다 했다 싶었는데, 네이티브 액티비티를 왔다 갔다 하다 보니 웹뷰 안에 있는 데이터가 서로 안 맞는 거예요.
"어? 분명 저장했는데 왜 뒤로가기 하면 초기값이지?" 싶은 순간, 멘붕 오셨다면 저랑 같은 경험 중이실 겁니다.
웹뷰(WebView) 기반의 하이브리드 앱을 개발하다 보면, React Query 같은 강력한 상태 관리 도구를 사용해도 네이티브 액티비티 간 데이터 동기화 문제가 발생하는 경우가 있습니다. 특히 하나의 네이티브 화면에서 웹뷰를 열고, 또 다른 네이티브 액티비티에서 같은 웹뷰를 띄웠을 때, 서로 최신 상태를 공유하지 못하는 것을 발견할 수 있습니다.
이 글에서는 왜 React Query만으로는 이 문제를 완전히 해결할 수 없는지, 그리고 웹뷰 간 상태 동기화를 안정적으로 구현하기 위한 방법에 대해 실제 사례를 바탕으로 정리해보았습니다. 하이브리드 앱 구조에서 데이터 일관성 문제로 고민 중이시라면 도움이 되실 거예요.
이 문제의 핵심은 웹뷰마다 따로따로 React 환경이 돌아간다는 거예요.
쉽게 말해, 브라우저에서 같은 웹사이트를 여러 탭에 띄운 것과 비슷해요. 일반적인 브라우저에서도 각 탭은 서로 상태를 공유하지 않죠.
예를 들어 현재 사용 중인 벨로그의 설정 페이지
에서 닉네임을 변경하더라도, 다른 탭에서 이미 접속 중인 작성글 페이지
에서는 제 닉네임이 변경되지 않습니다.
이와 동일하게 별다른 조치를 하지 않는다면 웹뷰에서 작성글 페이지 -> 설정 페이지로 이동한 후, 닉네임 설정을 바꾼다면 뒤로가기 시에 돌아간 작성글 페이지에서 닉네임이 변경되지 않은 것을 볼 수 있습니다.
분명 설정 페이지에서 닉네임 변경을 확인했고, 두 페이지에서 닉네임이 동일한 Query 데이터를 사용한다고 해도 말이죠!
React Query도 결국 웹뷰 내부의 JavaScript 세계 안에서만 작동하기 때문에, 네이티브 액티비티를 새로 띄우면 완전히 새로운 React 인스턴스가 뜨는 거예요.
이 글에서는 왜 이런 문제가 생기는지, 그리고 이걸 어떻게 해결할 수 있을지 직접 부딪히며 찾아낸 방법을 공유해보려 합니다.
React Query를 일반적인 브라우저 환경에서 사용할 때는, 우리가 페이지를 이동하더라도 동일한 쿼리의 데이터가 그대로 유지되는 걸 자주 경험합니다.
예를 들어, /home
페이지에서 유저 정보를 불러오고 /profile
페이지로 이동해도 다시 요청하지 않고 기존 데이터를 보여주죠.
이게 가능한 이유는 바로 React Query의 전역 상태 관리 방식 덕분이에요.
React Query는 내부적으로 QueryClient라는 전역 객체를 사용해서 데이터를 캐싱하고 관리해요. 우리가 보통 QueryClientProvider로 앱 전체를 감싸는 것도 이걸 모든 컴포넌트가 공유할 수 있게 하기 위해서죠.
브라우저에서 SPA(Single Page Application)로 작동하는 경우, 페이지 이동이 실제로는 리액트 컴포넌트의 전환일 뿐이기 때문에, 메모리 상의 QueryClient 인스턴스는 계속 살아 있어요.
그래서 한 번 받아온 데이터는, 설정된 staleTime이나 cacheTime이 만료되지 않는 한, 다른 페이지에서도 그대로 접근할 수 있는 거예요.
즉, 브라우저에서는 하나의 React 앱(=하나의 JS 환경) 안에서 모든 페이지가 돌아가기 때문에, 전역 상태도 하나!
하지만… 이게 각각의 네이티브 액티비티에서 열리는 웹뷰에서는 다르게 동작합니다. 😅
(이제 이 차이점이 왜 중요한지 더 잘 느껴지시죠?)
여기서 잠깐! 혹시 "네이티브 액티비티(Activity)" 라는 말이 낯설게 느껴졌다면 너무 걱정하지 마세요. 프론트엔드 개발자라면 이 개념을 처음 접하는 게 자연스러운 일이니까요.
간단히 말해서, "네이티브 액티비티"는 안드로이드에서 하나의 화면(혹은 페이지)을 구성하는 기본 단위입니다.
React에서 말하는 "페이지 컴포넌트" 같은 느낌이긴 한데, 훨씬 더 독립적인 실행 단위에 가까워요.
또 한 가지 중요한 점은, 안드로이드의 액티비티 구조는 "스택(stack)"처럼 쌓이는 방식이라는 거예요.
예를 들어 네이티브 앱에서 다음과 같은 흐름이 있다고 해볼게요:
이때 A와 B는 서로 완전히 다른 액티비티이자, 서로 다른 React 앱 인스턴스예요.
즉, 웹에서는 "라우팅만 바뀌었을 뿐 같은 앱 안에서 계속 작동"하지만, 네이티브에서는 아예 새로운 환경이 하나 더 생긴 것에 가까워요.
그래서 B에서 어떤 데이터를 변경했더라도, 다시 A로 돌아오면 A는 그걸 모른 채 예전 상태로 그대로 남아있는 거죠.
항목 | 브라우저 (SPA) | 네이티브 앱 (액티비티 + 웹뷰) |
---|---|---|
실행 환경 | 하나의 React 앱 인스턴스 | 각 액티비티마다 별도의 React 앱 인스턴스 |
JavaScript 엔진 | 단일 엔진 (동일한 JS 메모리 공간) | 각 웹뷰마다 독립된 JS 엔진 |
React Query 캐시 공유 | 공유됨 (전역 QueryClient) | 공유되지 않음 (각 웹뷰마다 별도 QueryClient) |
페이지 전환 방식 | 라우팅으로 이동 (react-router 등) | 새로운 액티비티 생성 후 웹뷰 로드 |
상태 유지 | 기본적으로 유지됨 (메모리 안에서) | 유지되지 않음 (새로운 앱 환경 시작) |
예시 | 브라우저에서 /profile → /settings 이동 시 상태 유지 | 네이티브에서 웹뷰A → 웹뷰B 이동 시 상태 초기화됨 |
상태 동기화 필요 여부 | 필요 없음 (자동 공유) | 필요함 (직접 구현 필요) |
React Query 같은 상태 관리 라이브러리는 "하나의 앱 환경 안에서 작동할 때" 전역 상태 공유가 가능합니다.
하지만 네이티브 앱에서는 액티비티마다 웹뷰가 분리되어 있고, 각 웹뷰는 자체적인 JS 엔진에서 React 앱을 새로 시작하기 때문에,
- React Query의 캐시도 공유되지 않고,
- 메모리 상태도 연결되지 않으며,
- 브라우저 탭을 새로 여는 것처럼 완전히 분리된 환경이 됩니다.
즉, React Query는 잘 동작하고 있지만 "웹뷰끼리"는 서로 모르는 사이인 것이죠.
그래서 데이터 동기화를 위해서는 여러 방법을 고려해 볼 수 있습니다.
웹뷰(WebView)끼리 상태를 동기화하려면, 서로의 데이터를 실시간으로 주고받을 수 있어야 합니다. 그런데 앞서 설명했듯이, 각 웹뷰는 서로 다른 React 앱 인스턴스이기 때문에 JavaScript 세계만으로는 서로 직접 접근할 수 없어요.
이럴 때 등장하는 것이 바로 "네이티브 브릿지(Native Bridge)"입니다.
브릿지란 웹(JavaScript)와 네이티브(Android/iOS) 사이에서 데이터를 주고받게 해주는 통신 통로입니다.
하이브리드 앱에서는 웹뷰 안의 JS 코드가 네이티브 코드에 메시지를 보내고, 반대로 네이티브 코드도 웹뷰 안의 JS 함수를 실행시킬 수 있어요.
즉, 웹뷰 A에서 JS → 네이티브로 메시지를 보내고, 네이티브가 이를 받아서 웹뷰 B의 JS에 다시 전달하는 방식으로 웹뷰 간 간접 통신이 가능해지는 거죠.
웹뷰 A에서 window.ReactNativeWebView.postMessage('nickname_updated')
실행
네이티브(Android/iOS)에서 이 메시지를 받아 처리
네이티브는 웹뷰 B에 있는 JS 함수 (예: window.syncNickname()
)를 호출
웹뷰 B는 전달받은 메시지를 바탕으로 쿼리를 리패치하거나 상태를 갱신
장점 | 단점 |
---|---|
웹뷰끼리 통신 가능: 서로 다른 React 앱 인스턴스 간에도 데이터를 주고받을 수 있음 | 구현 복잡도: JS ↔ 네이티브 ↔ JS 구조로 중간 과정이 많고 디버깅이 어려움 |
유연한 제어: 네이티브에서 원하는 조건과 타이밍에 맞춰 통신을 제어할 수 있음 | 플랫폼 의존성: Android, iOS 각각에 브릿지 코드 구현 필요 |
확장성: 상태 동기화 외에도 다양한 기능에 활용 가능 (예: 푸시 알림, 로그인 정보 공유) | 브릿지가 없는 환경에서 대처 어려움: 브릿지가 없는 앱 버전에서 네이티브와의 통신이 불가능하거나 매우 제한적 |
웹뷰 간 상태 동기화를 위해 두 번째로 고려할 수 있는 방법은 storage 이벤트를 활용하는 것입니다.
이 방법은 웹 브라우저에서 제공하는 기본 기능인 localStorage나 sessionStorage를 이용한 방법입니다. 특히 웹 환경에서 발생하는 storage 이벤트는 같은 도메인 내의 다른 탭이나 다른 웹뷰에 변화를 알려줄 수 있어, 웹뷰 간 데이터 동기화에 유용합니다.
storage 이벤트는 웹 브라우저의
localStorage
나sessionStorage
에 값이 저장될 때 발생하는 이벤트입니다. 이 이벤트는 같은 도메인을 사용하는 다른 탭이나 다른 웹뷰에서 감지할 수 있습니다.
예를 들어, 웹뷰 A에서 localStorage.setItem('nickname', 'newNick')
을 실행하면, 동일한 도메인에 있는 다른 웹뷰 B에서 이를 storage 이벤트로 감지하고 해당 데이터를 처리할 수 있습니다!
웹뷰 A에서 localStorage.setItem('nickname', 'newNick') 실행
storage 이벤트가 웹뷰 B에서 발생
웹뷰 B는 이벤트를 받아 nickname 값을 읽고 상태를 갱신
웹뷰 B에서 데이터 변경이 동기화됨
장점 | 단점 |
---|---|
웹뷰끼리 통신 가능: 서로 다른 React 앱 인스턴스 간에도 데이터를 주고받을 수 있음 | 구현 복잡도: JS ↔ 네이티브 ↔ JS 구조로 중간 과정이 많고 디버깅이 어려움 |
유연한 제어: 네이티브에서 원하는 조건과 타이밍에 맞춰 통신을 제어할 수 있음 | 플랫폼 의존성: Android, iOS 각각에 브릿지 코드 구현 필요 |
확장성: 상태 동기화 외에도 다양한 기능에 활용 가능 (예: 푸시 알림, 로그인 정보 공유) | 비동기 처리 주의: 통신 타이밍에 이슈가 발생할 수 있어, 이벤트 순서 관리 필요 |
storage 이벤트는 간단하고 효율적인 방법으로 웹뷰 간 상태 동기화가 가능합니다. 특히 복잡한 네이티브 브릿지 작업 없이도 여러 웹뷰가 상태를 동기화할 수 있는 장점이 있지만, 동일 도메인에 한정된다는 점과 속도에서 다소 제한적일 수 있다는 점을 고려해야 합니다.
세 번째 방법은 Broadcast Channel API를 사용하는 것입니다. 이 API는 여러 탭이나 웹뷰 간에 실시간으로 메시지를 주고받을 수 있는 기능을 제공합니다. Broadcast Channel API는 특히 동일한 도메인에서 여러 브라우저 창 또는 웹뷰가 있을 때 유용하게 사용될 수 있습니다. 이 방법을 사용하면 웹뷰 간 상태 동기화를 쉽게 구현할 수 있습니다.
Broadcast Channel API는 브라우저 탭 간의 메시지 전송을 지원하는 API입니다. 이를 통해 여러 웹뷰나 브라우저 탭 간에 비동기적으로 메시지를 전달할 수 있습니다.
예를 들어, 한 웹뷰에서 데이터를 업데이트한 후 다른 웹뷰에 이를 전달하고, 해당 웹뷰는 업데이트된 데이터를 처리할 수 있습니다.
핵심: 메시지가 브라우저의 여러 컨텍스트 간에 실시간으로 전파됩니다.
웹뷰 A에서 const channel = new BroadcastChannel('my_channel'); channel.postMessage('nickname_updated');
실행
웹뷰 B에서 같은 채널(my_channel)을 통해 메시지를 수신하고, 이를 처리
웹뷰 B에서 받은 메시지에 따라 상태를 갱신하거나 데이터를 리패치
장점 | 단점 |
---|---|
실시간 동기화 가능: 변경 사항이 즉시 모든 탭/웹뷰로 전파됨 | 동일 도메인에 한정: 같은 도메인에서만 동작 (서브도메인도 별개) |
간단한 구현: 브라우저에서 기본 제공되므로 별도의 라이브러리나 복잡한 설정 필요 없음 | 브라우저 지원 제한: 일부 오래된 브라우저에서는 지원되지 않음 |
비동기 메시지 처리: 이벤트 기반으로 처리되어, 메시지 전송과 받기가 비동기적으로 이루어짐 | 리소스 소모: 너무 많은 채널을 사용하거나 메시지를 자주 보내면 성능에 영향을 미칠 수 있음 |
Broadcast Channel API는 간단하고 실시간으로 상태를 동기화할 수 있는 매우 유용한 방법입니다. 웹뷰 간 메시지 전송을 위한 별도의 복잡한 설정 없이, 기본적으로 제공되는 API를 통해 쉽게 구현할 수 있습니다.
하지만 동일 도메인에서만 사용 가능하며, 일부 브라우저에서는 지원되지 않는 단점도 있으므로, 이를 사용할 때는 브라우저 호환성을 충분히 고려해야 합니다.
아래 표는 Storage 이벤트와 Broadcast Channel API를 비교한 내용입니다. 두 방법은 웹뷰 간 데이터 동기화를 가능하게 하지만, 각각의 장단점과 호환성에 차이가 있습니다.
특징 | Storage 이벤트 | Broadcast Channel API |
---|---|---|
동기화 방식 | localStorage 나 sessionStorage 에 값이 변경될 때 발생하는 이벤트로 동기화 | 실시간으로 메시지를 모든 탭/웹뷰에 전파하는 이벤트 기반 동기화 |
실시간 동기화 | 동기화는 이벤트 발생 후 처리되므로 실시간성이 다소 떨어짐 | 즉각적인 동기화가 가능하며, 거의 실시간으로 처리됨 |
호환성 | Safari를 포함한 대부분의 브라우저에서 지원 | Safari에서는 15.4 이상 버전에서만 지원 |
성능 | localStorage 크기 제한 및 동기화 속도에 따라 성능 이슈가 있을 수 있음 | 여러 채널을 사용하거나 메시지를 자주 보내면 성능에 영향을 줄 수 있음 |
리소스 관리 | 별도의 리소스 관리 필요 없으며 localStorage 에서 자동 관리됨 | 채널을 열어놓고 메시지를 수신할 때 리소스를 소모하므로, 불필요한 채널 관리가 필요함 |
Storage 이벤트는 거의 모든 최신 브라우저에서 잘 지원되며, Safari에서도 제대로 작동합니다.
하지만 localStorage와 같은 저장소에 저장할 수 있는 데이터 크기에는 제한이 있기 때문에, 큰 데이터를 저장하거나 자주 변경되는 데이터를 동기화할 때는 비효율적일 수 있습니다.
Broadcast Channel API는 Safari의 경우, 버전 15.4 이상에서만 지원됩니다.
이 API는 Chrome, Firefox, Edge 등에서는 잘 작동하지만, 구형 브라우저나 일부 Safari 버전에서는 지원되지 않기 때문에, 브라우저 호환성을 고려하여 사용할 필요가 있습니다.
이로 인해 브라우저 호환성 문제가 있을 수 있어, 대체 방법을 고려하거나 브라우저 기능을 감지하고 대응하는 코드를 작성하는 것이 중요합니다.
Storage 이벤트는 간단하고 기본적인 방법으로, 브라우저 지원이 좋고 특히 Safari에서도 잘 지원됩니다. 그러나 실시간 동기화나 대용량 데이터 처리에는 제약이 있을 수 있습니다.
Broadcast Channel API는 실시간 동기화를 제공하는 매우 유용한 방법이지만, Safari와 같은 일부 브라우저에서 지원되지 않으며, 브라우저 호환성을 충분히 고려해야 합니다.
저의 경우에는 앱이 ios 버전 15.0부터 지원하기 때문에 Storage 이벤트를 활용하는 방법을 택하게 되었습니다. Storage 이벤트를 활용하며 유지보수를 위해 아래와 같이 커스텀 훅과 constant로 웹뷰에서 데이터 동기화를 위한 로직을 분리해보았어요.
향후에 기능이 늘어나면 더 많은 데이터를 Storage 이벤트를 활용하여 QueryClient 캐시를 동기화할 가능성이 있다고 보고, 먼저 관련된 key를 constant로 관리하였습니다.
로컬 스토리지에 상태를 저장하기 위한 useLocalStoargeState
커스텀 훅을 만들었습니다.
해당 훅은 아래와 같이 사용하고자 하였어요. 설정 페이지 내부의 닉네임 form에서 닉네임 변경 시에 localStorage
의 changeNickname
값을 변경했어요.
또한 Storage 이벤트 감지 시 callback 함수를 실행시키는 useLocalStorageEffect
커스텀 훅을 만들었습니다.
해당 훅을 활용해 설정 페이지 액티비티 외의 작성글 페이지 액티비티에서도 QueryClient invalidate를 실행했어요.
위와 같은 과정을 통해 기존의 문제를 해결했어요.
작성글 페이지 -> 설정 페이지로 이동한 후, 닉네임 설정을 바꾼다면... 뒤로가기로 돌아간 작성글 페이지에서도 닉네임이 변경된 것을 확인할 수 있어요!
브릿지와 Storage 이벤트 외에 다른 방법은 없을까 고민하다 Broadcast Channel API를 발견하여 해당 포스트를 작성하게 되었습니다. 이번 프로젝트에서는 활용이 어려워서 아쉽지만 이후 다른 프로젝트에서 유용하게 사용할 수 있을 것 같아 기대가 됩니다!
Tanstack Query에서도 하이브리드 앱의 웹뷰에서 QueryClient 공유가 어렵다는 것을 인지하고 broadcastQueryClient를 실험 단계에서 개발 중인 것으로 보입니다. Tanstack에서도 외부 라이브러리를 통해 Broadcast Channel API를 활용한 것을 발견할 수 있었어요. Tanstack의 Broadcast Channel API 활용 방법은 이곳에서 확인하실 수 있습니다.