react native codepush 업데이트 다운도중 앱이 종료되면 롤백되는 이슈 해결

박은정·2023년 4월 19일
1

리액트네이티브

목록 보기
17/24

이전글에서 롤백되는 이슈를 해결하려 했지만 잘 안됐습니다.

그래서 react native codepush 이슈채널에서 rollback 키워드를 바탕으로 검색했는데, How to get the reason for the Rollbacks? 이슈채널에서 어느 분이 코드푸시의 롤백 동작원리 에 관한 코멘트를 공유한 내용을 발견했습니다.

코드푸시 롤백 동작원리

코드푸시는 오류 등을 추적하지 않으며, 업데이트가 성공했는지 여부를 codePush.notifyAppReady 혹은 codePush.notifyApplicationReady 가 호출되었는지로 판단합니다.
해당 메서드는 업데이트를 네이티브 측에서 성공적으로 설치된 것으로 표시하고, 앱이 재시작되었을때 성공적으로 설치되었으면서 보류중인 업데이트가 있는 경우 롤백처리를 합니다.
sync 메서드를 사용한 경우 업데이트 메커니즘의 일부로 이 작업이 자동으로 설치됩니다.

일반적으로 잘못된 번들 실행으로 인해 번들 초기화의 버그로 발생할 수 있습니다.
만약 다른 조건에서 앱 롤백을 처리하려면 업데이트를 수동으로 설치해야 하며, 이에 대한 추가 옵션을 제공할 필요가 없습니다.

결론

위 내용을 바탕으로 생각하면, 앱이 다운되기 전에 앱을 종료하면 코드푸시측에서는 실패로 간주하고 롤백처리를 하기 때문에 해당 이슈가 발생한 것 같습니다.
그래서 codePush(codepushOptions)(App) 으로 감싸서 sync 메서드가 호출되지 않게 하고, 수동으로 업데이트 내용을 확인하고 다운받고 설치하고 패치하는 과정이 필요하다 생각됩니다.

수동으로 업데이트 확인▪다운▪설치▪패치하기

시나리오

  1. 앱 실행
    1-1. 앱버전 및 코드푸시버전 확인
    1-2. 코드푸시의 업데이트 내용이 있다면 확인▪다운▪설치▪패치
  2. 백그라운드 전환시, 코드푸시 업데이트사항 체크하고 저장
    2-1. 백그라운드에서 포그라운드로 전환시, 저장된 코드푸시 업데이트사항을 바탕으로 확인▪다운▪설치▪패치

checkAppVersion : 앱버전 확인

최신앱버전인 경우 코드푸시버전 체크

import VersionInfo from 'react-native-version-info';
const checkAppVersion = () => {
  const remoteVersion = Number(서버에저장된앱버전.replace(/\./g, ''));
  const localVersion = Number(VersionInfo.appVersion.replace(/\./g, ''));
  if (remoteVersion <= localVersion) {
    checkCodepush();
  	return;    
  }
  // 구글플레이스토어 및 앱스토어로 페이지이동 후 앱종료
}

checkCodepush : 코드푸시버전 확인

const checkCodepush = async () => {
  try {
    setTimeout(() => {
      if (update === undefined) getCodepushState(); // 1
    }, 10000);
    const update = await codePush.checkForUpdate(); 
    codepushRef.current = update; // 2
    getCodepushState(update); // 3
    updateCodepush(update); // 4
  } catch (e) {
      console.log(e);
      getCodepushState();
    }
};
  1. codePush.checkForUpdate() : 코드푸시서버에 업데이트내용이 있는지 요청합니다.
    이때 코드푸시서버가 이상이 있으면 3분동안 확인이 안되는 경우가 있기 때문에 setTimeout으로 10초동안 대기했을때에도 업데이트내용을 확인하지 못하면 getCodepushState 함수에 undefined값을 전달합니다.
  2. codepushRef에 서버에서 받은 update값을 저장합니다.

