분류 Reactjs

푸시 React 네이티브 (코드)에 올바른 방법

컨텐츠 정보

  • 조회 495 (작성일 )

본문

1*INcX8_J11DoFP9uoRoWaBQ.jpeg2018 년 중반에 팀과 저는 Fundbox의 모바일 앱을 재 구축하는 임무를 맡았습니다. 

우리가 선택한 스택은 프레임 워크로서 React Native였습니다. Mobx가 상태 관리 라이브러리로 보완했습니다. 

또한 앱을 다른 상점에 제출하지 않고도 새로운 기능을 추가하고 버그를 신속하게 수정할 수 있는 기능을 원했습니다. 

우리는 CodePush가 정확히 필요한 것으로 나타났습니다.


https://medium.com/fundbox-engineering/the-right-way-to-code-push-react-native-cba82d0f8ec9 


지금까지 우리는 150 개 이상의 성공적인 CodePush 릴리스를 통해 iOS 및 Android 플랫폼 모두에 배포 할 수 있었습니다.

이 글에서는 CodePush에 대한 Fundbox의 접근 방식과 React Native 앱에서 이를 구현하는 방법을 공유 할 것입니다.


CodePush 개요 


CodePush를 사용하면 응용 프로그램의 코드를 변경하고 릴리스하며 사용자가 거의 즉시 새 버전을 얻을 수 있습니다. 최종 사용자가 앱 스토어에서 새 버전으로 업그레이드 할 필요는 없습니다.


아래에서 논의 할 특정 유형의 파일 만 업데이트 할 수 있다는 점을 명심해야 합니다.


모바일 앱의 지속적인 배포 


처음으로 React Native 앱을 처음 출시했을 때 Fundbox에서 웹 응용 프로그램을 배포하는 방식과 유사하게 앱에 대한 배포 방식의 지속적인 업데이트를 원했습니다. 따라서 모든 CodePush 업데이트가 필수로 구성되도록 결정했습니다. 

따라서 업데이트가 표시되면 즉시 설치됩니다. 새로운 JS 번들을 사용할 수 있을 때 사용자가 이전 JS 번들을 실행할 이유가 없으며 관리하기도 더 쉽습니다.


CodePush의 초기 구현은 앱 구성 요소를 감싸는 간단한 주문 구성 요소였습니다. 다음과 같이 보였습니다 :


import React from "react";
import { AppRegistry } from "react-native";
import codePush from "react-native-code-push";
import { Provider } from "mobx-react/native";
import Stores from "./src/stores";
import App from "./src/App";
const app = () => (
<Provider {...Stores}>
<App />
</Provider>
);
AppRegistry.registerComponent("Fundbox", () => codePush(app));


필수 업데이트가 제공되면 내부 CodePush 동기화 프로세스가 시작되어 새 릴리스를 다운로드하고 설치하며 완료되면 업데이트 된 새 JS 번들로 앱이 다시 시작됩니다. 이 구현은 사용자 친화적이 아닙니다. 이유를 설명하겠습니다.


런타임에 원하지 않는 UI 업데이트 


많은 사용자가 보게 되는 첫 번째 화면은 Fundbox 계정의 자격 증명을 요청하는 로그인 화면입니다. 초기 순진한 구현으로 CodePush가 릴리스 된 후 앱을 열고 자격 증명을 입력하기 시작한 사용자가 갑자기 새로 고침을 경험했습니다. 입력 한 자격 증명이 지워지고 다시 입력해야 했습니다. 물론 그것은 CodePush의 비동기 성 결과였습니다. 사용자가 UI에 참여하는 동안 업데이트 된 번들이 다운로드되어 설치되었습니다.


런타임시 UI 업데이트 방지 


이 문제를 해결하기 위해 업데이트 상태의 변화에 ​​대한 Mobx의 react과 함께 CodePush의 동기화 방법을 사용했습니다.


