사이드 프로젝트 개발 과정 - (react-native <-> webview 통신 환경 구성)

knh6269·2024년 6월 6일
6

ootd.zip

목록 보기
9/16
post-thumbnail

도입

react-native 개발자를 알아보려고 하다가 둘다 할 줄 아는 개발자 멋지지않을까 싶어서 직접 구현해보기로 했다.
reactreact-native둘다 문법이 비슷해 생각보다 힘들지는 않았다. ootdzip은 갤러리 선택 기능 등 native 기능을 사용해야하는 경우가 있다. 이럴때 webview에서 react-native로 메세지를 보낼수가 있는데 이때 payload를 같이 보내 원하는 동작을 수행한다. 오늘은 이 통신과정을 구현한 경험을 작성해보겠다.


message 통신이 이루어 지는 과정

react-native -> webview

react-native

<WebView
    ref={webViewRef}
    originWhitelist={["*"]}
    source={{ uri: "https://ootdzip.com" }}
    onMessage={recieveMessage}
/>

export const sendMessage = ({ webViewRef, type, payload }: SendMessage) => {
  if (webViewRef.current) {
    webViewRef?.current?.postMessage(JSON.stringify({ type, payload }));
  }
};

sendMessage({
  webViewRef,
  type: parseData.type,
  payload: response.result.imageUrls,
});

webview

export const getReactNativeMessage = () => {
  const listener = (event: WebViewMessageEvent) => {
    const parsedData = JSON.parse(event.data);  
  };

  if (window.ReactNativeWebView) {
    //android
    document.addEventListener('message', listener);

    //ios
    window.addEventListener('message', listener);
  }
};

webview -> react-native

webview

export const sendReactNativeMessage = ({ type, payload }: Message) => {
  if (window.ReactNativeWebView) {
    window.ReactNativeWebView.postMessage(JSON.stringify({ type, payload }));
  }
};

sendReactNativeMessage({ type: 'Cloth' });

react-native

  const recieveMessage = async (e: WebViewMessageEvent) => {
    const data = e.nativeEvent.data;

    if (isJsonString(data)) {
      const parseData = JSON.parse(data);
		  console.log(parseData)
		} else {
      console.error("유효하지 않r은 JSON 문자열:", data);
    }
  };

ootdzip 에서는 어떻게 사용하나요?

갤러리 이미지 업로드

유저의 갤러리에 있는 이미지를 업로드 하기 위해서 expo-image-picker를 사용했다.
expo-image-picker에서 얻은 데이터를 react-native에서 바로 s3로 업로드 하기로 했다.
s3 업로드 후 받은 urlwebview로 보내주었다.

native

//getRecieveMessage.tsx
if (parseData.type === 'gallery') {
        getSelectedPhoto(parseData.type).then(async (res) => {
          ...
          fetch("https://ootdzip.com/api/...", {
            method: "POST",
            headers: {
              "Content-Type": "multipart/form-data",
              Authorization: `Bearer ${await SecureStore.getItemAsync(
                "accessToken"
              )}`,
            },
            body: formData,
          })
            .then((response) => response.json())
            .then((response) => {
              sendMessage({
                webViewRef,
                type: parseData.type,
                payload: response.result.imageUrls,
              });
            })
            .catch((error) => {
              console.log("error", error);
            });
        });
      }

첫 시도

expo-image-picker 를 사용해 얻은 이미지 정보를 webview로 전송해 s3에 업데이트 하려고 했다. 하지만 formData의 파일 형식의 정보가 webview에서는 인식이 되지 않았다. 그래서 웹의 input file을 이용해 똑같은 이미지를 웹에서 업로드하고 모바일에서 업로드 해보았지만, 웹에서만 정상적으로 s3 업로드 api가 작동했다. 해당 사진을 가지고 있는 기기에서만 유효하다 라는 결론을 지었다.


뒤로가기

안드로이드

안드로이드의 경우 ios와 다르게 뒤로가기 버튼이 물리적으로 작동한다. 별도의 처리를 해주지 않으면 앱이 바로 꺼지는 현상이 발생했다. 이에 뒤로가기 버튼 작동 시 webviewrouter.back() 로직이 작동해야 했고, 별도의 처리를 해주었다.

const webViewRef = useRef<WebView>();
const [currentUrl, setCurrentUrl] = useState<string>("");
const [canGoBack, setCanGoBack] = useState<Boolean>(false);