useRef를 사용하는 이유는??

코드푸시서버가 다행히 문제가 없다면

  1. getCodepushState 함수에 update값을 전달해서 업데이트상태를 확인합니다.
  2. updateCodepush : 업데이트상태에 따라 코드푸시업데이트 진행

getCodepushState : 업데이트 상태 확인

    const getCodepushState = update => {
        if (codepushRef.currnet === undefined && update === undefined) {
            setCodepushState('SERVER_ERROR'); // 굳이 필요없음
            return 'SERVER_ERROR'; // update값 받아오지 못함
        }
        if (!update) {
            setCodepushState('UPTODATE'); 
            return 'UPTODATE'; // 최신상태
        }
        if (update.isMandatory) {
            setCodepushState('FORCED');
            return 'FORCED'; // 필수업데이트
        }
        setCodepushState('OPTIONAL');
        return 'OPTIONAL'; // 선택업데이트
    };

updateCodepush 함수 내부에서 await getCodepushState(codepushRef.current) 값으로 업데이트사항 체크해서 setCodepushState 처리를 하지 않아도 되지만
앱을 켰을때 인트로화면 다음으로 바로 보이는 홈화면에서 모달창이 나오는게 있어서, 업데이트 Alert를 보여주는 동안은 해당 모달창이 나오지 않게 하기 위해 codepushState 상태값을 추가했습니다.

updateCodepush: 코드푸시 상태에 따라 업데이트진행

const updateCodepush = async () => {
  // codepushState가 반영되지 않아서 별도로 호출해서 업데이트상태확인
  const _codepushState = await getCodepushState(codepushRef.current);
  try {
    switch (_codepushState) {
      case 'UPTODATE':
        // 최신버전일때 로직
       	break;
      case 'FORCED':
        Alert.alert('업데이트', '필수업데이트입니당', [
          { text: ' 확인', onPress: downloadCodepush },
        ]);
        break;
      case 'OPTIONAL':
        Alert.alert('업데이트', '나중에 업데이트해도 되긴해요.', [
          { text: '나중에', onPress: () => setCodepushState('LATER') },
          { text: '확인', onPress: downloadCodepush },
        ]);
        break;
      default:
        break;
      }
  } catch (e) { console.log(e); }
};

ReactNative Codepush 개선하기 글에서 보면 선택업데이트와 필수업데이트에서 패치하는 옵션이 각각 다르긴 한데, 저의 경우 선택업데이트인 경우 Alert에서 "나중에" 라는 버튼이 생긴것말고는, 선택이던 필수던 반드시 바로 적용되어야 한다고 생각했기 때문에 동일한 downloadCodepush 함수를 호출합니다.

downloadCodepush: 코드푸시 업데이트사항 다운 및 설치

const downloadCodepush = async () => {
    try {
        codepush.current
            .download(progress => {
          		// 업데이트 패키지 다운로드 진행중
                setGage(progress); // 다운로드상태
                setMsg('업데이트 리소스를 다운로드중입니다');
            })
            .then(newPackage => {
          		// 다운받은 업데이트 패키지 설치중
                setMsg('업데이트 리소스를 적용중입니다.');
                newPackage.install().done(() => {
                  	// 설치완료
                    setMsg('적용 완료! 곧 앱이 재실행됩니다.');
                  	// 코드푸시 서버에 업데이트사항 잘 적용했다고 전달 (안하면 롤백처리됨)
                    codePush.notifyAppReady(); 
                    codePush.restartApp(); // 변경사항 적용하기 위해 앱 재시작
                });
            });
    } catch (e) {
        console.log(e);
    }
};

다운로드되고 설치되는 시간이 짧지만은 않기 때문에 상황에 따라 유저에게 진행상황을 보여주게 설정했습니다.

CodepushLoading 컴포넌트 : 유저에게 코드푸시 업데이트 진행상황 보여줌

import * as Progress from 'react-native-progress';