// @flow
import React, { Component } from "react";
import CodePush from "react-native-code-push";
import type { SyncStatus } from "react-native-code-push";
import { reaction } from "mobx";
import { inject, observer } from "mobx-react/native";
import RootStack from "./Router";
import * as T from "./App.types";
@inject("DeviceStore", "NavigationStore")
@observer
export default class App extends Component<T.Props> {
async componentDidMount() {
const update = await CodePush.checkForUpdate();
// If mandatory update -> Init CodePush sync flow
if (update && update.isMandatory) {
const { IMMEDIATE } = CodePush.InstallMode;
// React to the sync status changes
reaction(this.getCodePushStatus, this.handleCodePushStatusChange);
return await CodePush.sync(
{ installMode: IMMEDIATE },
status => {
this.props.DeviceStore.codePushStatus = status;
},
() => {} // Don't remove this function (github.com/Microsoft/react-native-code-push/issues/516)
);
}
// Else -> Notify the app is ready
this.props.DeviceStore.appReady = true;
}
getCodePushStatus = () => {
return this.props.DeviceStore.codePushStatus;
};
handleCodePushStatusChange = (status: SyncStatus) => {
const { UP_TO_DATE, UPDATE_IGNORED } = CodePush.SyncStatus;
if ([UP_TO_DATE, UPDATE_IGNORED].includes(status)) {
// CodePush sync flow done -> Notify the app is ready
this.props.DeviceStore.appReady = true;
}
};
render() {
return (
<RootStack
onNavigationStateChange={(prevState, newState) => {
this.props.NavigationStore.setAppNavState(newState);
}}
ref={navigatorRef => {
this.props.NavigationStore.setTopLevelNavigator(navigatorRef);
}}
/>
);
}
}


그런 다음 LaunchRoute.js에서 CodePush 동기화 상태 업데이트를 듣습니다. 동기화가 완료되면 appReady Observable이 true로 설정되고 사용자가 자격 증명을 입력 할 수 있는 인증 경로로 사용자를 탐색 할 수 있습니다.


// @flow
import React, { Component } from "react";
import { reaction } from "mobx";
import { observer, inject } from "mobx-react/native";
import CodePush from "react-native-code-push";
import { View, Image, Animated } from "react-native";
import { styles } from "./LaunchRoute.style";
import * as T from "./LaunchRoute.types";
import SplashScreen from "react-native-splash-screen";
import splashAnimated from "../../../assets/images/common/splash/SplashAnimated.gif";
import TextComponent from "../../components/TextComponent/TextComponent";
@inject("NavigationStore", "DeviceStore")
@observer
export default class LaunchRoute extends Component<T.Props, T.State> {
state = {
isLoading: true,
codepushStatus: new Animated.Value(0)
};
async componentDidMount() {
// Make sure app is ready before going to the appropriate screen
if (this.props.DeviceStore.appReady) {
return this.props.NavigationStore.navigate(“auth”);
}
reaction(this.getAppReadyState, this.handleAppReadyStateChange);
}
getAppReadyState = () => {
return this.props.DeviceStore.appReady;
};
handleAppReadyStateChange = appReady => {
if (appReady) {
this.props.NavigationStore.navigate(“auth”);
}
};
splashAnimationLoaded() {
SplashScreen.hide();
}
renderSplashAnimation = () => {
if (this.state.isLoading) {
return (
<Image
onLoadEnd={this.splashAnimationLoaded}
source={splashAnimated}
resizeMode={"contain"}
/>
);
}
};
animateLoader(value) {
Animated.timing(this.state.codepushStatus, {
toValue: value,
duration: 500
}).start();
}
getCodePushStatus() {
switch (this.props.DeviceStore.codePushStatus) {
case CodePush.SyncStatus.UPDATE_INSTALLED:
this.animateLoader(1);
return "Updates installed";
case CodePush.SyncStatus.INSTALLING_UPDATE:
this.animateLoader(1);
return "Installing updates...";
case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
this.animateLoader(1);
return "Downloading updates...";
default:
this.animateLoader(0);
return "";
}
}
renderCodePushLoadingState() {
return (
<View style={styles.loaderWrapper}>
<Animated.View
style={[styles.loader, { flex: this.state.codepushStatus }]}
/>
</View>
);
}
render() {
return (
<View style={styles.wrapperStyle}>
{this.renderSplashAnimation()}
<TextComponent style={styles.codePushStatus}>
{this.getCodePushStatus()}
</TextComponent>
{this.renderCodePushLoadingState()}
</View>
);
}
}


이제 사용자가 앱을 열면 동기화 프로세스가 완료된 후에 만 ​​스플래시 화면이 탐색 됩니다.


전통적인 방출 흐름 


모바일 앱을 출시했거나 웹 개발에서 CI / CD를 연습 한 경우 일반적으로 다음 단계를 포함하는 배포 흐름이 있을 수 있습니다.

  1. 마스터 지점 (모바일 앱의 버전 지점)에서 기능 지점으로 분기 합니다.
  2. 일부 코드 변경 사항을 커밋하십시오.
  3. 테스트를 검토하고 수행하십시오.
  4. 기능 분기를 앱의 새 버전으로 해제하십시오.
  5. 마스터 브랜치로 다시 병합하십시오.

