create-react-native-app (crna) 사용하기

들어가며

회사에서 약 6개월간 React Native를 이용한 게임 앱(eyePoker - 화상채팅과 함께하는 포커게임) 개발을 마무리하였습니다. 후에 React Native를 사용해 새로운 앱을 개발할때 사용할 수 있도록 그간 React Native로 개발하면서 익힌 것들을 boilerplate로 만들려고 했습니다.

약 6개월전 처음 React Native를 접했을때 당시 v0.39 였으나 최근 확인해보니 stable 버전이 v0.46 까지 나와있었습니다. v0.46을 이용해 React Native 프로젝트를 생성하고자 하니 **Create React Native App**이 추가되어 있었습니다. (앞으로 Create React Native App을 **CRNA**라고 부르겠습니다.)

CRNA는 기존의 React Native CLI를 이용하지 않고 프로젝트를 생성하는 방법입니다. 지금부터 CRNA에 대해 알아보고 기존의 React Native CLI를 사용해서 프로젝트를 생성할때와 어떤 다른점이 있는지 알아보도록 하겠습니다.

create-react-native-app (crna)란?

먼저 아래의 영상을 통해 CRNA를 통해 프로젝트를 생성하고 실행하는 과정을 볼 수 있습니다. 약 6분정도의 짧은 영상입니다. 간단하게 한번 보시면 뒤의 내용을 좀 더 쉽게 이해할 수 있습니다.

React Native Document에서도 확인할 수 있듯이, CRNA는 React Native 프로젝트를 시작하기 훨씬 쉽게 해주는 도구입니다. React를 접해보신분은 한번쯤 들어보셨을 React Create App에서 영감을 받은 것이고, Expo라는 회사와 협업을 통해 만들어진 결과물 입니다.

Android나 iOS를 개발해본 경험 없이 React Native를 통해 앱 개발을 시작함에 있어서 네이티브 빌드에 필요한 도구를 설치하고 환경을 구성하는 것은 쉽지 않은 작업 입니다. 그러나 CRNA를 사용하면 Xcode 또는 Android Studio가 필요없으며 Linux 또는 Windows를 사용하여 iOS 장치 용으로도 개발할 수 있습니다. (이제 Mac이 필수가 아닙니다…)

CRNA는 네이티브 코드를 컴파일하지 않고 순수 JavaScript로 작성된 CRNA 프로젝트를 로드하고 Expo 앱을 사용하여 실행합니다. 그렇기 때문에 CRNA를 사용해 프로젝트를 생성할 경우 기존 CLI를 사용해서 생성한 결과물과는 다르게 프로젝트 폴더에 android와 ios 폴더가 없습니다.

그러나 아직 대부분의 React Native 라이브러리들을 사용하기 위해서는 android와 ios 폴더 아래 있는 네이티브 프로젝트 파일(Java, Objective-C, Swift)을 직접 수정하고 컴파일 해야하는 하는데 이러한 경우는 어떻게 할까요?

먼저, Expo는 이러한 경우를 대비해 카메라, 비디오, 연락처등 자주 사용되는 인기있는 라이브러리들을 번들로 제공하고 있습니다. 그렇기 때문에 대부분의 간단한 앱들은 따로 외부 라이브러리를 추가하지 않아도 만들 수 있습니다. 그러나 번들로 제공되지 않는 기능이 필요해 라이브러리를 추가해야하는 경우, 네이티브 코드 추가 및 빌드가 필요하기 때문에 CRNA를 사용해 생성한 프로젝트를 그대로 사용할 수가 없습니다. 이러한 경우를 대비해 Create React App에서 제공하는것과 마찬가지로 CRNA에서도 ejecting 기능을 제공하고 있습니다.

npm run eject 명령어를 통해 CLI를 통해 프로젝트를 생성한 것과 유사한 프로젝트를 얻을 수 있습니다. android와 ios 프로젝트 폴더가 생성됩니다. 그렇기 때문에 네이티브 빌드를 하기 위해서는 Xcode 또는 Android Studio가 필요합니다.

CRNA 프로젝트 생성

저는 nvm을 통해서 node v8.1.4, npm v4.6.1을 사용하고 있습니다. (npm은 v5.x대의 경우 에러가 발생하기 때문에 v4.x로 재설치를 하고 진행하셔야 합니다.)

이제 CRNA를 사용해서 프로젝트를 생성해보겠습니다.

1
2
3
4
$ npm i -g create-react-native-app
$ create-react-native-app crna-project
$ cd crna-project
$ npm install

**CRNA**를 사용해 프로젝트를 생성한 결과 구조는 다음과 같습니다.

1
2
3
4
5
6
7
8
crna-project
├── App.js
├── App.test.js
├── app.json
├── node_modules/
├── package.json
├── yarn.lock
└── README.md

참고) **CLI**를 생성해 프로젝트를 생성한 결과는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
cli-project
├── index.android.js
├── index.ios.js
├── android/
├── ios/
├── node_modules/
├── __tests__/
├── app.json
├── package-lock.json
├── package.json
└── yarn.lock

