react-native-webview (1) 웹뷰를 통해 웹과 앱 사이에서 데이터 주고 받기

김정우·2023년 3월 30일
1

react-native

목록 보기
1/1

들어가며

회사에서 프론트엔드 코드를 다시 쓰는 리빌딩 프로젝트를 진행 중이다.
가장 큰 목적 중 하나는 개발 생산성을 높이는 것인데, 이를 위해 nextjs로 웹 개발을 하고 앱은 React Native와 react-native-webview를 사용해서 nextjs 프로젝트를 띄워주는 웹앱 형태로 서비스를 구현하고 있다.

react-native-webview는 모바일 플랫폼 네이티브 웹뷰를 react native의 컴포넌트로 추상화하여 제공하는 라이브러리이다. 기본적으로 웹앱에서는 웹뷰를 통해 웹 프로젝트를 띄워주는 간접적인 기능만 구현하지만, 때때로 웹뷰로 보여지는 웹 프로젝트와 앱 프로젝트 간 데이터를 주고받아야 하는 경우가 있다.
앱의 자동 로그인 기능이 그 예시인데, 자동 로그인을 하고 있는 앱을 켤 경우 로드되는 웹 프로젝트는 로그인 정보를 가지고 있지 않기 때문에 앱에서 로그인이 되었다는 어떤 정보를 웹으로 넘겨줘야 할 것이다. 또 다른 주요한 예시로는 모바일 기기의 권한 활용에 관련된 여러 기능인데, 웹 프로젝트는 브라우저를 통해 여러가지 권한을 얻는 반면 앱에서는 모바일 기기를 통해 동의를 얻어야 하므로 웹 프로젝트의 권한 요청이 모바일 기기의 권한 요청으로 이어져야 한다.

이번 글과 다음 글에서는 이 react-native-webview 라이브러리를 사용해서 웹앱을 구현할 때 데이터를 주고 받는 방식에 대한 설명과 경험을 작성하려고 한다.

먼저 이번 글에서는 일반적으로 데이터를 주고 받는 방법을 주로 다룰 것이고, 다음 글에서는 이에 더해 데이터를 주고 받는 조금은 특별한 케이스에 대해 다룰 예정이다.

react-native-webview 설치

사실 설치하는 방법은 라이브러리 레포지토리에 잘 나와있긴 하지만(참고), 그래도 간단하게 정리한다.

  1. react-native-webview 설치
    yarn add react-native-webview
    물론 npm을 사용해도 설치가 가능하다.

  2. 네이티브 의존성 링크
    RN은 자바스크립트로 작성하지만 결국 네이티브에서 실행된다. 따라서 RN 라이브러리는 자바스크립트 라이브러리이기 때문에, 단순하게 라이브러리를 설치한다고 실제 모바일 기기에서 실행되지 않는다. 실제 동작을 위해서는 라이브러리에서 지원하는 기능들을 구동시키기 위해 네이티브 의존성들을 설치해줘야 한다. 아마 이 글을 사보고 웹뷰를 사용해보는 사람들의 경우 이런 링크를 수동으로 할 필요는 없겠지만, ios와 android 별로 추가 작업이 필요하다.

  • CocoaPods를 사용하는 경우, react-native link는 podfile을 업데이트 시켜준다. 따라서 업데이트된 podfile의 의존성들을 설치해주어야 한다.
    pod install

  • 6.X.X 이상 버전의 react-native-webview 라이브러리를 사용한다면 RN 프로젝트 android/gradle.properties 파일에 다음 코드를 추가해주어야 한다.

    	android.useAndroidX=true
    	android.enableJetifier=true
  1. 컴포넌트 사용
    설치가 되었으면 라이브러리에서 제공하는 웹뷰 컴포넌트를 사용하면 된다. 공식 문서에서 제공하는 예시는 다음과 같다.
import React, { Component } from 'react';
import { WebView } from 'react-native-webview';

class MyWeb extends Component {
  render() {
    return (
      <WebView
        source={{ uri: 'https://infinite.red' }}
        style={{ marginTop: 20 }}
      />
    );
  }
}

react-native-webview의 공식문서는 클래스 컴포넌트를 사용한 예시만 나와있어서 불편한 점이 꽤 많긴 하지만 우선 위 컴포넌트의 uri에 웹뷰를 통해 보고자 하는 웹 페이지의 주소를 적어두면 앱에서 웹을 볼 수 있게 된다.

웹뷰가 제공하는 기능도 문서에 잘 정리되어 있고(참고), 웹뷰를 활용하는 기능 별 보다 자세한 가이드도 제시되어 있다(참고).

데이터 주고 받기

그럼 이제 본격적으로 글의 주제에 관해 얘기해보자.

아까 예시로 들었던 자동 로그인의 경우에는 앱 -> 웹으로 '자동 로그인 된 상태'를 나타내는 데이터를 전달해주어야 한다. 반면 권한 요청의 경우에는 웹 프로젝트 코드에서 권한을 요청하는 기능을 수행할 때 실제로는 앱에서 권한을 요청해야 하므로 웹 -> 앱으로 데이터를 전달해야 한다.

