react-native
개발자를 알아보려고 하다가 둘다 할 줄 아는 개발자 멋지지않을까 싶어서 직접 구현해보기로 했다.
react
와 react-native
둘다 문법이 비슷해 생각보다 힘들지는 않았다. ootdzip
은 갤러리 선택 기능 등 native 기능을 사용해야하는 경우가 있다. 이럴때 webview
에서 react-native
로 메세지를 보낼수가 있는데 이때 payload를 같이 보내 원하는 동작을 수행한다. 오늘은 이 통신과정을 구현한 경험을 작성해보겠다.
<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,
});
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);
}
};
export const sendReactNativeMessage = ({ type, payload }: Message) => {
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({ type, payload }));
}
};
sendReactNativeMessage({ type: 'Cloth' });
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);
}
};
유저의 갤러리에 있는 이미지를 업로드 하기 위해서 expo-image-picker
를 사용했다.
expo-image-picker
에서 얻은 데이터를 react-native에서 바로 s3로 업로드 하기로 했다.
s3 업로드 후 받은 url을 webview로 보내주었다.
//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와 다르게 뒤로가기 버튼이 물리적으로 작동한다. 별도의 처리를 해주지 않으면 앱이 바로 꺼지는 현상이 발생했다. 이에 뒤로가기 버튼 작동 시 webview
의 router.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의 경우에는 뒤로가기 버튼이 없어 webview
에서 뒤로가기 버튼을 따로 구현해주었다. 하지만 이 버튼을 일일히 누르는게 불편했고, 유저들은 왼쪽 모서리를 오른쪽으로 스와이프 해 뒤로가기를 하는 다른 앱의 경험에 익숙해져 있었다.
그래서 아래와 같은 코드를 추가해 해결했다.
return (
<Webview
...
allowsBackForwardNavigationGestures={true}/>
)
ootdzip
의 모든 api에는 accessToken을 통한 인증이 필요하다.
react-native 의 s3 업로드 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
에 관한 콘솔을 찍고싶을때 native
로 message를 보내 로그를 확인할 수 있다.
sendReactNativeMessage({
type: 'console',
payload: data,
});
if (parseData.type === "console") {
console.log("webview에서 출력", parseData.payload);
}
앱을 백그라운드에 둔 상태로 휴대폰을 사용하다 메모리 사용이 크다고 판단되면 백그라운드의 앱을 꺼버리는 현상이 발생한다. 이 경우 앱으로 돌아가보면 하얀 화면만 출력된다. 이러한 현상을 위한 조치를 취했다
<Webview
...
onContentProcessDidTerminate={() => {
webViewRef.current?.reload();
}}/>
오늘은 webview <-> native 통신 환경 세팅에 대해 알아보았다.
앱 개발자분들 고생이 많으십니다.. 웹 개발 외에도 정말 할 일이 많다는걸 깨닫게 되었다.
아마 푸쉬알림까지 구현하게 되면 이 포스팅의 길이가 많이 길어질 것 같다.