React Native에 Code-push를 추가해보자.

Gyeop
14 min readFeb 14, 2021

--

블로그에 적는 첫 기술 포스팅이네요. 이번 주제를 작성하게 된 이유는 비즈니스 차원에서 자주 쓰는 기술이기도 하고, 잘 설정되어있는 코드푸시를 배포해보기만 했지, 실제로 추가해본 적이 없어서 정리하는 겸 적게 되었습니다.
잘못된 부분에 대한 지적은 언제나 환영입니다.

코드푸시란 무엇인가?

코드푸시는 Microsoft에서 개발자가 모바일 앱에 대해 사용자의 장치에 직접 배포할 수 있도록 하는 클라우드 서비스입니다. 기존에 앱에 새로운 기능을 추가하는 경우에는 다음과 같은 과정을 거쳐야 했습니다.

앱 버전을 올려 새로 빌드 → 마켓 업로드 → 마켓 검수(1~2일) → 배포

위의 절차가 단순하긴 하지만 작은 기능들이 추가될 때마다, 굉장히 반복적인 작업이 생기는 것이기도 하고, 버전관리에 신경도 써야 하고, 무엇보다도 시간이 걸리는 문제이기 때문에, 개발자의 입장에 있어서 굉장히 피하고 싶은 작업이 되었죠. 또 한 가지 문제는 배포를 하고도 사용자가 마켓에서 업데이트를 해야 했기 때문에, 새로운 기능을 제공하기 위해서는 사용자의 역할까지 포함된다는 점이었습니다.

코드푸시는 이러한 문제점을 해결해줍니다. 빌드를 하지 않아도, 마켓 검수를 거치지 않아도, 사용자가 마켓에서 업데이트를 하지 않아도 새로운 기능을 배포할 수 있습니다. (물론 앱 내부에서 포스업데이트와 비슷한 업데이트가 일어납니다.) 새로운 기능을 업데이트할 때 마다, 마켓 검수가 필요 없어지는 굉장한 장점이 생기게 됩니다. 또한 앱 내부에 생긴 오류들을 빠르게 픽스하여 배포할 수 있기도 하지요. 바로 이런 점이 코드푸시가 주는 이점이 아닐까 생각합니다.

현재 코드푸시를 지원하는 프레임워크로는 JS로 하이브리드 앱을 빌드할 수 있는 아파치 코도바(Cordova)리액트 네이티브(React Native)가 있습니다. 이번 포스팅에서는 리액트 네이티브에 코드푸시를 적용해보도록 하겠습니다.

이번 포스팅에서 적용 환경은 React Native CLI를 이용해서 제작했습니다. RN CLI와 관련된 설명은 여기를 눌러주세요. RN버전은 0.63이며, 0.60 미만은 다른 방법으로 설정해주셔야 합니다.

Sign up Appcenter

먼저 마이크로소프트에서 제공하는 Appcenter에 가입이 필요합니다. 가입하고 나서 나오는 Apps관리 페이지에서 원하는 플랫폼에 따라 앱을 추가해주면 됩니다.

React Native와 같이 크로스 플랫폼을 지원하는 경우에는 iOS와 Android 각각 따로 만들어주어야 합니다.

이후에 해당 앱의 이름을 클릭하면, 여러 가지 관리 시스템이 나오는데, MS의 Appcenter는 코드푸시 뿐만 아니라, 에러 수집 SDK와 빌드, 테스트 등의 기능을 제공하는데, 코드푸시에 관한 부분만 보도록 하겠습니다.

코드푸시 관리 스크린

Distribute 탭에서 CodePush를 누르면 배포현황을 확인할 수 있습니다.
우측 상단에 보면 배포환경도 확인할 수 있는데, 코드푸시에서는 staging과 production 두 개의 환경을 제공합니다.

이후에 나오는 가이드대로 따라 하면 됩니다.

코드푸시 배포를 위한 appcenter-cli 설치

npm install -g appcenter-cli

앱에 코드푸시 SDK 추가

(공식문서)

현재 앱에 react-native-code-push패키지를 추가한 후, iOS를 위해 플러그인을 설치합니다. (pod과 관련된 부분은 이미 셋팅이 되어있다는 가정하에 설명 없이 넘어가겠습니다.)

npm install --save react-native-code-push
cd ios
pod install

Podfile.lock에도 CodePush와 관련된 플러그인이 설치됨을 확인할 수 있습니다. 이후에 AppDelegate.m 파일의 헤더에 아래의 코드를 추가합니다.

#import <CodePush/CodePush.h>

또한 해당 파일의 맨 아래에 보면 sourceURLForBridge 에 관한 브릿지 코드를 볼 수 있습니다.

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

