react-native-webview (2) 웹 코드에서 앱 코드로 데이터 요청하기

김정우·2023년 4월 30일
0

들어가며

react-native-webview 라이브러리를 활용하여 웹앱에서 데이터를 주고받는 방법에 대해 설명한 지난 글에 이은 두번째 react-native-webview 활용 방법에 대한 글이다. 이번 글을 잘 이해하려면 지난 글에서 다룬 내용을 숙지하고 있어야 한다.

문제 상황 정의

지난 글에서는 웹, 앱에서 각각 postMessage, receiveMessage 메서드를 활용하여 앱에서 웹뷰를 통해 웹사이트에 방문했을 때 (1) 웹 -> 앱, (2) 앱 -> 웹으로 메시지를 주고 받는 방법에 대해 다루었다.
이 때 웹과 앱 사이에서 주고 받는 메시지는 단발적으로만 전달되었는데, 지난 글 말미에 언급했듯이 이런 방식은 웹뷰를 사용할 때 특정 케이스를 커버할 수 없게 된다.

유저 핸드폰 앨범에 있는 사진을 받아와 onSuccess 콜백 함수의 인자로 넘겨주는 함수 getPhotos를 구현한 아래 코드를 보자.

export const getPhotos = (onSuccess) => {
  if (isReactNativeWebView()) {
    // 앱으로 이미지를 받아오라는 메시지를 전달
    webviewBridge.postMessage("photo");

    // photos라는 변수에 이미지 데이터가 담기면 다시 웹단 코드로 처리
    onSuccess(photos);
    return;
  }

  ...
};
  • 핸드폰 앨범에 있는 사진에 접근해야 하는 행동은 브라우저에서 처리할 수 없기 때문에 우선 앱에서 접속한 경우를 분기처리해야 한다.
  • 앱에서 사진을 받아오는 작업을 처리하도록 postMessage 메시지를 보낸다.

이후 앱에서는 어떻게 일이 처리될까? 이전 글에서 다룬 일반적인 메시지 전달 방식을 생각해보자.
(1) 앱에서 유저에게 사진 접근 권한을 묻는다.
(2) 유저는 권한을 승인하고 사진을 선택한다.
(3) 앱에서 웹뷰 컴포넌트에 연결해둔 webviewRef 객체의 postMessage 메서드를 활용하여 선택된 사진 데이터를 웹 코드로 넘겨준다.

여기서 중요한 건 '(3)에서 웹으로 넘겨준 사진 데이터에 어떻게 접근할 수 있는가?'이다. 메시지는 {type, data} 형태의 객체로 전달되며, 메시지가 전달되는 경우 해당 type과 미리 매핑된 핸들러 함수가 실행된다. 즉, (3)에서 웹으로 전달한 메시지의 타입과 연결된 핸들러에서만 사진 데이터에 접근할 수 있다는 것이다.

여기까지 이해가 되었다면 위의 getPhotos 함수를 다시보자

export const getPhotos = (onSuccess) => {
  if (isReactNativeWebView()) {
    // 앱으로 이미지를 받아오라는 메시지를 전달
    webviewBridge.postMessage("photo");

    // photos라는 변수에 이미지 데이터가 담기면 다시 웹단 코드로 처리
    onSuccess(photos);
    return;
  }

  ...
};

결국 onSuccess의 인자로 넘겨주기 위해postMessage 함수를 실행한 이후에 photos라는 변수에 사진 데이터 값이 담겨있어야 하는데, 상술했듯 사진 데이터는 앱에서 웹으로 보낸 메시지의 타입과 연결된 핸들러에서만 접근이 가능하다.

자, 그럼 이제 문제가 분명해졌다.
(1) 어떻게 하면 getPhotos 함수에서 앱으로 메시지를 보낸 후 비동기적으로 앱에서 리턴해오는 사진 데이터 값에 접근할 수 있을까?
(2) 어떻게 하면 onSuccess 함수가 실행되기 전에 사진 데이터를 받아와서 해당 데이터에 접근할 수 있다고 보장할 수 있을까?

