My Boundary As Much As I Experienced

RN) React-Native-Webview로 웹뷰 띄우기 (+ 로딩스피너 ActivityIndicator 띄우기 및 onLayout 이벤트로 좌표위치 파악하기) 본문

FrontEnd/React Native

RN) React-Native-Webview로 웹뷰 띄우기 (+ 로딩스피너 ActivityIndicator 띄우기 및 onLayout 이벤트로 좌표위치 파악하기)

Bumang 2024. 9. 7. 02:19

많은 서비스들이 웹뷰를 쓴다. 특히 껍데기만 네이티브나 크로스플랫폼앱으로 만들고,

안의 내용물들은 모두 웹뷰를 쓰기도 한다. 이런 경우 하이브리드앱이라고 부른다.

하여튼 수많은 서비스들이 (그 양이 많든 적든) 일정부분 웹뷰를 활용해서 커버하는 부분이 있다.

왜 이렇게 다양하게 웹뷰를 활용할까?

 

하이브리드 앱의 경우: 전체 서비스를 웹뷰로.

1. 반응형 웹개발 내용을 앱으로 바로 포팅 가능

요새 거의 모든 웹사이트들은 모바일웹 서비스를 따로두기보단 '반응형 웹'으로 여러 해상도를 커버한다.

그런데 이를 모바일 앱에 바로 활용한다면? 그냥 앱개발을 따로 하지 않아도 된다는 엄청난 장점이 생긴다.

 

2. 웹 개발자 돌려쓰기 가능

iOS/Android 개발자들 한 무더기 뽑고 웹개발자들도 뽑고... 이러지 않아도 된다.

소수의 iOS/Android 개발자와 한 무더기의 웹개발자들을 뽑아 서비스 개발을 커버칠 수 있다.

 

3.웹에서 동작하는 기능 활용 가능:

특정 기능이 웹에서만 동작할 때(예: 특정 API가 웹에서만 지원되는 경우), 웹뷰를 사용하여 앱 내에서 활용 가능하다.

풍부한 npm패키지를 활용 가능하다는 장점이 있다.

 

크로스플랫폼 앱의 경우: 일부 페이지만 웹뷰로.

그러나 꼭 하이브리드 앱을 개발하지 않는다고 해도 웹뷰는 종종 쓰인다.

RN이나 flutter같은 크로스플랫폼 앱개발 기술을 사용할 때도 웹뷰는 쓰인다.

특히 회사 정책이나 약관, 회사 블로그 내 포스트 보여주기.. 등 동기화가 꼭 필요한데 성능은 전혀 필요없는 페이지에 쓰이게 된다.

또한 구글/카카오 OAuth나 가벼운 이벤트 페이지에도 종종 쓰이는 듯...

 

물론 위의 경우들을 모두 리액트네이티브 코드로 개발해도 된다.

그런데 웹뷰로 웹과 동기화를 잘 했다고 치면,

회사 약관이 변경되었다고 앱개발자들이 손수 앱 내 약관 수정하고

구글/애플 심사를 다시 받고, 업데이트를 해야되는,, 이런 귀찮은 일을 없애준다!

 

나도 최근 이번 초기앱의 약관이나 건의사항, 자주하는 질문 부분을 모두 웹뷰로 개발했다.

그리고 생각보다 꽤 쉬워서 놀랐는데 지금부터 React-Native-webview 라이브러리로

어떻게 웹뷰를 구현했는지 설명하겠다.

 

리액트 네이티브에서 정말 쉽게 웹뷰 사용하기

1. 설치

공식 문서 보면서 깔면 된다.

npm install react-native-webview

 

그리고 npm 설치 후 습관적으로 pod install도 해주자.

cd ios
pod install
cd ..

 

2. 라우터에 웹뷰 스크린 추가

웹뷰용 페이지 만들어서 라우터에 추가해주자.

      // 라우터 코드에 스크린 추가.
      ...
      <MainStack.Screen
        name="Webview"
        component={WebViewContainer}
        options={{
          header: () => <CustomHeader isGoBack />,
        }}
      />

 

쓰잘데기 없는 로직은 없애고 웹뷰 컴포넌트 사용하는 것만 보자면 아래와 같다.

SafeAreaView에다가 라이브러리가 제공하는 Webview 컴포넌트를 마운트한 다음에

