PWA InApp 설치 버튼 만들기

Ji-Heon Park·2024년 5월 17일
3

TmaxRG

목록 보기
3/10
post-thumbnail

0. PWA란 무엇인가?

PWA(Progressive Web App)는 웹과 네이티브 앱의 이점을 갖도록 개발된 웹 앱입니다. 사용자에게 네이티브 앱과 유사한 경험을 제공하며, 앱을 다시 다운로드/업데이트할 필요 없이 사용할 수 있습니다.

1. React InApp 버튼으로 설치 프롬프트 제공하기

일반적으로 PWA는 브라우저의 URL 바 아래에 '설치'(사진 속 URL바의 우측 첫번째 버튼) 버튼을 제공하지만, 사용자가 이를 인식하고 활용하기 까지는 힘듭니다.

현재 진행중인 프로젝트에서 사용자가 쉽게 이해하고 실행할 수 있도록 설치 버튼을 직접 구현하기로 했습니다.

웹 내에서 설치 프롬프트를 제공하기 위해서 beforeinstallprompt 이벤트를 활용합니다. beforeinstallprompt 이벤트는 웹 브라우저에서 Progressive Web App(PWA)이 사용자의 디바이스에 설치될 수 있는 조건을 만족시킬 때 발생하는 이벤트입니다.

MDN에서 제공하는 예제를 확인해보면 beforeinstallprompt를 활용하여 버튼을 통해 설치 프롬프트를 제공합니다.

이를 리액트 코드로 변환한 예제는 다음과 같습니다:

const [deferredPrompt, setDeferredPrompt] = useState(null);

const handleBeforeInstallPrompt = (event) => {
  event.preventDefault();
  setDeferredPrompt(event);
};

useEffect(() => {
  window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);

  return () => {
    window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
  };
}, []);

const handleInstallClick = () => {
  // deferredPrompt가 null이 아닌지 확인
  if (deferredPrompt) {
    deferredPrompt.prompt();

    // 사용자의 응답을 기다림
    deferredPrompt.userChoice.then((choiceResult) => {
      if (choiceResult.outcome === 'accepted') {
        console.log('User accepted the A2HS prompt');
        setDeferredPrompt(null);
      } else {
        console.log('User dismissed the A2HS prompt');
      }
    });
  }
};

return (
  <div>
    {deferredPrompt && (
      <button onClick={handleInstallClick}>
        Install App
      </button>
    )}
  </div>
);

2. 리액트에서 Dynamic Import와의 충돌

문제상황

현재 프로젝트에서 beforeinstallprompt 이벤트 객체가 넘어오지 않는 장애가 발생했습니다. 정적으로 생성된 HTML에서는 문제없이 작동했기 때문에, 리액트의 구조적 특성 때문에 발생하는 문제라고는 생각하지 않았습니다. 특히, 앱의 최상단 컴포넌트인 App.tsx에서는 이벤트 객체를 정상적으로 캡처할 수 있었습니다.

문제는 리액트의 라우터 기반 Suspense와 lazy loading이 적용된 컴포넌트에서 발생했습니다. 이 컴포넌트들을 일반적인 import 방식으로 변경하자, beforeinstallprompt 이벤트 객체를 문제없이 읽을 수 있었습니다.

문제의 원인

There's no guaranteed time this event is fired, but it usually happens on page load.

beforeinstallprompt 이벤트의 실행 시기는 페이지의 load 시점입니다. 그러나 리액트의 dynamic import를 사용하는 경우, 최초의 load 시점에 이미 이벤트가 발생한 후 컴포넌트가 마운트됩니다. 이 때문에 lazy loaded 컴포넌트들은 이 이벤트를 놓치고 있었습니다.

해결 방안

lazy import 기능을 제거하는 것은 PWA 기능 때문에 많은 이점을 포기하는 것이므로, 다른 해결책을 찾았습니다. 이벤트를 캡처하는 로직을 최상단 컴포넌트(lazy loading을 적용받지 않는)에 배치하고, 이벤트 객체를 전역 상태로 관리하여 하위 컴포넌트에 프로바이더로 전달하였습니다.

const PromptContext = React.createContext();