기존 CLI를 사용했을때와 다르게 index.android.js와 index.ios.js는 App.js로 변경되었으며, 네이티브 빌드에 사용되는 android, ios 폴더는 사라졌습니다. 또한 app.json 파일은 그대로 존재하지만 파일 내용에는 차이가 있습니다.

**CRNA**를 사용해 생성한 프로젝트의 app.json

1
2
3
4
5
{
"expo": {
"sdkVersion": "18.0.0"
}
}

**CLI**를 사용해 생성한 프로젝트의 app.json

1
2
3
4
{
"name": "cli-project",
"displayName": "cli-project"
}

CRNA를 사용해 생성한 프로젝트의 app.json은 expo의 sdkVersion을 표시하고 있습니다.

CRNA 프로젝트 실행 - 1 (With 모바일 Expo 앱)

이제 생성한 CRNA 프로젝트를 실행해 보겠습니다. 기존 CLI를 통해 프로젝트를 생성했을 때는 실행을 위해 Android SDK와 Xcode가 필요하였고 다음의 명령어로 네이티브 빌드를 하여 실행했습니다.

1
2
$ react-native run-android
$ react-native run-ios

그러나 CRNA를 사용해 생성한 프로젝트는 다음의 명령어로 실행합니다. 명령어를 실행하면 다음과 같은 QR코드가 출력됩니다.

1
$ npm start

crna 프로젝트를 실행한 결과

QR코드가 출력되어 당황할 수 있지만 이 QR코드를 통해서 방금 생성한 프로젝트를 실행할 수 있습니다. 처음에 말씀드렸다시피 CRNA는 네이티브 코드를 컴파일하지 않고 순수 JavaScript로 작성된 CRNA 프로젝트를 로드하고 Expo 앱을 사용하여 실행한다고 했습니다. 그렇기 때문에 저희가 실행 결과를 확인하기 위해서는 Expo 앱을 추가로 설치해야합니다.

Expo앱은 JavaScript를 작성하여 기본 iOS 및 Android 앱을 제작할 수있게 해주는 도구, 라이브러리 및 서비스 세트이며 Expo앱 다운로드를 통해서 다운로드 후 설치 합니다.

다운로드 후 Expo앱을 실행하고 QR코드를 입력하면 다음과 같이 앱이 실행됩니다.

Expo 앱을 통한 CRNA 프로젝트 실행 - 1
Expo 앱을 통한 CRNA 프로젝트 실행 - 2

CLI를 통해 생성한 프로젝트를 실행하여 가상머신을 통해 결과를 확인했을 때와 같이 동일한 결과를 얻을 수 있습니다.

CRNA 프로젝트 실행 - 2 (With 데스크탑 Expo Tool (Expo XDE))

조금 전에는 Expo 앱과 CRNA를 실행한 후 QR코드를 통해 자신의 모바일에서 결과를 확인했습니다. 이번에는 모바일 앱이 아닌 Expo XDE를 설치한 후 가상머신을 통해 실행해 보도록 하겠습니다. Expo XDE를 실행한 후 조금전 생성한 CRNA 프로젝트를 로드합니다.
(Expo XDE를 설치하면서 에러가 발생하시는 분은 Expo Installation을 참고해서 Expo XDE에 필요한 다른 도구들을 설치해야합니다.)

Expo XDE로 CRNA 프로젝트 로드

프로젝트가 로드되면 우측 상단의 Share 버튼을 통해 조금전 npm start에서 제공했던것과 동일하게 QR코드를 제공받아 모바일 Expo 앱에서 프로젝트를 실행할 수 있고, Device 버튼은 현재 데스크탑에 설치되어 있는 iOS, Android 가상머신을 통해 실행할 수 있습니다.

이외에도 Expo XDE의 기능은 여러가지가 있습니다. 이외의 기능들은 Expo Documentation & Guides에서 확인할 수 있습니다.

CRNA 프로젝트 ejecting

지금까지 CRNA를 통해 프로젝트를 생성하고 실행해 보았습니다. 마지막으로 ejecting 기능에 대해서 알아보겠습니다. CRNA의 ejecting 기능은 번들로 제공되지 않는 기능이 필요해 라이브러리를 추가해야하는 경우 네이티브 코드 추가 및 빌드가 필요하기 때문에 CRNA를 사용해 생성한 프로젝트를 그대로 사용할 수가 없으므로 사용하게됩니다.

1
$ npm run eject

CRNA - eject 옵션 선택

eject 기능을 실행하면 2가지 옵션중 하나를 선택하게 됩니다. 1) CLI로 생성한 프로젝트와 같은 모양을 같는 프로젝트로 변경할 것인지, 2) 인기있는 라이브러리이 포함된 ExpoKit을 그대로 사용하며 android, ios 와 같은 네이티브 폴더를 eject 할 것인지 선택하게 됩니다.

