이전글에서 롤백되는 이슈를 해결하려 했지만 잘 안됐습니다.
그래서 react native codepush 이슈채널에서 rollback 키워드를 바탕으로 검색했는데, How to get the reason for the Rollbacks? 이슈채널에서 어느 분이 코드푸시의 롤백 동작원리 에 관한 코멘트를 공유한 내용을 발견했습니다.
코드푸시는 오류 등을 추적하지 않으며, 업데이트가 성공했는지 여부를 codePush.notifyAppReady
혹은 codePush.notifyApplicationReady
가 호출되었는지로 판단합니다.
해당 메서드는 업데이트를 네이티브 측에서 성공적으로 설치된 것으로 표시하고, 앱이 재시작되었을때 성공적으로 설치되었으면서 보류중인 업데이트가 있는 경우 롤백처리를 합니다.
sync
메서드를 사용한 경우 업데이트 메커니즘의 일부로 이 작업이 자동으로 설치됩니다.
일반적으로 잘못된 번들 실행으로 인해 번들 초기화의 버그로 발생할 수 있습니다.
만약 다른 조건에서 앱 롤백을 처리하려면 업데이트를 수동으로 설치해야 하며, 이에 대한 추가 옵션을 제공할 필요가 없습니다.
위 내용을 바탕으로 생각하면, 앱이 다운되기 전에 앱을 종료하면 코드푸시측에서는 실패로 간주하고 롤백처리를 하기 때문에 해당 이슈가 발생한 것 같습니다.
그래서 codePush(codepushOptions)(App)
으로 감싸서 sync
메서드가 호출되지 않게 하고, 수동으로 업데이트 내용을 확인하고 다운받고 설치하고 패치하는 과정이 필요하다 생각됩니다.
최신앱버전인 경우 코드푸시버전 체크
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;
}
// 구글플레이스토어 및 앱스토어로 페이지이동 후 앱종료
}
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();
}
};
codePush.checkForUpdate()
: 코드푸시서버에 업데이트내용이 있는지 요청합니다.setTimeout
으로 10초동안 대기했을때에도 업데이트내용을 확인하지 못하면 getCodepushState
함수에 undefined값을 전달합니다.useRef를 사용하는 이유는??
코드푸시서버가 다행히 문제가 없다면
getCodepushState
함수에 update값을 전달해서 업데이트상태를 확인합니다.updateCodepush
: 업데이트상태에 따라 코드푸시업데이트 진행 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
상태값을 추가했습니다.
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
함수를 호출합니다.
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);
}
};
다운로드되고 설치되는 시간이 짧지만은 않기 때문에 상황에 따라 유저에게 진행상황을 보여주게 설정했습니다.
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
일때는 모달창을 띄워주면서 활용합니다.
결론적으로 코드푸시 로직을 아래와 같은 이유로 수정했습니다.
codePush.checkForUpdate()
호출 확인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) {}
}
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로 판단되었습니다.