고윤태의 개발 블로그

누구나 접근하기 쉽지만 얻어 가는 것이 있는 글을 쓰기 위해 노력 중입니다.

개발

useCodePush 구현하기

고윤태 2023. 4. 24. 14:49

구현하게 된 계기

이번에 근무하고 있는 회사에서는 React-Native를 사용하고 있습니다.
하지만 React-Native의 가장 큰 장점인 CodePush를 사용하지 않고 업데이트마다 매번 앱, 플레이 스토어의 심사를 거치고 있는 상황이었습니다.

너무 비효율적이다 생각한 저는 의견을 제시하여 바로 CodePush를 도입을 하게 되었고 제가 직접 반영하게 되었습니다.

CodePush를 반영하고 난 뒤 사용자의 CodePush의 업데이트를 관리하는 custom hook을 만들자 해서 구현하게 되었습니다.

 

사실 주원인은 디자이너 분이 기본 제공하는 Dialog가 예쁘지 않다고 말씀하셔서 customModal을 쓰기 위해 구현했습니다.

구상

구현을 위해 제가 생각한 포인트 관점입니다. 
이 포인트에 맞춰서 개발을 진행했습니다.

  • 앱 버전 업데이트가 필요한 유저
    • 앱, 플레이 스토어로 이동시키기
  • CodePush(선택적) 업데이트가 필요한 유저
    • 선택적 업데이트로 "다음에요"라는 버튼 클릭 시 업데이트를 다음에 앱을 실행한 경우 다시 뜨게 하기
  • CodePush(강제적) 업데이트가 필요한 유저
    • 필수 업데이트 진행 후 앱 재시작

구현

import { useEffect, useState } from 'react';
import CodePush, { DownloadProgress, RemotePackage } from 'react-native-code-push';
import { getVersion } from 'react-native-device-info';
import { Linking, Platform } from 'react-native';

type CodePushState = 'UPTODATE' | 'FORCED' | 'OPTIONAL';
export type CodePushUpdateState = 'READY' | 'PENDING' | 'COMPLETE';

export type CodePushInfoState = {
  type?: CodePushState;
  isUpdating: boolean;
};

const useCodePush = () => {
  const [codePushUpdating, setCodePushUpdating] = useState<CodePushUpdateState>('READY');
  const [syncDownloadProgress, setSyncDownloadProgress] = useState<DownloadProgress>({
    totalBytes: 0,
    receivedBytes: 0,
  });
  const [codePushInfoState, setCodePushInfoState] = useState<CodePushInfoState>({
    isUpdating: false,
  });

  const getCodepushState = (update: RemotePackage | null): CodePushInfoState => {
    if (!update) {
      return { type: 'UPTODATE', isUpdating: false };
    }

    return { type: update.isMandatory ? 'FORCED' : 'OPTIONAL', isUpdating: true };
  };

  const codePushUpdate = async (update: RemotePackage) => {
    setCodePushUpdating('PENDING');
    try {
      update
        .download((progress: DownloadProgress) => {
          setSyncDownloadProgress(progress);
        })
        .then((newPackage: any) => {
          newPackage.install().done(() => {
            CodePush.notifyAppReady();
            CodePush.restartApp();
          });
        });
    } catch (error) {
      // 에러처리
    } finally {
      setCodePushUpdating('COMPLETE');
    }
  };

  const forcedCodepushUpdate = (update: RemotePackage) => {
    codePushUpdate(update);
  };

  const optionalCodepushUpdate = (update: RemotePackage) => {
    codePushUpdate(update);
  };

  const delayUpdate = () => {
    setCodePushInfoState({ isUpdating: false });
  };

  const checkCodePush = async () => {
    try {
      const update = await CodePush.checkForUpdate();
      const codePushState = getCodepushState(update);
      setCodePushInfoState(codePushState);
    } catch (error) {
      //error 처리
    }
  };

  const showAppUpdateAlert = () => {
    //Alert를 이용하여 os에 따라  앱스토어 또는 플레이스토어로 이동하도록 구현했습니다.
  };

  const isSameVersion = (local: string, remote: string): boolean => {
      //현재 사용자의 버전과 앱 스토어에 올라간 버전을 확인하여 앱 업데이트가 필요한지 여부를 체크합니다
  };

  const checkInitApp = async () => {
    const localVersion = getVersion();
    const remoteVersion = getRemoteVersion() //여기에는 구현을 해놓지 않았지만 remote 버전을 불러옵니다.

    const isSame = isSameVersion(localVersion, remoteVersion);

    if (isSame) {
      checkCodePush();
    } else {
      showAppUpdateAlert();
    }
  };

  useEffect(() => {
    checkInitApp();
  }, []);

  return {
    codePushUpdating,
    syncDownloadProgress,
    codePushInfoState,
    forcedCodepushUpdate,
    optionalCodepushUpdate,
    delayUpdate,
  };
};

export default useCodePush;

코드 설명

코드 로직 순서

  1. 최신 버전 앱인지 체크 (N : 앱 업데이트 유도)
  2. CodePush 업데이트 내용이 있는지 체크 (N : 로직 종료)
  3. CodePush 업데이트가 강제인지 선택적인지 체크 후 Modal 노출
  4. 사용자 선택(강제 또는 선택)

이런 식으로 구현했습니다.

추가 설명

delayUpdate 함수에서 isUpdating를 false로 바꾼 이유는 "업데이트가 없는 척하기" 위해서 저런 식으로 구현했습니다.