ExpoKit 옵션을 선택하여 ExpoKit은 그대로 사용할 수 있도록 유지합니다. Expokit 옵션을 선택하게 되면 iOS bundle identifier등 Android의 package name을 설정하게 되는데, 이때 주의해야할 점은 package name을 올바르지 않게 설정하게 되면 후에 네이티브 컴파일에서 오류가 생기게 됩니다. 이를 다시 수정하는건 번거로운 일이니 처음 설정시 신중하게 설정해야합니다. (‘-‘나 ‘_’와 같은 문자 대신 ‘.’을 이용해 패키지 명을 작성합니다.)

CRNA - eject 설정 사항

crna 프로젝트를 eject하고 난 후에는 다음과 같이 폴더 구조가 변경됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
crna-project (eject with ExpoKit)
├── App.js
├── App.test.js
├── app.json
├── android/
├── ios/
├── node_modules/
├── .expo
├── .expo-source/
├── package.json
├── yarn.lock
└── README.md

이제 네이티브 관련 코드가 생성되었기 때문에 react-native-cli 명령어들을 사용할 수 있습니다. 그러나 react-native run-ios 명령어를 통해 실행을 하게되면 에러가 발생합니다. 이는 eject후 expo관련 모듈을 네이티브(ios) 폴더 아래에서 다시 설치해줘야하기 때문입니다. ios 폴더 아래에서 다음의 명령어를 실행합니다. (CocoaPods이 없으신 분들은 CocoaPods 홈페이지를 참고하여 설치합니다.) 또한 node_module도 다시 설치해야 하기때문에 프로젝트 폴더 아래에서 npm install을 실행합니다.

1
2
$ npm install // 프로젝트 폴더 아래에서
$ pod install // 프로젝트폴더-ios 폴더 아래에서

eject 후 Expo - ios 모듈 설치

이제 react-native run-ios 명령어를 실행하면 가상머신을 통해 실행할 수 있습니다. 그러나 기존 CLI를 통해 생성한 프로젝트와 다르게 CRNA로 프로젝트를 생성한 후 eject 하여도 최초 CRNA로 프로젝트를 생성한 것이라면 Expo 혹은 exp(Expo Cli Tool)의 도구가 필요합니다. (React Native 프로젝트 실행시 Packager라는게 필요한데 CRNA로 프로젝트를 셍성했다면 Expo혹은 exp가 이 packager를 대신 실행하게 됩니다.)

그렇기 때문에 Expo를 실행하여 프로젝트를 로드하거나 새로운 터미널을 실행하여 exp start를 실행한 후 react-native run-ios를 실행하면 가상머신에서 프로젝트가 실행됩니다.

Expo 라이브러리 사용해보기

Expo SDK는 다양한 라이브러리를 제공하고 있습니다. 기본적으로 자주 쓰이고 필요한 라이브러리들은 다 있고, 관리가 잘 되고 있기 때문에 믿고 쓸 수 있습니다. 아직 그 수가 많지는 않지만 Feature-Request에서 사용자와의 소통을 통해 지속적으로 라이브러리를 추가해가고 있습니다.

지금부터는 Expo SDK에 Lottie 라이브러리를 사용해 보겠습니다. Lottie는 Airbnb에서 만들어 제공하는 라이브러리로 iOS, Android, React Native에서 After Effects로 만든 애니메이션을 실시간으로 앱에서 정적 이미지를 사용하는 것처럼 쉽게 애니메이션을 사용할 수 있도록 도와줍니다.

Expo Lottie Document를 참고해서 Lottie 라이브러리를 사용하면 다음과 같은 간단히 테스트가 가능합니다.

Expo lottie 테스트

기존의 CLI를 통해 프로젝트를 생성하고 Lottie를 사용해보신분들은 정말 과정이 간편해졌다는걸 느낄 수 있습니다. Expo SDK를 사용하지 않고 React Native 프로젝트에서 Lottie를 사용하기 위해서는 네이티브 코드에 Lottie관련 코드들을 추가하고 컴파일 과정에서 생기는 문제를 해결하기 위해서 많은 시간이 필요했습니다. (React Native Lottie - Github을 참고해보세요) 그러나 Expo SDK를 사용하면 정말 간단한게 사용할 수 있게 되었습니다.

마치며

새롭게 추가된 CRNA는 React Native에 있어서 큰 변화라고 생각합니다. 아직은 Expo SDK에서 제공하는 라이브러리가 많지 않기 때문에 ejecting이 불가피하지만 Expo의 지속적인 관리로 앞으로 더 많은 라이브러리들과 기능들이 추가되고 점점 사용하기 쉽고 편하게 바뀔것이라 생각됩니다.

앞으로 새롭게 시작하는 React Native 프로젝트는 CLI보다는 CRNA를 통해서 프로젝트를 시작한 후 ejecting 기능을 사용해 Expo SDK를 그대로 가져가며 프로젝트는 진행하는것이 좋을것 같습니다.

추가적으로 Expo에 대해서 더 궁금한게 있으신분은 Expo-Frequently Asked Questions를 참고하시면 될것 같습니다.

참고

Redux 사용하기

