From fe139d77195217abf5f0d9ba282bd472d06fbdd3 Mon Sep 17 00:00:00 2001
From: gain <163835628+leegain1@users.noreply.github.com>
Date: Wed, 23 Sep 2026 02:10:25 +0900
Subject: [PATCH] =?UTF-8?q?feat:=20=EC=84=B8=EC=9D=B4=ED=94=84=EC=97=90?=
=?UTF-8?q?=EC=96=B4=EB=A6=AC=EC=96=B4=20=EC=8A=A4=ED=8A=B8=EB=A6=BD=20?=
=?UTF-8?q?=EC=83=89=EC=9D=84=20=EC=9B=B9=EC=97=90=EC=84=9C=20=EB=B0=9B?=
=?UTF-8?q?=EC=95=84=20=EC=B9=A0=ED=95=98=EA=B8=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
스트립 색을 tailwind.config.js에 hex로 복사해 두고 있었다. 웹의 모든 화면이
background-alternative 하나였던 시절의 전제라, 화면 배경이 라우트 옵션이 된
뒤로는 흰 배경 화면 6개에서 노치 아래에 회색 띠가 보인다.
앱은 WebView 안의 DOM을 볼 수 없으니 웹이 알려주는 수밖에 없다. onMessage로
safeAreaColors 메시지를 받아 스트립 색을 state로 들고 간다. 웹이 알려주기
전과 이 기능이 없는 옛 웹을 위해 기본값을 남기고, onMessage는 아무 스크립트나
보낼 수 있으므로 형태와 색 표기를 검증한다.
색을 앱이 들고 있지 않게 되어 tailwind.config.js의 web-background-* 항목은
지웠다.
---
src/features/webview/WebViewScreen.tsx | 28 +++++++++---
src/features/webview/safeAreaColors.ts | 60 ++++++++++++++++++++++++++
tailwind.config.js | 10 +----
3 files changed, 84 insertions(+), 14 deletions(-)
create mode 100644 src/features/webview/safeAreaColors.ts
diff --git a/src/features/webview/WebViewScreen.tsx b/src/features/webview/WebViewScreen.tsx
index 342e7f3..429ceee 100644
--- a/src/features/webview/WebViewScreen.tsx
+++ b/src/features/webview/WebViewScreen.tsx
@@ -2,10 +2,14 @@ import * as WebBrowser from "expo-web-browser";
import { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, BackHandler, Linking, Platform, StyleSheet, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { WebView, type WebViewNavigation } from "react-native-webview";
+import { WebView, type WebViewMessageEvent, type WebViewNavigation } from "react-native-webview";
import type { ShouldStartLoadRequest } from "react-native-webview/lib/WebViewTypes";
import { WEB_URL } from "@/constants/config";
import WebViewMessage from "@/features/webview/components/WebViewMessage";
+import {
+ DEFAULT_SAFE_AREA_COLORS,
+ parseSafeAreaColorsMessage,
+} from "@/features/webview/safeAreaColors";
import { isSameOrigin } from "@/utils/url";
// WebView는 서드파티 컴포넌트라 NativeWind의 className이 적용되지 않는다. style로 채운다.
@@ -19,6 +23,7 @@ export default function WebViewScreen() {
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [canGoBack, setCanGoBack] = useState(false);
+ const [safeAreaColors, setSafeAreaColors] = useState(DEFAULT_SAFE_AREA_COLORS);
// 안드로이드 하드웨어 백 버튼은 기본적으로 앱을 종료한다. 웹 히스토리가 남아 있으면 뒤로 보낸다.
useEffect(() => {
@@ -40,6 +45,9 @@ export default function WebViewScreen() {
const handleRetry = useCallback(() => {
setHasError(false);
setIsLoading(true);
+ // 다시 띄우는 웹이 색을 알려줄 때까지는 기본값으로 돌아간다 — 실패 직전 화면 색이 남으면
+ // 엉뚱한 화면 위에 그 색 스트립이 얹힌다.
+ setSafeAreaColors(DEFAULT_SAFE_AREA_COLORS);
webViewRef.current?.reload();
}, []);
@@ -47,6 +55,15 @@ export default function WebViewScreen() {
setCanGoBack(navigation.canGoBack);
}, []);
+ // 웹은 화면 배경이 바뀔 때마다 세이프에어리어 스트립 색을 보낸다. 아는 메시지만 반영한다.
+ const handleMessage = useCallback((event: WebViewMessageEvent) => {
+ const colors = parseSafeAreaColorsMessage(event.nativeEvent.data);
+
+ if (colors) {
+ setSafeAreaColors(colors);
+ }
+ }, []);
+
// 서비스 바깥 주소는 웹뷰 안에서 열지 않고 시스템 브라우저·기본 앱으로 넘긴다.
const handleShouldStartLoad = useCallback((request: ShouldStartLoadRequest) => {
const { url } = request;
@@ -89,10 +106,10 @@ export default function WebViewScreen() {
return (
// 세이프에어리어는 네이티브가 담당하고 웹은 주어진 영역을 100%로 채우기만 한다.
// 위아래 스트립은 맞닿는 웹 화면과 같은 색으로 칠해야 경계선이 보이지 않는데,
- // 위는 웹 본문 배경 / 아래는 Bottom Nav 배경이라 색이 서로 달라
- // SafeAreaView 하나로는 칠할 수 없다. 인셋을 직접 재서 나눠 칠한다.
+ // 위아래 색이 서로 다르고 화면마다도 달라서 SafeAreaView 하나로는 칠할 수 없다.
+ // 인셋을 직접 재서 나눠 칠하고, 색은 웹이 알려준 값을 쓴다(safeAreaColors.ts 참고).
-
+
setHasError(true)}
@@ -104,6 +121,7 @@ export default function WebViewScreen() {
}}
onLoadEnd={() => setIsLoading(false)}
onLoadStart={() => setIsLoading(true)}
+ onMessage={handleMessage}
onNavigationStateChange={handleNavigationStateChange}
onShouldStartLoadWithRequest={handleShouldStartLoad}
ref={webViewRef}
@@ -116,7 +134,7 @@ export default function WebViewScreen() {
)}
-
+
);
}
diff --git a/src/features/webview/safeAreaColors.ts b/src/features/webview/safeAreaColors.ts
new file mode 100644
index 0000000..cb10f35
--- /dev/null
+++ b/src/features/webview/safeAreaColors.ts
@@ -0,0 +1,60 @@
+/**
+ * 세이프에어리어 스트립 색 — stream-client-web이 postMessage로 알려준다.
+ *
+ * 앱은 WebView 안의 DOM을 볼 수 없다(별도 저장소·별도 배포라 앱 안에 웹 코드가 없고,
+ * WebView는 픽셀만 보여준다). 그래서 어떤 화면이 무슨 배경인지는 웹이 말해주는 수밖에 없다.
+ *
+ * 예전에는 이 색을 tailwind.config.js에 hex로 복사해 뒀는데, 웹에서 화면마다 배경이
+ * 달라지면서(web#50) 흰 배경 화면에서 회색 띠가 보였다. 웹만 배포돼도 조용히 어긋나는
+ * 구조라 값을 앱이 들고 있지 않도록 바꿨다.
+ *
+ * 송신부: stream-client-web의 `src/components/ui/useNativeSafeAreaColors.ts`.
+ * 표식 문자열과 필드 이름은 양쪽이 맞춰야 한다.
+ */
+
+const SAFE_AREA_COLORS_MESSAGE_TYPE = "safeAreaColors";
+
+export interface SafeAreaColors {
+ bottom: string;
+ top: string;
+}
+
+// 웹이 색을 알려주기 전(로딩 중)과, 이 기능이 없는 옛 웹이 물렸을 때 쓰는 값.
+// stream-client-web의 WDS 배경 토큰 기본값이다 — 어디까지나 폴백이고, 웹이 말해주면 덮인다.
+export const DEFAULT_SAFE_AREA_COLORS: SafeAreaColors = {
+ bottom: "#FFFFFF",
+ top: "#F7F7F8",
+};
+
+// onMessage는 WebView 안의 어떤 스크립트든 보낼 수 있다. 색 자리에 이상한 값이 들어와
+// 스트립이 검게 칠해지는 일이 없도록 표기를 확인하고 통과시킨다.
+const CSS_COLOR_PATTERN = /^#[0-9a-f]{3,8}$|^rgba?\([\d\s.,%/]+\)$/i;
+
+function isColor(value: unknown): value is string {
+ return typeof value === "string" && CSS_COLOR_PATTERN.test(value);
+}
+
+/** 웹이 보낸 원문에서 스트립 색을 꺼낸다. 이 앱이 아는 메시지가 아니면 `null`. */
+export function parseSafeAreaColorsMessage(data: string): SafeAreaColors | null {
+ let payload: unknown;
+
+ try {
+ payload = JSON.parse(data);
+ } catch {
+ // JSON이 아니면 우리 메시지가 아니다. 웹이 다른 용도로 postMessage를 쓸 수 있으므로
+ // 오류로 다루지 않고 흘려보낸다.
+ return null;
+ }
+
+ if (typeof payload !== "object" || payload === null) {
+ return null;
+ }
+
+ const { bottom, top, type } = payload as Record;
+
+ if (type !== SAFE_AREA_COLORS_MESSAGE_TYPE || !isColor(top) || !isColor(bottom)) {
+ return null;
+ }
+
+ return { bottom, top };
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 58480d0..407a91f 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -3,15 +3,7 @@ module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
theme: {
- extend: {
- colors: {
- // stream-client-web이 쓰는 WDS 배경 토큰 값을 그대로 옮긴 것.
- // 세이프에어리어 스트립을 웹 화면과 같은 색으로 잇는 용도로만 쓴다.
- // 웹에서 토큰 값이 바뀌면 여기도 함께 맞춰야 한다.
- "web-background-alternative": "#F7F7F8",
- "web-background-normal": "#FFFFFF",
- },
- },
+ extend: {},
},
plugins: [],
};