이렇게 웹앱에서 웹과 앱 사이에서 데이터를 주고 받기 위해서 웹뷰를 활용할 수 있다.
사실 이 방법에 대해서는 공식 문서에서도 잘 소개해주고 있긴하다(참고). 하지만 내가 사용하는 방식은 조금 다르기도 하고, 어차피 이번 글에서 다루는 내용 자체는 소개와 안내 정도의 의미를 갖는 정도이다,.

세가지 토픽을 소개하고 이를 중점으로 설명하려고 한다.

1. 웹 페이지를 앱(웹뷰)에서 접속한 것을 어떻게 알 수 있나요?

제일 먼저 웹 서비스 자체는 자신이 앱에서 웹뷰를 통해 보여지든지 데스크탑의 브라우저를 통해 실행되든지 실행에 있어서 차이가 없다. 하지만 우리는 어떤 플랫폼 (앱 <-> 나머지)에서 실행되는지에 따라 다른 처리를 해줘야 하기 때문에 웹 코드에서 '앱에서 웹뷰를 통해 보여지고 있다'는 정보를 얻어야 한다.

이는 window 객체에 ReactNativeWebview 값이 있는지 여부를 확인해보면 알 수 있다. react-native-webview를 통해 웹에 접속한 경우 window.ReactNativeWebview 객체가 존재하고, 이 값을 사용하여 필요한 분기 처리 등을 수행하면 된다.

우리 회사는 nextjs로 웹 프로젝트를 구현했는데, 이런 경우에는 단순히 window.ReactNativeWebview를 참조하면 참조 에러가 발생하게 된다. nextjs에서는 서버 사이드 렌더링을 제공하는데, 서버에는 window 객체가 존재하지 않기 때문이다. 이러한 문제를 처리하는 방법이 여러가지가 있겠지만, 제일 간단한 것은 해당 값의 참조를 useEffect 등의 훅 안에서 처리하는 방법이다. useEffect는 클라이언트 사이드에서 실행되는 것이 보장되기 때문에 window 객체가 있다는 것 역시 보장된다.

다음과 같은 커스텀 훅을 만들어서 사용할 수 있다.

const useIsReactNativeWebview = () => {
  const [isReactNativeWebview, setIsReactNativeWebview] = useState(false);
    
  useEffect(() => {
  	if (window.isReactNativeWebview) setIsReactNativeWebview(true);
  }, []);
  
  return isReactNativeWebview;
}

2. 웹에서 앱으로 어떻게 데이터를 전송할 수 있나요?

이건 두 파트로 좀 더 상세하게 나눌 수 있다.

1. 웹 -> 앱으로 데이터 보내기
상술한 window.ReactNativeWebview 객체에는 postMessage 라는 메서드가 존재한다. 해당 메서드의 인자로 넘겨주는 데이터는 현재 웹 페이지를 띄워주는 웹뷰(= 앱)에 전달 된다. 이 때 전달하는 데이터는 항상 직렬화 해주어야 한다.

또한 여러가지 유형의 데이터를 전송할텐데 앱에서 전송된 데이터를 구분하여 처리하기 위해 전달할 데이터의 type을 함께 전달한다. 코드 예시는 다음과 같다.

const postMessage = (type, data) => {
  window.ReactNativeWebView?.postMessage(JSON.stringify({ type, data }));
};

postMessage 함수를 실행할 때 발생시켜야 할 여러가지 side effect 들을 함께 처리할 수 있기 때문에 wrapper 함수를 사용한다.

2. 웹에서 전달한 메시지를 앱에서 받기

(이번에는 함수형 컴포넌트로) 다시 웹뷰 컴포넌트를 보자.

const Webview = () => (
  <WebView
    source={{ uri: 'https://infinite.red' }}
    style={{ marginTop: 20 }}
    onMessage={({ nativeEvent }) => {
      const { type, data } = JSON.parse(nativeEvent.data);
	
      const handler = receiveWebviewMessageMap.get(type);
      handler?.(data);
    }}
  />  
);

Webview 컴포넌트의 onMessage 속성에는 웹에서 postMessage를 통해 데이터를 전달했을 때 실행되는 콜백함수를 정의할 수 있다.

웹에서 데이터를 직렬화 한 다음 전달했으니, 해당 값을 역직렬화하여 type와 data 값을 구한다. 그리고 type에 해당하는 핸들러를 실행시켜주면 되는데 if 문을 난사하는 것보다 타입과 핸들러를 매핑시켜놓은 객체를 따로 관리하면 코드를 보다 깔끔하게 분리하여 관리할 수 있다.

3. 앱에서 전송하는 데이터를 웹에서 어떻게 받을 수 있나요?

마찬가지로 두 가지로 나누어 보자.