문제 해결하기

어떻게 이 문제를 해결할 지 본격적으로 고민하기 전에 나는 먼저 문제가 해결된 상황을 상상해봤다.
나는 getPhotos 함수를 어떤 방식으로 구현하고 싶은걸까?

답은 간단했다. 네트워크 요청을 처리하는 비동기 함수를 사용하는 것처럼 만들고 싶었다.

export const getPhotos = async (onSuccess) => {
  if (isReactNativeWebView()) {
    // 앱으로 이미지를 받아오라는 메시지를 전달
    const photos = await webviewBridge.postMessage("photo");

    // photos라는 변수에 이미지 데이터가 담기면 다시 웹단 코드로 처리
    onSuccess(photos);
    return;
  }

  ...
};

(1) 어떻게 하면 getPhotos 함수에서 앱으로 메시지를 보낸 후 비동기적으로 앱에서 리턴해오는 사진 데이터 값에 접근할 수 있을까?
-> 기존의postMessage 함수는 메시지를 앱단으로 전달하고 종료된다. 별다른 반환값이 없는 함수인데, 서버에서 데이터를 받아오기 위해 네트워크 요청을 보내는 것처럼 postMessage 함수 실행 이후 내가 원하는 데이터를 반환받으면 된다.

(2) 어떻게 하면 onSuccess 함수가 실행되기 전에 사진 데이터를 받아와서 해당 데이터에 접근할 수 있다고 보장할 수 있을까?
-> postMessage 함수에 await를 걸어서 photos 변수에 값을 할당한 이후에 onSuccess 함수를 실행시킨다.

자바스크립트의 Promise 객체와 지난 게시글에서 학습했던 observer를 활용해서 해당 기능을 구현할 수 있을 것 같아 전체 플로우를 보다 구체적으로 생각해보았다.
1. 앱으로 메시지를 보낸 후, 데이터가 필요한 경우 postMessage를 비동기 함수로 만든다. 이 때, 마치 네트워크 API를 호출하는 것처럼 데이터를 받을 때까지 pending 상태로 만들어둔다.
2. 앱에서 데이터 처리가 완료되면 웹뷰로 해당 데이터를 돌려준다.
3. 웹에서 해당 데이터를 받았다는 것이 확인되면, 이에 반응하여 pending 상태였던 postMessage를 데이터와 함께 resolve 시킨다.
4. postMessage의 반환을 기다리고 있던 getPhotos 함수에서는 해당 데이터를 가지고 onSuccess 콜백 함수를 실행한다.

각각의 단계를 보다 구체적으로 살펴보자.

1. postMessage 비동기 함수로 만들기

제일 먼저 해야 할 것은 postMessage 함수를 네트워크 요청처럼 비동기 함수로 만드는 일이다. 그래야 photos 변수에 값이 할당되기 전에 onSuccess 함수가 실행되지 않는다는 것을 보장할 수 있다.

먼저 window.ReactNativeWebview.postMessage를 비동기 함수로 감싸줘야 하는데 이를 구현하기 위해 추가로 해줘야 할 작업이 있다.

const postMessage = async (
  type,
  data?,
  options,
) => {
  window.ReactNativeWebView?.postMessage(JSON.stringify({ type, data }));
  if (options?.isAsyncPostMessage) {
    const result = await handlePostMessageResult(type);
	return result;
  }

return null;
};

const handlePostMessageResult = async (type) => {
  const wrappedPromise = getWrappedPromiseObj(messageMap[type]);

  const result = await wrappedPromise.promise;
  return result;
};

export const getWrappedPromiseObj = (type) => {
  let resolvePromise;
  let rejectPromise;

  const promise = new Promise((resolve, reject) => {
    resolvePromise = resolve;
    rejectPromise = reject;
  });

  const wrappedPromise = {
    promise,
    type,
    resolve: resolvePromise,
    reject: rejectPromise,
  };

  return wrappedPromise;
};