공부하고 반복해서 복습하고 마지막에는 스스로 정리해보는 시간을 갖는것이 스스로에게 많은 도움이 되는것 같습니다. 그래서 이번에는 **ReactNative에서 Redux를 사용하기**라는 주제로 포스팅을 하려합니다. Flux에서 Redux 그리고 ReactNative까지 정말 자세하고 친절하게 설명해주신 분들이 많습니다. 제가 공부하며 참고했던 좋은 글들과 강의는 React Native 글 모음에 따로 정리를 하였습니다. 이 글에서는 제가 그동안 Redux 공부하며 이해가 잘 되지 않았던 부분들에 중점을 맞춰서 정리해보고 React Native에 Redux를 적용시키는 것으로 마무리하려 합니다. 아직은 Redux에 대해 잘 안다고 말할 수 없지만, 이번 기회로 정리하며 다시 다잡으려 합니다. 혹시나 제가 잘못이하거나 틀린 부분이 있다면 댓글로 알려주시면 감사하겠습니다.

Flux와 Redux

Redux의 시작은 Flux

Redux는 페이스북에서 MVC 패턴의 단점을 보완하고자 만든 아키텍처인 Flux의 구현체중 하나입니다. Flux의 구현체는 Redux 외에도 Reflux, rx-flux 등… 여러 구현체가 있지만 그중 Redux가 가장 널리 사용되고 있습니다.

저는 Redux를 처음 접했을때 Flux와 많이 혼동했습니다. Redux와 Flux의 구성 요소와 역할에 대해 혼란스러웠고 정리가 잘 되지 않았는데 이는 Redux와 Flux의 다른점에 대해 집중하지 않았기 때문입니다. Redux는 Flux와 마찬가지로 애플리케이션의 상태를 예측 가능하게 합니다. 그러나 Redux는 Flux와 다른 특징이 있습니다. 바로 **핫 리로딩(hot reloading)**과 **시간 여행 디버깅(time travel debugging)**입니다. 이 두가지의 특징으로 인해 Redux는 Flux의 구성요소에는 없는 리듀서(reducer)가 생겨나고, 스토어(Store)의 역할이 조금 변하게 됩니다.

리듀서(Reducer)

Flux에서 스토어는 (1) 상태 변환을 위한 로직, (2) 현재 애플리케이션의 상태를 포함하고 있습니다. 스토어가 이 두 가지를 모두 갖고 있기 때문에 핫 리로딩시 문제가 발생합니다. 상태 변환을 위한 로직을 수정하기 위해 스토어 객체를 리로딩하면 스토어에 저장된 기존의 상태와 뷰를 비롯한 나머지 시스템과의 이벤트 구독이 사라지게 되어 문제가되기 때문입니다. 이를 해결하기 위해 Redux에서는 리듀서(reducer)가 새로운 구성요소로 추가됩니다.

리듀서는 스토어가 갖고 있던 상태 변환을 위한 로직을 대신 갖게됩니다. 따라서 스토어는 액션이 발생했을 때 어떤 상태 변화를 만들어야 하는지 알기위해 리듀서에게 요청합니다. 이렇게 기존 스토어에서 상태 변환을 위한 로직이 분리되었기 때문에 이제 핫 리로딩이 가능하게 됩니다.

리듀서는 첫 번째 인수로 기존 상태의 값 두 번째 인수로는 액션을 가집니다. 리듀서를 작성할 때는 주의사항이 있는데, 첫 번째 인수로서 기존 상태를 갖고 있는 state는 수정하지 않고 상태를 수정할 때는 새롭게 생성하여야 합니다. 이 주의사항으로 지킴으로써 각각의 액션이 발생할 때마다 새로운 상태의 객체가 생성되어 결과적으로는 Redux의 특징인 시간 여행 디버깅이 가능하게 됩니다.

스토어(Store)

Redux의 스토어는 Flux의 스토어와는 다소 차이가 있습니다. 먼저 Flux에서는 다수의 스토어를 가질 수 있었고, 각 스토어는 자신의 범위에 있는 애플리케이션의 상태를 변환할 수 있는 로직을 포함하고 있었습니다. 그러나 Redux는 하나의 스토어만을 가집니다. 또한 Redux의 스토어는 상태 트리(state tree) 전체를 유지하는 책임을 가지며, Flux의 디스패쳐(dispatcher)의 역할도 대신합니다.(Flux의 디스패쳐는 모든 스토어를 갖고 있고, 액션 생성자로부터 액션을 넘겨받으면 스토어에 전달합니다.) 따라서 Redux에서는 스토어에서 제공하는 dispatch 함수로 디스패쳐의 동작을 대신합니다.

Redux 구성요소

지금까지 Flux와 Redux의 차이점과 그로 인해 Redux가 가질 수 있게된 특징에 대해 알아보았습니다. 지금부터는 Redux의 구성요소를 살펴보겠습니다.

액션(Action)

액션은 애플리케이션의 상태를 갖고 있는 스토어로 전달하는 데이터 묶음입니다. store.dispatch() 를 통해 스토어에 액션을 전달할 수 있습니다.