// 최상위 컴포넌트에서 Context를 제공
const PromptProvider = ({ children }) => {
  const [deferredPrompt, setDeferredPrompt] = useState(null);

  useEffect(() => {
    const handler = event => {
      event.preventDefault();
      console.log('Event captured');
      setDeferredPrompt(event);
    };

    window.addEventListener('beforeinstallprompt', handler);

    return () => {
      window.removeEventListener('beforeinstallprompt', handler);
    };
  }, []);

  return (
    <PromptContext.Provider value={deferredPrompt}>
      {children}
    </PromptContext.Provider>
  );
};

// 하위 컴포넌트에서 Context를 사용
const SomeComponent = () => {
  const deferredPrompt = useContext(PromptContext);

  const handleInstallClick = () => {
    if (deferredPrompt) {
      deferredPrompt.prompt();
      // ...
    }
  };

  return (
    <div>
      {deferredPrompt && <button onClick={handleInstallClick}>Install App</button>}
    </div>
  );
};

이 방식은 최상위 컴포넌트에서 beforeinstallprompt 이벤트를 캡처하고 전역 상태를 통해 이를 관리함으로써, 모든 관련 컴포넌트에서 이벤트에 접근할 수 있도록 합니다. 이를 통해 lazy loading의 이점을 유지하면서도 InApp 버튼을 통한 설치 프롬프트 기능을 제공할 수 있습니다.

3. 동적 URL 지원을 위한 Dynamic Manifest 생성

문제상황

해당 PWA 인앱 설치의 요구조건은 PWA로 접속 시 해당하는 학교의 URL로 진입하도록 구현해야합니다.

e.g. https://서비스_주소/live-class?school={학교ID}

초기에는 사용자의 로컬 스토리지에 학교의 ID 값을 저장하고, PWA로 접속 시 값을 불러와 해당 경로로 리디렉션하였습니다.

  const isPWA = useMediaQuery({ query: '(display-mode: standalone)' });

 useEffect(() => {
    const PWA_URL = localStorage.getItem(PWA_URL_KEY);
   
    if (isPWA && PWA_URL && window.location.pathname === '/') {
      PWA_URL_REDIRECTION();
    }
  }, [isPWA]);

그러나 iOS 기기에서는 브라우저와 별도의 앱 공간을 사용하여 웹 스토리지를 공유하지 않는 문제가 있습니다.

All iOS browsers are basically a Safari with different interface and hence inherently preventing any type of data sharing between PWA and the browser.

해결 방안: 동적 Manifest 파일 생성

문제를 해결하기 위해 매니페스트 파일을 동적으로 생성하여 웹페이지에 추가하는 방법을 사용했습니다. 다음과 같이 index.html에 manifest를 생성하여 웹페이지에 추가하는 스크립트를 사용했습니다.

<script>
  const manifest = {
    name: 'My App',
    short_name: 'My App',
    display: 'standalone',
    theme_color: '#ffffff',
    background_color: '#ffffff',
    icons: [
      { src: `${window.location.origin}/icon-192x192.png`, sizes: '192x192', type: 'image/png' },
    ],
    start_url: window.location.href
  };
  const link = document.createElement('link');
  link.rel = 'manifest';
  link.href = `data:application/json;base64,${btoa(
  unescape(encodeURIComponent(JSON.stringify(manifest)))
)}`;
</script>
  1. 현재 위치 기반으로 manifest 객체 생성 (start_url을 통해 리디렉션 경로를 현재 위치로 설정)
  2. 새로운 링크 요소 생성
  3. 링크의 href 속성을 매니페스트 객체를 JSON 문자열로 변환하고, 이를 Base64로 인코딩하여 데이터 URL로 설정

이 방법은 매니페스트 파일을 별도의 파일로 호스팅하지 않고, 직접 링크에 매니페스트 데이터를 포함시킬 수 있습니다.

이를 통해 iOS에서의 스토리지 미공유 문제를 해결하고, PWA의 시작 경로를 유연하게 지정할 수 있도록 했습니다.

References

profile
Frontend Developer | 기록되지 않은 것은 기억되지 않는다

0개의 댓글

관련 채용 정보