postMessage
먼저 데이터 반환이 필요한 postMessage를 구분해주기 위해 options 파라미터를 추가한다. isAsyncPostMessagetrue라면 데이터 반환이 필요한 postMessage로 간주된다. 그럼 handlePostMessageResult 함수가 리턴될 때까지 대기하게되고(await) handlePostMessageResult가 반환되면 그제서야 postMessage 함수가 종료된다.
-> 이것으로 가장 바깥 함수인 postMessage를 비동기 함수로 만들 수 있다.

handlePostMessageResult
postMessage 함수의 비동기적인 특성은 사실상 handlePostMessageResult 함수에서 기인한다.
해당 함수에서는 postMessage 함수에서 넘겨주는 메시지 타입을 Promise로 감싸서 해당 Promise가 resolve 되길 기다렸다가 종료된다.

getWrappedPromiseObj
메시지 객체를 Promise 객체로 감싸서 반환하는 함수이다. 해당 Promise가 어떠한 타입의 메시지를 처리하는 것인지를 나타내기 위해 type 값을 선언했다. 또한 해당 Promise의 resolve, reject를 Promise 바깥에서도 처리할 수 있는 구조를 만들기 위해 resolvePromise, rejectPromise 변수를 활용 및 promise 바깥으로 노출시켰다. 이렇게 구현한 이후는 추후 설명할 observer 패턴에서 사용하기 위함이다.

해당 단계에서 작업한 것을 정리하자면 (1) 메시지를 Promise로 변환하고, (2) 해당 Promise가 종료될 때까지 postMessage 함수를 pending 상태로 유지하면서 반환하지 않게 만듦으로써 potsMessage 함수를 비동기 함수로 변환하였다.

그럼 Promise 함수는 언제, 어떻게 resolve 될 수 있을까? 또한 위에서 문제를 정의할 때 앱에서 넘겨준 사진 데이터는 메시지의 해당 타입과 연결된 핸들러에서만 접근할 수 있는데 postMessage 함수에서 어떻게 접근해서 해당 데이터를 반환해준다는 것일까?

2. 웹에서 해당 데이터를 받았다는 것이 확인되면, 이에 반응하여 pending 상태였던 postMessage를 데이터와 함께 resolve 시킨다.

  • 앱에서 넘겨준 사진 데이터는 메시지의 타입과 연결된 핸들러에서만 접근할 수 있다.
  • 그리고 우리는 위에서 메시지의 타입을 감싼 Promise 객체를 생성하고, postMessage 함수가 해당 Promise 객체의 상태가 바뀔때까지 리턴되지 않도록 처리했다.

이 두가지 정보를 종합하면, 문제 해결의 실마리를 얻을 수 있다.
메시지의 타입과 연결된 핸들러에서 postMessage가 기다리고 있는 Promise 객체를 종료시킬 수 있게 하는 것이다.

지난 글에서 정의한 receiveMessage 함수이다.

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

receiver.addEventListener('message', (event) => {
  const { type, data } = JSON.parse(event.data);
  
  // 여기에서 Promise 객체에 접근할 수 있다면?

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

주석이 쓰여진 위치에서는 앱에서 보낸 메시지의 타입과 데이터에 접근할 수 있다. 즉 해당 위치에서 Promise 객체에 접근하여 사진 데이터를 resolve의 인자로 넘겨준다면 최종적으로 postMessage가 사진 데이터를 반환할 수 있게되는 것이다.

이 때 postMessagereceiveMessage라는 서로 다른 함수에서 같은 Promise 객체를 공유할 어떠한 공간이 필요한데, 이를 위해서 사용한 것이 observer 패턴이 되겠다.
사내 코드에 적용된 부분이라 더 디테일하게 설명하기는 어렵지만, 간략하게 설명하자면 postMessage에서는 옵저버에 객체를 등록하고 receiveMessage 에서는 타입을 활용하여 해당 타입에 등록되어있는 Promise 객체들을 종료시키는 구조이다.

profile
hello world!

0개의 댓글