생성 된 ipa / apk 파일이 코드 변경 사항과 함께 제공되므로 모바일 앱과 마찬가지로 웹 개발에도 효과적입니다. 

그러나 CodePush 릴리스의 경우 이 흐름으로는 충분하지 않습니다. 왜 그런지 보자 ...


CodePush 릴리스주의 사항 


앞에서 CodePush 릴리스는 특정 파일 형식 만 번들로 제공한다고 언급했습니다. React Native 앱에는 다양한 UI 구성 요소 및 해당 기능을 지원하는 기타 JS 파일이 포함됩니다. 따라서 기본 코드에 대한 AppDelegate.m, MainActivity.java 로의 변경 또는 새 플러그인 추가는 CodePush 번들에 포함되지 않습니다.

추가 한 새 플러그인과 무관 한 기본 코드를 실행하는 클라이언트를 생각해보십시오. 새로 추가 된 플러그인을 호출하려고 하는 CodePush 번들을 배포하면 앱이 중단됩니다.


다음 CodePush 릴리스에서 앱이 중단되지 않도록 하려면 어떻게 해야 합니까?


견고한 CodePush 릴리스 흐름 


CodePush 릴리스는 위에서 설명한 기존 흐름과 다른 방식으로 테스트 및 배포해야 합니다.


목표는 사용자가 새로운 CodePush 릴리스 업데이트를 경험하는 방식을 테스트하는 것입니다. 

그러기 위해서는 기능 분기를 전용 준비 / 테스트 CodePush 환경으로 푸시하거나 코드 푸시를 정확하게 수행해야 합니다. 

그런 다음 기능 분기 자체에서 테스트를 수행하는 대신 릴리스 된 기본 코드를 나타내는 분기에 대해 테스트해야 합니다.


사용자는 현재 처음 분기 한 분기의 번들 출력을 실행하고 있음을 기억하십시오. 

코드를 변경하기 전에 이것이 버전 분기입니다. 

기능 분기를 준비 / 테스트 CodePush 환경으로 코드 푸시 한 후에는 사용자가 경험하는 것처럼 들어오는 CodePush 번들의 경험을 시뮬레이션 할 수 있습니다.


예를 들면 다음과 같습니다.


CodePush 번들 버전 1과 함께 애플리케이션 (v1.0.0)이 릴리스되었다고 가정합니다. 이것은 v1.0.0–1로 설명 될 수 있습니다.

  • 이제 일부 코드를 변경 한 후 CodePush 버전 2를 릴리스 하려고 합니다. 또한 CodePush 릴리스가 라이브 v1.0.0–1을 위반하지 않도록 하려고 합니다.
  • v1.0.0–1 분기에서 기능 1.0.0–2 분기로 분기 합니다.
  • 일부 코드 변경 사항을 커밋하십시오. 기본 변경 사항이 도입되면 나중에 테스트를 실행하는 동안 앱이 중단됩니다.
  • 테스트를 검토하고 수행하십시오.
  • 1.0.0–2 기능 분기에서 CodePush 준비 / 테스트 릴리스를 만듭니다.
  • 사용자와 동일한 코드베이스를 실행하려면 로컬 지점을 1.0.0–1로 다시 전환하십시오.
  • 정리 — 프로젝트를 빌드 하기 전에 설치 가능한 모든 플러그인과 캐시 된 파일을 제거하는 방법입니다.
cd <project_root># cleaning node_modules and cache:
watchman watch-del-all
rm -rf ~/.rncache node_modules $TMPDIR/react-* $TMPDIR/metro* $TMPDIR/haste-*
npm i
# iOS:
rm -rf ios/Podfile.lock ios/Pods ios/build
cd ios && pod install
cd <project_root>
# Android:
rm -rf android/app/build android/.gradle
# Running Metro bundler with reset-cache:
npm start — — reset-cache
  • 준비 / 테스트 환경에서 앱을 실행합니다. 이전에 출시 한 새로운 CodePush 번들이 다운로드 되어 설치됩니다.
  • 테스트를 실행하십시오. 모든 것이 통과되면 견고한 CodePush가 릴리스 된 것입니다.
  • CodePush 릴리스를 프로덕션으로 승격 하십시오.

마무리 


CodePush는 버그를 수정하고 코드를 변경하는 데있어 충분한 공간을 제공합니다. 코드 업데이트를 처리 할 수 있는 유연성이 뛰어나고 설치시기를 제어 할 수 있습니다. 

다시 말해, 복잡성의 또 다른 차원을 추가합니다. 따라서 사용자가 향후 릴리스 업데이트를 경험하는 방식을 시뮬레이션하고 신중하게 테스트해야 합니다.