const backPress = useCallback(() => {
    if (webViewRef.current && canGoBack) {
      webViewRef.current.goBack();
      return true; 
    }
    BackHandler.exitApp();
    return false;
 }, [canGoBack]);

useEffect(() => {
    BackHandler.addEventListener("hardwareBackPress", backPress);
    return () => {
      BackHandler.removeEventListener("hardwareBackPress", backPress);
    };
  }, [backPress]);


const onNavigationStateChange = (navState: WebViewNavigation) => {
    setCurrentUrl(navState.url);
    setCanGoBack(navState.canGoBack);
 };

return (
	<Webview 
       ...
       ref={webViewRef}
       onNavigationStateChange={onNavigationStateChange}/>
)

ios

ios의 경우에는 뒤로가기 버튼이 없어 webview에서 뒤로가기 버튼을 따로 구현해주었다. 하지만 이 버튼을 일일히 누르는게 불편했고, 유저들은 왼쪽 모서리를 오른쪽으로 스와이프 해 뒤로가기를 하는 다른 앱의 경험에 익숙해져 있었다.
그래서 아래와 같은 코드를 추가해 해결했다.

return (
	<Webview 
       ... 
       allowsBackForwardNavigationGestures={true}/>
)

jwt 토큰 교환

api를 위한 accessToken 전송

ootdzip 의 모든 api에는 accessToken을 통한 인증이 필요하다.
react-natives3 업로드 api에서도 accessToken이 필요하다는 말이다.
그래서 나는 webview에서 로그인 시 react-native에도 accessToken을 저장하기로 했다.

fetch("https://ootdzip.com/api", {
 			...
            headers: {
              ...,
              Authorization: `Bearer ${await SecureStore.getItemAsync(
                "accessToken"
              )}`,
            },...
          })

자동 로그인

webview의 로컬 스토리지는 앱이 종료되면 초기화 되는 현상을 발견했다. 그래서 나는 expo-secure-store를 활용해 react-native에 저장하고 로그인 시 webview로 넘겨줘야 하는 방식을 사용해야했다.

토큰 받기

if (parseData.type === "accessToken") {
     await SecureStore.setItemAsync("accessToken", parseData.payload);
}
if (parseData.type === "refreshToken") {
     await SecureStore.setItemAsync("refreshToken", parseData.payload);
}

앱 로드시 토큰 보내기

native

useEffect(() => {
    async () => {
      sendMessage({
        webViewRef: webViewRef,
        type: "jwt",
        payload: {
          accessToken: await SecureStore.getItemAsync("accessToken"),
          refreshToken: await SecureStore.getItemAsync("refreshToken"),
        },
      });
    };
  }, []);

webview

if (parsedData!.type === 'token') {
      const accessToken = parsedData?.payload.accessToken;
      const refreshToken = parsedData?.payload.refreshToken;
      localStorage.setItem('accessToken', accessToken);
      localStorage.setItem('refreshToken', refreshToken);
    }

로그 확인

expo go 에서 앱을 작동해보는 경우 native에 관한 콘솔을 찍고싶을때 nativemessage를 보내 로그를 확인할 수 있다.

webview

 sendReactNativeMessage({
      type: 'console',
      payload: data,
 });

native

if (parseData.type === "console") {
        console.log("webview에서 출력", parseData.payload);
}

그밖의 webview 세팅

백화현상

앱을 백그라운드에 둔 상태로 휴대폰을 사용하다 메모리 사용이 크다고 판단되면 백그라운드의 앱을 꺼버리는 현상이 발생한다. 이 경우 앱으로 돌아가보면 하얀 화면만 출력된다. 이러한 현상을 위한 조치를 취했다

https://velog.io/@young_mason/React-Native-ios%EC%97%90%EC%84%9C-%ED%9D%B0%ED%99%94%EB%A9%B4%EC%9D%B4-%EB%9C%A8%EB%8A%94-%EB%AC%B8%EC%A0%9C#%EB%AC%B8%EC%A0%9C-%EC%9B%90%EC%9D%B8

<Webview
 ...
 onContentProcessDidTerminate={() => {
        webViewRef.current?.reload();
 }}/>

정리

오늘은 webview <-> native 통신 환경 세팅에 대해 알아보았다.
앱 개발자분들 고생이 많으십니다.. 웹 개발 외에도 정말 할 일이 많다는걸 깨닫게 되었다.
아마 푸쉬알림까지 구현하게 되면 이 포스팅의 길이가 많이 길어질 것 같다.

0개의 댓글