const CodepushLoading = ({ msg, gage, codepushState }) => {
    const dispatch = useDispatch();
  
  	// 코드푸시 업데이트 다운로드 상황 0~1사이의 소수점으로 반환
    const getProgress = () => {
        if (gage.receivedBytes <= 0) return 0;
        return Number((gage.receivedBytes / gage.totalBytes).toFixed(2));
    };

    // codepushState가 변경될때마다 redux로 코드푸시상태 변경
    // 홈화면에 나오는 모달창 제어하기 위해 사용 (필수사항아님)
    useEffect(() => {
        if (codepushState) dispatch(changeState(codepushState));
    }, [codepushState]);
  
  	// 0<진행률<1 일때만 페이지 보여줌
    if (!getProgress() || getProgress() >= 1) return null;
    return (
        <View style={[StyleSheet.absoluteFill, styles.bg]}>
            <Progress.Bar
                progress={getProgress()}
                width={200} // 너비
                borderWidth={0}
                borderRadius={4} 
                color='royalblue' // 색상
                useNativeDriver
            />
            <Text>{msg}</Text>
        </View>
    );
};

const styles = StyleSheet.create(
  bg: {
    flex: 1,
    backgroundColor: 'white',
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 100,
  }
)

downloadCodepush 에서 업데이트 패키지를 다운하고 설치하는 동안 gage, msg 상태값이 변경되었는데 앱의 최상단에서 해당 컴포넌트로 유저에게 보여줍니다.
이때 codepushState 상태값이 변경될때마다 redux를 사용해서 상태값을 변경해서 홈화면에서 OPTIONAL, FORCED 일때는 모달창을 보여주지 않다가
선택업데이트일때 Alert창에서 나중에버튼을 누르면 LATER 이거나, 최신버전 UPTODATE, 서버이상이어서 업데이트상태확인 못한 경우 SERVER_ERROR 일때는 모달창을 띄워주면서 활용합니다.

코드푸시 서버이상 테스트

결론적으로 코드푸시 로직을 아래와 같은 이유로 수정했습니다.

  1. 업데이트사항 다운로드 중 앱 종료하면 롤백 → 롤백되지 않고 다시 codePush.checkForUpdate() 호출 확인
  2. codePush.checkForUpdate() 호출했을때 코드푸시서버가 문제가 생기면 3분이 지나도 대기중이라 앱 실행이 안됨

1번의 경우는 금방 테스트할 수 있었지만, 2번의 경우 코드푸시 서버가 언제 장애가 생길지 모르기 때문에 codePush.checkForUpdate() 작업이 오래걸리도록 했습니다.

자바스크립트에서 setTimeout 을 한다고 해도 기다리는 것이 아니라 다음 코드를 진행하고 setTimeout 콜백함수를 실행하기 때문에 제가 원하는 상황이 아니었습니다.

그러던 중, 자바스크립트에서 프로그램의 실행을 지연시키기 라는 글에서 아래와 같은 sleep 함수를 찾았습니다.

function sleep(ms) {
  console.log(`${ms/1000} 초 sleep`);
  const wakeUpTime = Date.now() + ms;
  while (Date.now() < wakeUpTime) {}
}

checkCodepush 함수 내부에서 테스트

const checkCodepush = async () => {
  try {
    setTimeout(() => {
      if (update === undefined) getCodepushState();
    }, 10000);
    const update = await codePush.checkForUpdate(); 
    sleep(20000); // 20초동안 upate내용 받지못함
    codepushRef.current = update; 
    getCodepushState(update);
    updateCodepush(update);
  } catch (e) {
      console.log(e);
      getCodepushState();
    }
};

20초동안 코드푸시서버에서 업데이트내용을 받지못한다고 가정했을때, 10초동안 기다려도 update는 undefined로 더 이상 기다리지 않고, codepushState가 SERVER_ERROR로 판단되었습니다.

profile
새로운 것을 도전하고 노력한다

0개의 댓글