앱이 웹 컨텐츠를 표시하는 데 사용할 수 있는 내장형 브라우저
웹뷰 내에서 실행되는 자바스크립트는 기본 시스템 API를 호출할 수 있습니다. (이러한 기능을 브릿지라고 합니다.)
생각만해도 어질어질하죠?
웹 없이 앱을 시작하되, 네이티브 개발 리소스가 없다면 플러터
리액트를 사용하여 웹 서비스를 개발 중이라면 리액트 네이티브
import { Component } from 'react';
import { View } from 'react-native';
import { WebView } from 'react-native-webview';
export default class App extends Component {
render(){
const runFirst = `
document.body.style.backgroundColor: 'red';
setTimeout(function() {window.alert('hi')}, 2000);
true;
`
return (
<View style={{ flex: 1}}>
<WebView
source={{ url: 'https://github.com/react-native-webview/react-native-webview'}}
onMessage{(event) => {}}
injectedJavaScript={runFirst}
/>
</View>
)
}
}
해당 코드는 웹뷰가 웹을 열때 위에 있는 템플릿 문자열을 넣어서 전송할 수 있습니다. 즉, 해당 코드가 먼저 실행됩니다. => alert으로 hi가 출력
const html = `
<html>
<body>
<script>
setTimeout(function(){
window.ReactNativeWebView.postMessage("Hello!")
},2000)
</script>
</body>
</html>
`
import { Component } from 'react';
import { View } from 'react-native';
import { WebView } from 'react-native-webview';
export default class App extends Component {
render(){
return (
<View style={{ flex: 1}}>
<WebView
source={{ html }}
onMessage{(event) => {
alert(event.nativeEvent.data);
}}
/>
</View>
)
}
}
# Expo CLI 설치
sudo npm install -g expo-cli
# 원하는 프로젝트로 변경
cd { 원하는 프로젝트 경로 }
# Expo 프로젝트 생성
# 구버전: expo init { 프로젝트 명 }
npx create-expo-app --template {Example}
yarn create expo-app
npm create expo-app
# WebView 라이브러리 설치
yarn add react-native-webview
import { WebView } from 'react-native-webview';
export default function App(){
return (
<WebView style={styles.container} source={{ uri: 'https://github.com/hyeon9782' }}
)
}
이제 끝인 걸까요? 아닙니다!
<a></a>
)import { NavigationContainer } from '@react-navigation/native';
const App = () => {
<NavigationContainer>
{/* Stack, Drawer, BottomTab */}
</NavigationContainer>
}
import { createNativeStackNavigator } '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
const StackNavigator = () => (
<Stack.Navigator>
<Stack.screen name="Home" component={HomeScreen} />
</Stack.Navigator>
)
import { createDrawerNavigator } '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
const DrawerNavigator = () => (
<Drawer.Navigator>
<Drawer.screen name="Home" component={HomeScreen} />
</Drawer.Navigator>
)
import { createBottomTabNavigator } '@react-navigation/bottom-tab';
const BottomTab = createBottomTabNavigator();
const BottomTabNavigator = () => (
<BottomTab.Navigator>
<BottomTab.screen name="Home" component={HomeScreen} />
</BottomTab.Navigator>
)
컴포넌트에서 스크린 컴포넌트가 보내주는 navigate나 route를 전달받지 못할 때가 있습니다.
// react-navigation/native
const navigation = useNavigation();
const route = useRoute();
// window navigator 객체의 userAgent
const isiOS = window.navigator.userAgent.match('iPad');
|| window.navigator.userAgent.match('iPhone');
|| window.navigator.userAgent.match('iPod');
const isAndroid = window.navigator.userAgent.match('Android');
// 리액트 네이티브 웹뷰 객체의 inject
const isWebView = !!window.ReactNativeWebView;
const MAX_MOBILE_SIZE = 992;
const useIsMobile = () => {
const [width, setWidth] = useState(undefined);
useEffect(() => {
const handleResize = () => {
const { innerWidth: changedWidth } = window;
setWidth(changedWidth);
}
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize)
}, []);
return { isMobile: !!width && width < MAX_MOBILE_SIZE };
}
const sendRouterEvent = async (params) => {
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ROUTER_EVENT', ...prams }))
}
const useWebView = () => {
const isWebViewRef = useRef(false);
useEffect(() => {
if (isWebViewRef.current) return;
if (typeof window !== 'undefined' && window.ReactNativeWebView) {
isWebViewRef.current = true;
}
}, [])
return {
isWebView: isWebViewRef.current
}
}
리액트에서 사용하는 useWebView 커스텀 훅
const useAppRouter = () => {
const { isWebView } = useWebView();
const router = useRouter();
const path = async (url, as, options = { shallow: true }) => {
return isWebView
? sendRouterEvent({
path: `${BASE_URL}${url}`,
screenName: options.appScreenName ?? '',
data: {...(options.appSendData ?? {})},
})
: router.push(url, as, options);
}
return { push }
}
useWebView를 Next에서 사용하는 router를 사용해서 한 번 더 래핑
웹뷰일 때는 sendMessage, 웹뷰가 아닐때는 라우터를 푸시
const WebViewContainer = ({ navigation }) => {
const requestOnMessage = (event) => {
const nativeEvent = JSON.parse(event.nativeEvent.data);
if (nativeEvent.type === 'ROUTER.EVENT'){
const { path } = nativeEvent;
const pushAction = StackActions.push(key, { url: path, isStack: true});
navigation.dispatch(pushAction);
return;
}
}
return (
<WebView
source={{ uri: BASE_URL }}
onMessage={requestOnMessage}
)
}
const Home = () => {
return (
<Tab.Navigator>
<Tab.Screen name="Feed" component={Fedd}>
<Tab.Screen name="Messages" component={Messages}>
</Tab.Navigator>
)
}
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={Home}
options={{ headerShom}}
>
<Stack.Screen name="Profile" component={Profile}>
<Stack.Screen name="Setting" component={Setting}>
</Stack.Navigator>
</NavigationContainer>
)
}
케이스에 맞게 화면을 구성할 수 있습니다.
푸시 알림 기능은 구글과 애플에서 유료로 막아놨습니다. 하지만 서드파티 툴을 사용하면 가능합니다.
외부 푸시 알림 서버는 HTTP 요청으로 소통합니다.
// app.json
"expo": {
// 그 외 설정
"plugins": [
[
"expo-notifications",
{
"icon: "./assets/favicon.png",
"color": "#ffffff"
}
]
],
"extra": {
"eas": {
"projectId": "여러분의 프로젝트 아이디"
}
}
}
import * as Notifications from 'expo-notifications';
Notifications.setNotificationHandler({
handleNotification: async () => {
return {
shouldPlaySound: false,
shouldSetBadge: false,
shouldShowAlert: true
}
}
})
useEffect(() => {
const subscription1 = Notifications.addNotificationReceivedListener((notification) => {
console.log(알림이 도착했어요!)
const { userName, type } = notification.request.content.data;
setUserInfo({ name: userName, type });
})
const subscription2 = Notifications.addNotificationResponseReceivedListener((response) => {
console.log(알림을 터치했어요!)
const { userName, type } = response.notification.request.content.data;
Alert.alert(`도착한 유저 정보: ${type} - ${userName}`)
})
return () => {
subscription1.remove();
subscription2.remove();
}
}, [])
Notifications.scheduleNotificationAsync({
content: {
title: '로컬 알림 타이틀이에요.',
body: '로컬 알림에 들어갈 내용이에요',
data: { userName: targetUserName, type: '로컬 알림'},
},
trigger: {
seconds: 2,
}
})
const { status } = await Notifications.getPermissionsAsync({
ios: {
allowAlert: true,
allowBadge: true,
allowAnnouncements: true,
}
})
권한이 있는지 확인하는 코드입니다. 권한이 없다면 요청을 해야합니다.
const { status } = Notifications.requestPermissionsAsync();
const pushTokenData = await Notifications.getExpoPushTokenAsync({ projectId })
eas build
해당 명령어 하나면 앱이 빌드됩니다. iOS 번들링은 유료입니다!
실제로 앱에서만 사용할 수 있는 네이티브 기능을 넣지 않고 웹과 동일한 경우에는 구글과 애플에서 거절 메일이 올때도 있습니다.
앱을 확장하고 싶다? 네이티브 개발이 필수입니다.
이번 영상에서는 웹뷰를 이용해 기존에 있던 웹 서비스를 앱으로 빠르게 구현하는 방법을 다뤘습니다. 저는 웹뷰에 대한 지식이 전혀 없음에도 해당 영상만 보고도 앱을 출시할 수 있을 거 같다는 생각이 들만큼 아주 라이브러리 설명부터 빌드까지 설명을 해주시는데요. 리액트 네이티브의 Expo라는 프레임워크를 사용합니다. 웹뷰에 대한 관심은 있지만 어떻게 시작해야될지 모르겠는 분들이 보시면 정말 도움이 많이될 거 같습니다.