1. 앱 -> 웹으로 데이터 보내기
위 공식문서에 나와 있는 것과 같이 Webview 컴포넌트의 injectJavaScript 속성을 사용하면 웹뷰에 실행될 자바스크립트 코드를 직접 주입 시키는 형태로 데이터를 전달할 수 있다.

해당 방식의 예시는 다음과 같다.

import React, { 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; // note: this is required, or you'll sometimes get silent failures
    `;
    return (
      <View style={{ flex: 1 }}>
        <WebView
          source={{
            uri: 'https://github.com/react-native-webview/react-native-webview',
          }}
          onMessage={(event) => {}}
          injectedJavaScript={runFirst} // injectJavaScript 속성과는 다르지만 비슷하게 동작한다.
        />
      </View>
    );
  }
}

하지만 나는 다른 방식을 사용한다. Webview 컴포넌트에 ref를 붙이면 ref를 이용하여 웹 -> 앱으로 데이터 보내는 것과 비슷한 방식으로 데이터를 전달할 수 있다.

const index = () => {
	const webviewRef = useRef();

	const postMessage = ({ type, data }) => {
		if (!ref.current) return;
  	
	  	webviewRef.postMessage(JSON.stringify({ type, data }));
	};

	return (
	    <WebView
	      ref={webviewRef}
		/>
    );
}

2. 앱에서 전달한 데이터를 웹에서 받기

기본적인 메커니즘은 앱에서 데이터를 받는 것과 비슷하다.
앱에서 postMessage를 통해 데이터를 보내는 것이 하나의 이벤트가 되어 웹 코드에서는 이벤트에 반응하도록 코드를 작성하면 된다. 즉, 이벤트 리스너를 달아주면 된다.

const receiver = platform === ios ? window : document;

receiver.addEventListener('message', (event) => {
  const { type, data } = JSON.parse(event.data);

  const handler = receiveWebviewMessageMap.get(type);
  handler?.(data);
});

함수의 몸체 부분은 앱에서 데이터를 전달받았을 때 처리하는 코드와 동일하다.
여기서 눈여겨 봐야 할 것은 addEventListener의 주체인 receiver 라는 값이다. 대단한 값은 아니고 모바일 기기의 플랫폼에 따라 서로 다른 값을 부여해야 하는데, 이를 하나의 변수에 담아서 사용한 것이다. platform === ios 부분은 일반적으로 user agent의 값을 확인해서 ios / android를 구분한다.

마치며

이렇게 웹뷰를 사용해서 웹 <-> 앱 코드 간에 데이터를 공유하는 기본적인 방법에 대해 알아보았다. 간단한 코드로 데이터를 공유할 수 있지만 타입 스크립트를 활용하거나, 실무에서 사용하다보면 전체적인 코드가 보다 복잡해지긴 한다.
그래도 사실 구조 자체는 간단하기도 하고 데이터를 보내고 받는 코드가 웹, 앱에서 거의 비슷해서 크게 어려운 점은 없을 것이라 생각한다.

다음 글에서는 데이터를 공유하는 보다 특수한 케이스에 대해서 다룰 예정이다. 이 글에서 소개한 방식은 (1) 한 플랫폼에서 다른 플랫폼으로 데이터를 전달하는 것과 (2) 데이터를 받아서 처리하는 코드가 분리되어 있다.
만약, 유저의 사진첩에서 사진을 받아와서 해당 데이터를 처리하는 기능을 웹뷰를 통해 구현한다고 하면 어떻게 할 수 있을까?

// 웹 코드

1. 유저의 사진첩에서 데이터를 가져온다. // 사진에 접근하는 권한을 요청하고 사진을 받아오는 과정은 네이티브(앱)에서 처리해야 한다.
2. 사진 데이터를 가공한다.

다음의 순서로 처리하면 된다고 생각 될 것이다.
(1) 1을 수행하기 위해 웹에서는 postMessage로 사진 데이터가 필요하다는 정보를 앱으로 보낸다.
(2) 앱의 onMessage에서 타입을 확인 후, 사진에 접근하는 권한을 요청하고 유저가 사진을 선택한다.
(3) 앱에서 웹으로 postMessage를 통해 사진 데이터를 전달한다.
(4) 웹에서는 receiver.addEventListener를 통해 등록한 핸들러에서 사진 데이터를 전달받아 2를 수행한다.

여기서 문제는 2가 실행되기 전에 1이후 (2), (3)이 모두 수행되어야 한다는 점이다. (2), (3)이 실행되기 전까지 코드는 2번 라인으로 넘어가지 않고 1번에서 대기하고 있어야 하기 떄문이다. 1에 해당하는 postMessage는 호출되고 끝인데, 어떻게하면 2가 실행될 때 데이터를 받아온 상태라는 것을 보장할 수 있을까?

나는 postMessage를 호출 했을 때, 반환값(사진)을 받을 수 있다면 위 기능을 구현할 수 있겠다고 생각했다. 다음 글에서는 이 방법 'postMessage에서 반환값 받기'를 주제로 글을 작성할 예정이다.

참고자료

profile
hello world!

0개의 댓글