object-c를 몰라도 대충 코드를 슥 읽어보면 디버그 모드가 아닐 때, 무언가를 return하는 코드가 보입니다. 아래의 else 부분의 return값을 다음과 같이 변경해줍니다.

return [CodePush bundleURL];

이후에 Info.plist 파일을 수정해야 하는데, VSCode에서 직접 파일을 수정해도 되지만, 더 안전하게 xcode로 설정을 변경해보겠습니다. xcode로 iOS폴더의 workspace를 오픈한 후에 왼쪽에 디렉토리에 Info.plist를 볼 수 있습니다. 이후 아래와 같이 추가해줍니다. 우측의 Value에는 code push key가 들어있어야 합니다. (저기에 키를 바로 넣어도 상관없지만, 빌드환경마다 변경하면서 관리하기 어렵기 때문에 env파일로 따로 빼서 참조하도록 설정해주면 더 좋습니다. 저는 따로 설정해주었습니다.)

이로써 iOS 셋팅은 끝났습니다.

(안드로이드는 다음에 천천히 알아가는걸로.. )

CLI에 로그인하기

이후 터미널에서 앱센터 위에서 설치한 appcenter-cli 에 로그인해주어야 합니다.

appcenter login

아마 처음 접속한 유저라면, MS에서 정보수집에 관한 권한설정 안내문이 나올 텐데, 선택이기 때문에 읽어보고 넘어가시면 됩니다.

이후에 브라우저에서 앱센터가 열리면서 현재 로그인되어있는 앱센터 계정의 액세스 토큰을 줍니다. 그대로 복사해서 입력해주면 Logged in as Username 이라는 안내가 나옵니다. 이후에 다음 명령어로 확인해봅시다.

appcenter codepush deployment list --app {userName}/{appName} -k
올바르게 로그인 되어서, 잘 나오네요!

코드푸시 업데이트 배포

커맨드라인으로 배포를 해봅시다. 아래의 명령어로 간단히 할 수 있습니다.

appcenter codepush release-react -a {userName}/{appName} -d {Staging|Production}

위의 명령어는 일반적인 코드푸시 업데이트입니다. 강제 업데이트를 실행하고 싶다면 -m (mandatory)을 추가해주면 됩니다.
다만 커맨드라인으로 굉장히 하기 귀찮기 때문에, 배포자동화는 거의 필수입니다. 업데이트 옵션도 다양할 뿐만 아니라, 앱버전을 관리하면 바이너리 판단을 위해 버전정보 설정도 필요해지기 때문에, 실제로 사용한다면 CI를 붙이는 게 빠르게 배포하기에 용이합니다. 특히 CI에는 테스트 후 자동배포 하도록 설정해주면 더욱 안정적으로 배포가 가능해집니다.

+그리고 가끔 코드푸시가 안되는 경우가 있다면 Appcenter의 status를 체크해봅시다.

코드푸시 코드 작성하기

