From dadf13e2980bb53fcccbf19e07a4347fe4911e83 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Thu, 17 Sep 2026 14:50:45 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20=ED=98=B8=EC=8A=A4=ED=8A=B8=EA=B0=80?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=ED=95=98=EC=A7=80=20=EB=AA=BB=ED=95=9C=20?= =?UTF-8?q?=EC=9B=B9=EB=B7=B0=20=EB=A9=94=EC=8B=9C=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EA=B4=80=EC=B8=A1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 같은 웹 화면이 홈 웹셸, ClubDetailScreen, webview/[slug] 세 WebView 안에서 도는데 메시지 처리 구현은 두 벌이다. 공용 훅과 홈의 인라인 switch가 서로를 모른 채 각자 자라서, 호스트마다 아는 메시지 목록이 다르다. 그 자체보다 나쁜 건 어긋남이 조용하다는 점이다. 공용 훅의 default는 console.warn만 남기고 홈의 switch는 default조차 없이 바깥 catch가 삼킨다. 그래서 웹이 6월(#1665)부터 보내던 SUBSCRIBE_TOGGLE을 상세 컨테이너가 버리고 있다는 사실을 12월에 사용자 제보로 알았다(ffb53d0). - 각 호스트 switch의 default에서 reportUnknownBridgeMessage를 부른다. dev는 console.warn, prod는 Mixpanel Bridge UnknownMessage 이벤트 - 판별 기준을 "알려진 타입 집합과 대조"가 아니라 "이 호스트의 switch가 잡지 못함"으로 뒀다. 홈만 처리하는 REQUEST_APP_VERSION처럼 호스트별로 목록이 다른 지금 구조에서, 집합 대조는 정상 처리된 메시지를 오탐한다 - 서드파티가 쏜 비정형 메시지를 거르려고 문자열 type만 취급한다. JSON 파싱 실패는 대상이 아니다. 기존 로그로 이미 드러나고, 비JSON을 쏘는 스크립트가 있으면 노이즈가 된다 - useWebViewMessageHandler의 host를 필수 옵션으로 뒀다. 웹뷰 호스트가 늘어날 때 타입 에러로 막혀 이름을 붙이게 된다 리포터는 void를 반환하고 예외를 밖으로 내보내지 않는다. 관측이 실패해도 메시지 처리는 계속돼야 한다. 기존 9종의 처리 경로는 건드리지 않았다. 갈라진 구현을 한 벌로 합치는 일은 이 PR의 범위가 아니다. 홈에만 있는 REQUEST_APP_VERSION, 호스트마다 다른 OPEN_EXTERNAL_URL 구현, NAVIGATE_BACK의 서로 다른 처리를 훅으로 흡수해야 해서 규모가 다르다. 그 전까지 이 관측 장치가 어긋남을 드러낸다. Co-Authored-By: Claude Opus 5 (1M context) --- app/webview/[slug].tsx | 1 + constants/eventname.ts | 7 ++++++- hooks/use-webview-message-handler.ts | 7 ++++++- ui/club-detail/club-detail-screen.tsx | 1 + ui/home/home-webview-screen.tsx | 5 ++++- utils/webview.ts | 23 +++++++++++++++++++++++ 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/app/webview/[slug].tsx b/app/webview/[slug].tsx index 5b1e7f0..8561912 100644 --- a/app/webview/[slug].tsx +++ b/app/webview/[slug].tsx @@ -143,6 +143,7 @@ export default function WebViewScreen() { }; const { handleMessage } = useWebViewMessageHandler({ + host: 'webview_slug', onNavigateBack: handleBack, onNavigateWebview: handleNavigateWebview, }); diff --git a/constants/eventname.ts b/constants/eventname.ts index ae8a21b..668ec7a 100644 --- a/constants/eventname.ts +++ b/constants/eventname.ts @@ -18,4 +18,9 @@ export const PAGE_VIEW_EVENT = { MAIN_PAGE: 'MainPage', SUBSCRIBE_PAGE: 'SubscribePage', MORE_PAGE: 'MorePage', -} \ No newline at end of file +} + +// 앱 자체 진단용 — 사용자 행동이 아니라 웹↔앱 브리지 상태를 관측한다. +export const DIAGNOSTIC_EVENT = { + BRIDGE_UNKNOWN_MESSAGE: 'Bridge UnknownMessage', +} diff --git a/hooks/use-webview-message-handler.ts b/hooks/use-webview-message-handler.ts index 4b7143f..19ff45b 100644 --- a/hooks/use-webview-message-handler.ts +++ b/hooks/use-webview-message-handler.ts @@ -1,8 +1,11 @@ import { WebViewMessage, WebViewMessageEvent, WebViewMessageTypes } from '@/types/webview-message.types'; +import { reportUnknownBridgeMessage } from '@/utils/webview'; import { useCallback } from 'react'; import { Linking } from 'react-native'; interface UseWebViewMessageHandlerOptions { + /** 처리되지 않은 메시지를 어느 화면이 받았는지 구분하기 위한 이름 */ + host: string; // 뒤로가기 요청 시 호출 onNavigateBack?: () => void; // 웹뷰 내 화면 이동 요청 시 호출 @@ -21,6 +24,7 @@ interface UseWebViewMessageHandlerOptions { // WebView 메시지를 처리하는 Hook export const useWebViewMessageHandler = ({ + host, onNavigateBack, onNavigateWebview, onSubscribe, @@ -83,12 +87,13 @@ export const useWebViewMessageHandler = ({ ); break; default: - console.warn('[WebViewHandler] 알 수 없는 메시지 타입:', message); + reportUnknownBridgeMessage((message as { type?: unknown }).type, host); } } catch (error) { console.error('[WebViewHandler] 메시지 파싱 오류:', error); } }, [ + host, onNavigateBack, onNavigateWebview, onSubscribe, diff --git a/ui/club-detail/club-detail-screen.tsx b/ui/club-detail/club-detail-screen.tsx index 5e6893f..f3adf0f 100644 --- a/ui/club-detail/club-detail-screen.tsx +++ b/ui/club-detail/club-detail-screen.tsx @@ -124,6 +124,7 @@ export default function ClubWebViewScreen() { // WebView 메시지 핸들러 const { handleMessage } = useWebViewMessageHandler({ + host: 'club_detail', onNavigateBack: handleBack, onSubscribe: async (targetId: string, clubName?: string) => { trackEvent(USER_EVENT.SUBSCRIBE_BUTTON_CLICKED, { diff --git a/ui/home/home-webview-screen.tsx b/ui/home/home-webview-screen.tsx index ae70c3f..392171b 100644 --- a/ui/home/home-webview-screen.tsx +++ b/ui/home/home-webview-screen.tsx @@ -2,7 +2,7 @@ import { useHomeWebViewPreloadContext } from '@/contexts/home-webview-preload-co import { useMixpanelContext } from '@/contexts/mixpanel-context'; import { useSubscribedClubsContext } from '@/contexts/subscribed-clubs-context'; import { ensureAccessToken } from '@/services/auth-token.service'; -import { appendSessionId, buildStudentTokenInjection, getWebViewUserAgent, isWebViewOrigin } from '@/utils/webview'; +import { appendSessionId, buildStudentTokenInjection, getWebViewUserAgent, isWebViewOrigin, reportUnknownBridgeMessage } from '@/utils/webview'; import Constants from 'expo-constants'; import { useRouter } from 'expo-router'; import * as WebBrowser from 'expo-web-browser'; @@ -145,6 +145,9 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) { payload: { version: Constants.expoConfig?.version ?? 'unknown' }, }); break; + + default: + reportUnknownBridgeMessage(type, 'home'); } } catch { // 파싱 실패 무시 diff --git a/utils/webview.ts b/utils/webview.ts index 35cb69b..e50721b 100644 --- a/utils/webview.ts +++ b/utils/webview.ts @@ -1,3 +1,5 @@ +import { DIAGNOSTIC_EVENT } from '@/constants/eventname'; +import { getMixpanel } from '@/utils/mixpanel'; import Constants from 'expo-constants'; import { Platform } from 'react-native'; @@ -53,3 +55,24 @@ export const buildStudentTokenInjection = ( window.__MOADONG_STUDENT_TOKEN__ = ${JSON.stringify(token)}; })(); true;`; }; + +// 웹이 앱보다 앞서 나가 새 메시지를 추가했는데 호스트가 그걸 모르는 상황을 관측한다. +// 각 호스트 switch의 default에서만 부르므로, 그 호스트가 실제로 처리하지 못한 것만 잡힌다. +// 리포팅 실패가 메시지 처리를 막아선 안 되므로 예외를 밖으로 내보내지 않는다. +export const reportUnknownBridgeMessage = (type: unknown, host: string): void => { + // 서드파티가 쏜 비정형 메시지를 걸러내기 위해 문자열 type만 취급한다. + if (typeof type !== 'string') return; + + console.warn(`[WebViewBridge] 처리되지 않은 메시지: ${type} (host: ${host})`); + + getMixpanel() + .then(mixpanel => + mixpanel?.track(DIAGNOSTIC_EVENT.BRIDGE_UNKNOWN_MESSAGE, { + message_type: type, + host, + app_version: Constants.expoConfig?.version || '1.0.0', + platform: Platform.OS, + }), + ) + .catch(err => console.error('[WebViewBridge] 리포팅 실패:', err)); +};