액션은 평범한 자바스크립트 객체이며, type 속성을 갖고 있습니다. 이 type 속성은 액션을 전달받은 스토어가 애플리케이션의 상태변환 로직을 갖고 있는 리듀서를 참조할때 사용하게 됩니다.

액션 생성자(Action Creator)

액션 생성자는 액션을 만드는 함수입니다. Flux에서는 함수 내부에서 dispatch 함수를 통해 액션을 전달하지만 Redux의 액션 생성자는 단지 액션을 반환하기만 합니다. 실제 액션을 전달할때는 결과값을 dispatch 함수에 전달하거나 생성된 액션을 자동으로 보내주는 바인드된 액션 생성자를 만듭니다.

리듀서(Reducer)

액션은 무언가 일어나 상태가 변할것이라는 사실을 말할뿐, 그 결과 실제 애플리케이션의 상태가 어떻게 바뀌는지는 리듀서가 담당합니다. 리듀서는 이전 상태와 액션을 받아서 다음 상태를 반환하는 역할을 하는 순수 함수입니다. 따라서 항상 다음 상태를 계산해서 반환하는 역할만 합니다. API 호출이라던가, Date.now()나 Math.random() 과 같은 순수하지 않은 함수를 호출하는 일은 해서는 안됩니다.

Redux는 처음에 리듀서를 undefined 상태로 호출하여 초기 상태를 반환합니다. 리듀서는 서로 독립적으로 수행된다면 분리될 수 있고, 이 분리된 리듀서는 *루트 리듀서라는 하나의 객체로 조합될 수 있습니다. 결과적으로 처음에 undefined 상태로 호출되면 각각의 자식 리듀서들이 초기 상태를 반환하게 되고, 각각의 리듀서는 전체 상태에서 자신의 부분만을 관리합니다. 모든 리듀서의 state 매개변수는 서로 다르고, 자신이 관리하는 상태 부분에 해당합니다.

스토어(Store)

스토어는 “무엇이 일어날지”를 표현하는 액션과 이 액션에 따라 애플리케이션의 상태를 어떻게 수정할지를 나타는 리듀서를 함께 가져오는 객체입니다. 스토어는 다음과 같은 일들을 합니다.

  • 애플리케이션의 상태를 저장
  • getState()를 통해 상태에 접근
  • dispatch(action)을 통해 상태를 수정할 수 있게 함
  • subscribe(listener)를 통해 리스너를 등록

Redux의 스토어는 Flux와는 달리 하나의 스토어만을 가질 수 있기 때문에 데이터를 다루는 로직을 나누고 싶다면 여러개의 리듀서를 조합하여 대신할 수 있습니다.

뷰 레이어 바인딩(The view layer binding)

뷰 레이어 바인딩은 생성된 스토어를 뷰에 연결하기 위해 필요합니다. 뷰 레이어 바인딩은 connect() 을 통해 컴포넌트(뷰)가 애플리케이션의 상태 업데이트를 받을 수 있도록 모든 연결을 만들어줍니다.

루트 컴포넌트(Root component)

모든 React 애플리케이션은 루트 컴포넌트를 가집니다. 루트 컴포넌트는 계층 구조에서 가장 위에 위치하는 컴포넌트이며, 스토어를 생성하고 어떤 리듀서를 사용할지 알려주며 뷰 레이어 바인딩과 뷰를 불러옵니다.

Redux 사용 준비

애플리케이션을 생성하며 Redux의 구성요소들이 서로 연결됩니다.

  1. **combineReducers()**를 통해 다수의 리듀서를 하나로 묶은 후 루트 컴포넌트가 **createStore()**를 이용해 스토어를 생성할때 전달합니다.
  2. 루트 컴포넌트는 공급 컴포넌트와 스토어 사이를 연결함으로써 스토어와 컴포넌트 사이의 커뮤니케이션을 준비합니다. (이후 컴포넌트에서 connect()를 통해 상태 업데이트를 받을 수 있습니다.)

Redux 데이터 흐름

Redux의 아키텍쳐는 엄격한 일방향 데이터 흐름에 따라 전개되며 데이터의 흐름은 4단계에 따라 진행됩니다.

1. 액션 생성 후 스토어에 전달
액션은 무언가 일어나 상태가 변할 것이라는 내용을 담고 있는 객체입니다. 액션 생성자를 통해 액션을 생성한 후 store.dispatch(action)을 통해 스토어에 전달합니다.

2. 스토어가 리듀서를 호출
스토어는 리듀서에 현재의 상태 트리와 전달받은 액션을 두 가지 인수로 전달합니다.

3. 루트 리듀서가 각 리듀서의 출력을 합쳐 하나의 상태 트리 생성
각각의 상태를 다루는 리듀서에 의해 생성된 결과를 하나로 합쳐 루트 리듀서가 하나의 상태 트리를 생성합니다.