본격적인 셋팅이 끝났으니, 코드푸시를 사용할 수 있는 환경을 만들어 봅니다. 코드푸시를 진행할 수 있는 코드로는 크게 두 가지의 큰 그림으로 나누어볼 수 있습니다.
1. codePush.sync 를 이용해 처음부터 끝까지 자동화하는 방법.
2. 업데이트 자체를 커스텀하는 방법 (checkForUpdate -> remotePackage download -> localPackage install` -> restartApp )
상황에 따라 다르겠지만, 1번은 코드푸시 모듈에서 자체적으로 업데이트체크부터 업데이트 실행까지 손쉽게 해결할 수 있는 방법이 있지만, 자체적인 커스텀이 불가능하다는 단점이 있습니다. 예를들어 업데이트를 위해 뜨는 alert는 네이티브가 제공하는 alert를 사용하게 됩니다.
2번의 경우에는 1 번에 비해 비교적 직접 설정해주어야하고 추가적으로 에러핸들링을 해주어야 하는 부분이 많지만, 유연하게 상황에 따라 코드푸시를 제공할 수 있도록 기능 추가가 가능해진다는 장점이 있습니다.

해당 포스팅에서는 codePush.sync 를 이용해서 손쉽게 코드푸시를 자동화 하는 방법으로 진행해보겠습니다. (sync를 이용하지 않고직접 커스텀한다고 해도 2의 방법에 적힌 순서대로 진행하면 크게 다르진 않습니다.)

+참고로 docs.microsoft.com에 있는 코드푸시 API문서가 MS에서 관리하는 것 치곤 잘 업데이트되지 않습니다. 그렇기 때문에 API문서보다는 패키지 내부의 소스코드를 읽어보는 게 더욱 빠릅니다. 또는 Github를 참고하세요.

import React, {Component} from 'react';
import {SafeAreaView, StyleSheet, Text, TouchableOpacity} from 'react-native';
import codePush, {UpdateDialog} from 'react-native-code-push';
import {colors} from './utils/colors';const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.white,
justifyContent: 'center',
alignContent: 'center',
},
center: {
justifyContent: 'center',
alignItems: 'center',
}
});
// ON_APP_START, ON_APP_RESUME, MANUAL이 있으며 필요에 따라 설정해주면 된다.
const codePushOptions = {checkFrequency: codePush.CheckFrequency.MANUAL};
const updateDialog: UpdateDialog = {
title: '업데이트가 필요합니다.',
appendReleaseDescription: false,
descriptionPrefix: '',
// Force Update
mandatoryContinueButtonLabel: '업데이트 하기',
mandatoryUpdateMessage: '새로운 기능 제공을 위해 앱이 업데이트 됩니다.',
// Update
optionalIgnoreButtonLabel: '다음에 하기',
optionalInstallButtonLabel: '업데이트하기',
optionalUpdateMessage: '새로운 업데이트가 있습니다.',
};
class App extends Component {
onButtonPress() {
codePush.sync({
updateDialog,
// IMMEDIATE, ON_NEXT_RESTART, ON_NEXT_RESUME, ON_NEXT_SUSPEND이 있으며 필요에 따라 설정해주면 된다.
installMode: codePush.InstallMode.IMMEDIATE,
});
}
render() {
return (
<SafeAreaView style={styles.container}>
<TouchableOpacity onPress={this.onButtonPress} style={styles.center}>
<Text>코드푸시 업데이트 하기</Text>
</TouchableOpacity>
</SafeAreaView>
);
}
}
export default codePush(codePushOptions)(App);

바벨 또는 타입스크립트를 사용한다면 데코레이터 문법으로 더욱 간결하게 작성할 수 있습니다. 함수형으로는 HOC로 더욱 간단해지겠네요.

const App = () => {
const onButtonPress = () => {
codePush.sync({
updateDialog,
// IMMEDIATE, ON_NEXT_RESTART, ON_NEXT_RESUME, ON_NEXT_SUSPEND이 있으며 필요에 따라 설정해주면 된다.
installMode: codePush.InstallMode.IMMEDIATE,
});
};
return (
<SafeAreaView style={styles.container}>
<TouchableOpacity onPress={onButtonPress} style={styles.center}>
<Text>코드푸시 업데이트 하기</Text>
</TouchableOpacity>
</SafeAreaView>
);
};
export default codePush(codePushOptions)(App);

역시 Hooks가 간결하네요.

*참고로 codePush.sync() 는 Promise를 반환하기 때문에, 필요에 따라 비동기 핸들링을 해주는것이 좋습니다.

버튼을 눌러서 코드푸시를 받아올 수 있습니다.

지금은 셋팅을 위해 코드푸시 업데이트를 선택적으로 받아올 수 있도록 했지만, 사실 이런식으로 코드를 작성해서 실제로 이용하는 것은 별로 좋지 않은 방법이라고 생각합니다. 사용자가 조금 더 자연스럽게 최신 코드푸시 버전을 받아올 수 있도록 스플래시 스크린에서 progress bar를 보여주면서 코드푸시를 모두 받은 후 앱이 실행되도록 설정하는 방식이 더욱 자연스러울 것 같네요. 또한 코드푸시에서는 이를 위한 API들도 모두 제공되고 있습니다.

코드푸시 만능일까?

아니 마켓업데이트를 하지 않아도 된다니!? 너무좋잖아?! 라고 생각하실 수도 있지만, 사실 단점도 존재합니다. 네이티브 코드가 변경되는 경우엔 무조건 마켓업데이트를 거쳐야 하고, 네이티브쪽 코드와 연관되어있는 JS코드가 수정되면, 이에 따라 코드푸시 바이너리를 나누어 관리해주어야하는 상황이 생깁니다. 특히나 Force Update를 하지 않는 경우라면, 버전관리가 굉장히 까다로워집니다. 안드로이드 최신버전/미만버전, iOS 최신버전/미만버전 모두 코드푸시를 해주어야 하는 불상사가 일어나게 되죠.

지금까지 RN Codepush 설정에 관해 간단히 설명해 보았습니다. 리액트 네이티브를 이용해 서비스를 제공한다면, 적용해야 할 기술이자, 리액트 네이티브의 사용하는 가장 큰 이유가 아닐까 싶습니다. 물론 모바일 환경에서는 웹뷰(Web View)라는 대항마가 있지만, 네이티브 코드를 건드릴 수 없다면, 분명 리액트 네이티브도 좋은 선택이라고 생각합니다.(브릿지가 필요해진다면 네이티브코드를 알아야 하지만요.)

설 연휴의 마지막 저녁에 적는 포스트네요.
다들 새해 복 많이 받으세요 🙇🏻‍♂️

전체 소스코드는 여기서 확인하실 수 있습니다.

--

--