uri를 주입해주면 된다. 나는 useRoute를 이용하여 url을 패러미터로 가져오는 방법을 택했는데,

사실 prop으로 가져와도 되고, 원한다면 하드코딩으로 언제나 한 페이지만 불러오게 할수도 있다.

...
import { useRoute, RouteProp, useNavigation } from "@react-navigation/native";
import { WebView } from "react-native-webview"; // 웹뷰컴포넌트 임포트


const WebViewContainer = () => {
  const route = useRoute<WebviewRouteProp>();
  const { url, title } = route.params;

  return (
    <SafeAreaView style={styles.container}>
      <WebView
        source={{ uri: url }}
        style={{ flex: 1 }}
      />
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  ...
});

export default WebViewContainer;

 

 

 

3. 웹뷰가 페이지를 로딩하는 약 1초간을 위해 로딩 인디케이터 추가

나는 웹뷰가 불러오는 동안 로딩 인디케이터를 보이게 하고 싶어서

Webview 컴포넌트가 제공하는 속성인...

        onLoadStart={() => setLoading(true)}
        onLoadEnd={() => setLoading(false)}
        onLoadProgress={({ nativeEvent }) => { ... })}

등을 사용했다. 이름에서 알 수 있듯 웹뷰가 현재 로드중인지, 완료됐는지 등의 시점에 콜백을 실행시켜주는 기능을 한다.

loading이란 State를 연결해서 아래처럼 로딩 중일때는 RN 로딩 인디케이터를 보이게 했다.

(사용자 폰 OS의 로딩인디케이터를 사용하게 해준다.)

      {loading && (
        <ActivityIndicator
          onLayout={handleLayout}
          size="large"
          style={{
            position: "absolute",
            zIndex: 3,
            top: "50%",
            left: "50%",
            transform: [
              { translateX: -layout.width / 2 },
              { translateY: -layout.height / 2 },
            ],
          }}
        />
      )}

 

4. 막간 팁: onLayout이벤트로 요소 위치값을 알아내어 중앙정렬

여기서 onLayout은 또 다른 개념인데,

웹에서 getBoundingClientRect 메소드에 대응하는 개념이라고 보면 된다.

x좌표값, y좌표값, width값, height값, 스크롤 값 등의 정보를 얻고 싶으면 onLayout이벤트를 받아오면 된다.

 

결국 전체 코드는 아래와 같다.

import { useRoute, RouteProp, useNavigation } from "@react-navigation/native";
import { TotalNavigationProp } from "../../../App";
import React, { useEffect, useState } from "react";
import {
  ActivityIndicator,
  SafeAreaView,
  StyleSheet,
  LayoutChangeEvent,
} from "react-native";
import { WebView } from "react-native-webview";

import { type MainStackParamList } from "types/navigator";
import CustomHeader from "components/header";

type WebviewRouteProp = RouteProp<MainStackParamList, "Webview">;

const WebViewContainer = () => {
  const [loading, setLoading] = useState(true);
  const [layout, setLayout] = useState({ width: 0, height: 0 });

  const route = useRoute<WebviewRouteProp>();
  const { url, title } = route.params;

  const handleLayout = (event: LayoutChangeEvent) => {
    const { width, height } = event.nativeEvent.layout;
    setLayout({ width, height });
  };

  const navigation = useNavigation<TotalNavigationProp>();
  useEffect(() => {
    navigation.setOptions({
      header: () => <CustomHeader isGoBack title={title} />,
    });
  }, []);

  return (
    <SafeAreaView style={styles.container}>
      <WebView
        onLoadStart={() => setLoading(true)}
        onLoadEnd={() => setLoading(false)}
        onLoadProgress={({ nativeEvent }) => {
          console.log("Loading progress:", nativeEvent.progress);
        }}
        source={{ uri: url }}
        style={{ flex: 1 }}
      />
      {loading && (
        <ActivityIndicator
          onLayout={handleLayout}
          size="large"
          style={{
            position: "absolute",
            zIndex: 3,
            top: "50%",
            left: "50%",
            transform: [
              { translateX: -layout.width / 2 },
              { translateY: -layout.height / 2 },
            ],
          }}
        />
      )}
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

export default WebViewContainer;