4. Redux 스토어가 루트 리듀서에 의해 반환된 상태 트리를 저장
새로운 상태 트리가 앱의 다음 상태입니다. store.subscribe(listener)를 통해 등록된 모든 리스너가 불러내지고 이들은 현재 상태를 얻기 위해 store.getState()를 호출합니다. connect()를 통해 컴포넌트에 스토어가 연결되어 있다면 컴포넌트는 이를 반영하고 자신의 setState()나 forceUpdate() 메소드를 실행해 자동적으로 render() 메소드를 호출합니다.

React Native에서 Redux 사용하기

이제 React Native에서 Redux를 사용해보겠습니다. React Native를 위한 개발환경 설정은 공식홈페이지를 참조하시기 바랍니다. 이 예제 프로젝트에서는 간단하게 Redux를 사용하는 법에 초점을 맞춰 진행해보겠습니다.
간단히 카운팅하는 앱을 만들어보려 합니다. 완성 화면은 다음과 같습니다.
result

프로젝트 생성

먼저 React Native 프로젝트를 생성합니다. (현재 버전은 0.41.2 입니다)

1
react-native init example

다음으로 redux를 사용하기 위해 필요한 모듈들을 설치합니다.

1
npm install redux react-redux --save

이제 Redux의 구성요소를 참고하여 다음과 같이 새로운 폴더 구조를 생성합니다. (앞으로 사용할 폴더와 파일만 명시하였습니다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
example
├── __tests__/
├── android/
├── ios/
├── node_modules/
└── src/
├── actions/
├── index.js
├── countAction.js
└── types.js
├── components/
└── Count.js
├── reducers/
├── index.js
└── countReducer.js
└── app.js
├── index.android.js
├── index.ios.js
└── package.json

루트 컴포넌트 생성 & 설정

먼저 app.js 파일을 작성합니다. App 컴포넌트는 루트 컴포넌트로 사용될 것이며, 액션과 리듀서 파일을 작성후 스토어를 추가할것입니다.

다음은 Android와 iOS의 진입파일인 index.android.jsindex.ios.js을 수정하여 App 컴포넌트를 루트 컴포넌트로 등록합니다.

지금까지의 코드를 작성한 후 화면은 다음과 같습니다.
result1

Count 컴포넌트 생성

이제 Count.js 파일을 작성하여 Count 컴포넌트를 생성하겠습니다. Count 컴포넌트는 현재 카운트 되고 있는 숫자를 보여주는 텍스트와 카운트를 증가시키는 버튼으로 구성됩니다.

Count 컴포넌트를 app.js에서 사용합니다.

Count 컴포넌트를 추가한 후 화면입니다.
result2

Count 액션 생성

액션은 애플리케이션의 상태를 갖고 있는 스토어로 전달하는 데이터 묶음이라고 했습니다. 이 카운팅 앱에서는 액션 객체의 type을 통해서 증가인지 감소인지, payload를 통해서는 증가 혹은 감소할 값을 전달할 것입니다.

먼저 actions/types.js를 작성합니다. types.js 에는 액션의 타입으로 사용될 값을 상수로 정의합니다. 타입은 후에 리듀서에서도 사용되기 때문에 미리 상수로 정의하는것이 실수를 줄일수도 있고, 후에 액션을 관리하는데에도 유용합니다.

이제 countAction.js를 작성합니다. type과 payload로 이루어진 액션 객체 액션 생성자를 통해 생성되어 스토어로 전달됩니다.

마지막으로 index.js를 작성합니다. index.js 에서는 여러개의 액션을 하나의 객체로 묶어 컴포넌트 파일에서 쉽게 사용할 수 있도록 해주는 역할을 합니다.

Count 리듀서 생성

액션을 전달받은 스토어가 상태를 변경하기 위해 리듀서에게 어떠한 상태변환을 해야하는지 요청합니다. 리듀서에서는 이 요청을 처리할 수 있도록 코드를 작성해야 합니다.
countReducer.js를 작성합니다. 리듀서는 함수입니다. 첫 번째 인자로 이전의 상태를 전달받고, 두 번째 인자로는 액션을 전달받습니다. 전달 받은 액션의 type을 통해 새로운 상태를 반환하는것이 리듀서의 역할입니다.
애플리케이션 실행 후 Redux는 처음에 리듀서를 undefined 상태로 호출합니다. swtich문에서 default인 상태에서 초기 상태를 설정합니다.

액션과 마찬가지로 index.js를 작성합니다. 여러개의 리듀서를 묶어 컴포넌트 파일에서 쉽게 사용할 수 있도록 해주는 역할입니다.
redux 모듈의 combineReducer는 트리 구조로 분리된 여러개의 상태를 하나의 단일 상태 트리로 조합합니다.

루트 컴포넌트에 스토어 연결

생성한 액션과 리듀서를 애플리케이션에서 사용할 수 있도록 루트 컴포넌트에 설정해주어야 합니다. 처음 작성했던 app.js를 다음과 같이 수정합니다.
redux 모듈에 있는 createStore를 통해 스토어를 생성할 수 있습니다. store 생성시 인자로 리듀서를 필요로 합니다.
생성된 스토어를 React 에서 사용하기 위해 react-redux 모듈에서 Provider를 사용합니다.

Count 컴포넌트 바인딩

뷰 레이어 바인딩은 생성된 스토어를 뷰에 연결하기 위해 필요하다고 설명했습니다. 이제 생성한 액션과 리듀서를 Count 컴포넌트에서 사용할 수 있도록 connect를 통해 연결을 만들어줍니다.
connect 메서드는 Store의 state를 컴포넌트의 props로 전달하고 상태의 변화가 있을 때 자동으로 컴포넌트의 render를 재호출합니다. connect 메서드는 다음의 인자를 가집니다.

  • mapStateToProps : 스토어의 state를 해당 컴포넌트의 props로 전달(mapping)합니다.
  • mapDispatchToProps : 스토어의 dispatch를 props에 전달합니다. dispatch를 통해 액션생성자에서 생성한 액션을 스토어로 전달할 수 있습니다.

간단한 카운팅앱이 완성되었습니다. 버튼을 통해 count의 값을 변경할 수 있습니다.

포스팅을 마치며

MVC 패턴에 익숙해져있던 저에게 Redux는 머리로는 이해할 수 있었지만 가슴으로는? 이해기 쉽지 않았었습니다. 이번 기회에 다시 하번 정리를 하며 제가 처음에 혼란스러워했던 부분들을 최대한 설명해드리려 노력했습니다. 위의 예제가 좋은 예제라고는 말할 수 없지만 그래도 React Native에서 Redux가 어떻게 사용되는지 전체적인 구조를 보는데에는 나쁘지 않다고 생각합니다.
액션과 리듀서를 생성하고 사용하는 부분에 있어서는 여러가지의 방법이 있지만 최대한 Redux를 쉽게 이해할 수 있도록 작성해보았습니다. 좀 더 효율적으로 작성하는 법은 다음에 기회가 된다면 포스팅해보겠습니다.

시작하며 읽기 좋은 글 모음

React Native를 공부하고 사용하면서 도움이 되었던 글들을 모아놓았습니다. 각 카테고리별로 분류하였으며 처음 공부하시는 분들은 카테고리에서 위에서 아래로 순서대로 읽으시면 많은 도움이 될거라 생각합니다. 앞으로도 지속적으로 업데이트 할 예정입니다.


최근 업데이트 - 2017 / 05 / 30

React


Flux

  • Flux로의 카툰 안내서 - bestalign’s dev blog
    Redux는 Flux 아키텍처의 구현체중 하나 입니다. Redux를 공부하기전 Flux에 대해 알아볼 수 있습니다.

Redux

정적 이미지 사용하기

React Native는 웹뷰를 통해 인터페이스를 구축하는 하이브리드 방식(예를 들면 Ionic)과는 달리 자바스크립트로 작성한 코드가 Native UI 컴포넌트로 만들어집니다. React Native에서 이미지를 사용하는 코드는 iOS와 Android 모두 통합되어 있지만, 정적인 이미지 파일을 추가하는 부분은 Naming에 있어 차이가 있습니다. 이번에는 React Native에서 정적 이미지를 사용하는 방법과, 정적 이미지 추가 시 반복되는 작업을 좀 더 효율적으로 할 수 있도록 Node를 사용하여 스크립트를 작성하는 부분을 다뤄보도록 하겠습니다. 글 작성 기준 React Native의 최신버전은 0.4입니다.

React Native 정적 이미지 사용하기

React Native에서 정적 이미지를 추가하는 방법은 공식홈페이지에서도 잘 설명되어 있지만 한번 더 살펴보겠습니다.

다음과 같이 react-native 의 기본적인 Image 컴포넌트를 통해 정적인 이미지 파일을 사용할 수 있습니다. 아래의 코드는 작성된 컴포넌트의 파일과 같은 경로에 있는 gold.png라는 파일을 사용합니다.

1
<Image source={require('./gold.png')} />

하나의 파일을 두개의 플랫폼(iOS와 Android)에서 같이 사용할 수 있지만 만약 플랫폼에 따라 다른 이미지를 사용하고 싶다면 gold.png라는 파일을 gold.android.pnggold.ios.png로 각각 저장하여 사용할 수 있습니다. 이와 같은 경우는 주로 Android와 iOS에서 같은 목적으로 사용되는 이미지가 다를 경우 유용합니다. (React Native에서 iOS와 Android의 Entry file이 index.ios.js와 index.android.js 인것과 같은 개념입니다.)

또한 React Native의 packager플랫폼 뿐만 아니라 디바이스 스크린의 해상도에 따라서도 다른 이미지를 제공할 수 있습니다. 따라서 다음과 같이 이미지 파일의 이름을 해상도에 따라 달리하여 사용할 수 있습니다.

1
2
3
4
├─ Test.js
└── img/
├── gold@2x.png
└── gold@3x.png
1
2
{/* Test.js 코드의 render 부분 */}
<Image source={require('./img/gold.png')} />

파일 구조와 Test.js의 코드가 위와 같을 때 iPhone 6와 iPhone 6 Plus는 각각 gold@2x.pnggold@3x.png 파일을 사용합니다. 만약 디바이스 스크린의 해상도에 해당하는 파일이 없다면 가장 밀접한 해상도를 가진 파일을 사용합니다.

정리하면 디바이스의 플랫폼과 해상도에 따라 파일을 사용하고 싶을 경우 다음과 같이 사용할 수 있습니다.

1
2
3
4
5
6
├─ Test.js
└── img/
├── gold@2x.android.png
├── gold@2x.ios.png
├── gold@3x.android.png
└── gold@3x.ios.png

React Native에서 정적인 이미지를 사용할 경우 주의해야할 점은 공식홈페이지를 참고하시면 좋을것 같습니다.

Native 측면에서 바라보기

React Native는 기존 Android와 iOS에서 이미지를 추가하여 사용하는 방법을 그대로 사용할 수도 있습니다. 이미 위의 방법으로 해결이 가능하며 이 방법이 좀 더 복잡하기에 사용을 권장드리지 않지만 이런방법으로도 가능하다는걸 알고계시면 좋을것 같습니다. Native에서 Android와 iOS의 이미지 파일은 다음과 같은 경로에 저장하여 사용합니다.

  • Android Test-app/android/app/src/main/res/
  • iOS Test-app/ios/Test-app/images.xcassets/
### Android Android에서 해상도별로 이미지를 추가하는 방법을 알아보겠습니다.

1.처음 프로젝트 생성시에는 drawable관련 폴더가 없으므로 android/app/src/main/res/ 에 해당 폴더들을 생성합니다.

  • drawable-mdpi
  • drawable-hdpi
  • drawable-xhdpi
  • drawable-xxhdpi
  • drawable-xxxhdpi

2.해상도 별로 각 drawable 폴더에 파일을 저장합니다. (각 폴더별 저장되는 이미지는 파일명이 모두 같아야합니다.) 파일명이 gold.png 라면 각 폴더에는 다음과 같이 존재하게 됩니다.

  • drawable-mdpi/gold.png
  • drawable-hdpi/gold.png
  • drawable-xhdpi/gold.png
  • drawable-xxhdpi/gold.png
  • drawable-xxxhdpi/gold.png

iOS

iOS에서 이미지를 추가하는 방법은 Android에 비해 좀 더 번거롭습니다. 글로 설명하는것보다 사진을 참고하는 것이 이해하기 쉽기 때문에 사진을 첨부합니다.

1.먼저 xcode에서 프로젝트의 root폴더에서 ios폴더를 로드한 후 images.xcassets 폴더를 클릭 후 AppIcon 아래의 빈공간을 우클릭하면 New Image set라는 메뉴가 있습니다. 클릭 후 1x, 2x, 3x에 이미지를 넣으신 후 이미지의 이름을 설정합니다.
xcode에서 이미지 추가1

2.위와 같이 이미지를 넣고나면 해당 경로에 다음과 같은 형식으로 파일이 생성됩니다. Contents.json은 이미지 파일에 대한 정보를 갖고 있습니다.
xcode에서 이미지 추가2

이미지 사용하기

Android와 iOS에 이미지 추가가 모두 끝났습니다. 추가한 이미지를 React Native 에서 사용해보겠습니다.

1
2
{/* Test.js 코드의 render 부분 */}
<Image source={require('image!gold')} />

위의 코드에 **image!**라는 부분의 추가와 이미지 파일의 확장자가 사라졌다는게 전과 다릅니다. 이미지의 위치를 알리는 경로 또한 명시할 필요가 사라졌습니다.

코드로 한번 더 복습하기

이제 React Native에서 정적인 이미지를 추가하는 방법을 모두 알아보았습니다. 마지막으로 코드상에서 정적인 이미지를 사용하는 방법을 정리해보겠습니다.

– Native 에서 이미지를 추가하는 방법을 사용하였을 경우

1
2
{/* Test.js 코드의 render 부분 */}
<Image source={require('image!gold')} />

– 프로젝트 폴더내에 추가 후 상대경로를 사용하는 경우

1
2
{/* Test.js 코드의 render 부분 */}
<Image source={require('./image/gold.png')} />

– 첫번째와 두번째 방법을 모두 사용하는 경우 (Native에서 사용되는 폴더에서 이미지를 찾은 후 존재하지 않을 경우 상대경로를 사용하여 이미지를 사용)

1
2
{/* Test.js 코드의 render 부분 */}
<Image source={ {uri: 'gold', isStatic: true} } />

눈으로 확인하는 결과

iPhone6와 iPhone6 Plus 에서 Gold 이미지가 기기의 해상도에 맞게 다른 파일로 사용된것을 확인할 수 있습니다.

![iPhone6 에서의 Gold 이미지](/images/post/2017-01-22/iPhone6.png)
![iPhone6Plus 에서의 Gold 이미지](/images/post/2017-01-22/iPhone6Plus.png)

다음 포스팅에서는 정적인 이미지 파일을 쉽게 추가하여 사용할 수 있도록 자동화하는 스크립트 작성에 대해서 작성하겠습니다.