목록

react-navigation과 redux 통합하기

tl;dr

단도직입적으로…

대부분의 경우에 새로운 기술을 접하거나 소개할 때는 React Native 시작하기 같은 제목의 글을 쓰는 것이 보통일 것이다(선빵). 그러나 React Native는 시작하는 것이 사실 좀 번거롭기도 하다. 게다가 시작하기 류의 글은 왠지 이어서 쓰기가 부담스러울 것 같다. 그래서 대신에 요즘 백수생활의 시간낭비 삼아 사소한 앱을 만들면서 겪은 내가 삽질한 결과를 조금 옮겨보기로 했다.

Expo

expo 종합선물셋트

Expo는 무엇인가? Expo는 꿈돌이가 춤추던 대전 엑스포가 아니다. 그게 93년이니까 어쩌면 이 글을 보는 분 중에 그 해에 태어난 분들도 있겠다(흠좀무). 처음 React-Native를 react-native init으로 시작하는 것은 다소 자해행위 같은 느낌이 있다. 특히 안드로이드 환경설정하는 것은 매우 귀찮은데, 무언가 해야할 것이 많아서 내가 이러려고 React Native 시작했나 하는 자괴감이 들게 된다. Expo는 이런 귀찮은 작업을 무마해주는 도구로, 애플리케이션 코드만 짜면 Expo 클라이언트 앱을 통해서 바로 코딩 결과를 확인할 수 있다.

Expo XDE(https://github.com/expo/xde/releases)를 설치하면 GUI로 프로젝트 생성과 빌드를 클릭 몇번으로 해결할 수 있다. 코딩 결과는 아이폰과 안드로이드용 Expo 클라이언트를 통해 확인할 수 있다. CLI 선호자를 위해 npm 패키지로 CLI도 제공한다. SDK가 제공하는 다양한 기능을 활용하면 네이티브 모듈과 연결하는 수고도 줄여준다. 이 정도면 종합선물셋트 수준이다. 꿈돌이 종합선물셋트!

사실 지금 작업 중인 나의 삽질 가득한 개인 프로젝트는 react-native init으로 시작했다가, Expo로 해봤다가, 다시 react-native init으로 돌아가긴 했다. 둘을 비교해보면 Expo로 만들면 빌드한 앱 용량이 좀 커지는 편이고, 네이티브 모듈을 연결하려면 결국 eject를 해야 하는 등의 아쉬움이 있다.

그렇지만 처음 시작할 때 복잡한 설정을 거치지 않아도 되서 이 글을 쓰는 목적에는 잘 부합하기 때문에 여기서는 Expo를 사용한다.

Expo XDE

XDE 최신버전을 내려받고 실행한다. 가입은 GitHub 계정으로 할 수 있다. XDE에서 Project - New Project를 누르고 디렉토리를 지정하면, 프로젝트 스캐폴딩을 부어주고, 의존 모듈도 설치하고, 북치고 장구치고 서장이랑 받도 먹고 Expo 클라이언트에서 접속할 수 있는 URL도 알려준다. XDE를 설치하는게 내키지 않는다면 create-react-native-app을 써보는 것도 좋다. 그렇지만 이 경우에도 결과물을 확인할 때는 Expo 클라이언트를 사용하게 된다. 이제 문제의 디렉토리를 에디터에서 열고 본론으로 넘어간다.

초기 설정을 마치고 커밋한 결과는 여기에서 확인할 수 있다.

내비게이션의 세계

react-navigation은 React Native 프로젝트에 흔히 사용하는 내비게이션 모듈 중 한가지이다. 이것 외에도 인기있는 패키지가 몇가지 더 있는데 그것이 알고싶다면 여러가지 패키지를 비교한 아래의 글들을 참고하면 좋을 것 같다.

여기에 한가지 더 추가하자면 판단기준으로 쓰기는 적절하지 않지만, Facebook에 개설된 React Native Community 그룹의 최근 인기도를 보면 react-navigation 보다 react-native-navigation의 인기가 좀 더 올라간 것 같기는 하다.

9월 10월

react-navigation 적용하기

서론이 엄청 길었다. 일단 react-navigation을 설치한다.

yarn add react-navigation

일단 아무 것도 없는 황무지에 내비게이션을 붙여보자. react-navigation은 내비게이션 구조에 따라 StackNavigator, TabNavigator, DrawerNavigator를 제공한다. 예를 들어 첫화면, 소개, 도움말 같이 3개의 화면을 구현해야 한다면, 내비게이터와 각각의 화면을 정의해야 한다. 디렉토리 구조는 대략 이렇게 잡았다.

/navigator
  AppNavigator.js
  index.js
/screens
  Home.js
  About.js
  Help.js
  index.js
App.js

우선 내비게이터를 정의해본다. 내가 작업하는 앱에서는 DrawerNavigator를 쓰고 있었는데, 이 글에서는 TabNavigator를 써봐야겠다. 예제 코드를 참고해서 작성해보았다. TabNavigator API는 이렇다.

TabNavigator(RouteConfigs, TabNavigatorConfig)

RouteConfigs에 각 라우팅별로 navigationOptions를 줄 수 있는데, 여기에 레이블과 아이콘을 설정한다.

// navigation/AppNavigator.js
Home: {
  screen: Home,
  navigationOptions: {
    tabBarLabel: 'Home',
    tabBarIcon: ({ tintColor, focused }) => (
      <Ionicons
        name={focused ? 'ios-home' : 'ios-home-outline'}
        size={26}
        color={tintColor}
      />
    ),
  },
},

위에서 Ionicicons의 경우에는 Expo에서 제공해주는 @expo/vector-icons를 사용한다. 전체 아이콘 목록(https://expo.github.io/vector-icons/)도 참고하면 좋겠다.

TabNavigatorConfig에는 다음과 같이 설정을 추가했다. 각 옵션에 대해서는 문서에서 살펴볼 수 있다.

{
  tabBarPosition: 'bottom', // 탭을 하단으로 위치시킨다. ios는 하단, 안드로이드는 상단이 기본값이다.
  animationEnabled: true,
  tabBarOptions: {
    inactiveTintColor: '#333',
    activeTintColor: '#400011',
    style: {
      backgroundColor: '#fff',
    },
    labelStyle: {
      fontSize: 10,
    },
    indicatorStyle: {
      backgroundColor: '#400011',
    },
    showIcon: true, // 안드로이드는 기본값이 false이므로, 통일을 위해 true로 설정한다.
    showLabel: true,
  },
}

이럴 수가

어서오세요, 안녕히 가세요(?)

이 부분까지 마치고 커밋한 결과는 여기에서 확인할 수 있다.

redux에 상태 떠넘기기

redux와 연결하는 방법은 문서에 잘 소개되어 있기는 하다. 저장소에서 예제 코드도 찾아볼 수 있다. 보고 잘 흉내내 본다.

할 수 있어요

  1. redux 스토어와 루트 리듀서를 만든다.
  2. 내비게이션 리듀서를 만든다.
  3. App.js에서 렌더링했던 AppNavigatornavigator 속성으로 addNavigationHelpers({ dispatch, state })을 넘겨준다.
  4. 3을 위해서는 AppNavigator를 렌더링하는 부모 컴포넌트를 redux에 연결해주어야 한다.
  5. 4에서 만든 redux에 연결한 컨테이너 컴포넌트를 Provider에게 순순히 넘겨준다.

1번은 스킵하고 2번을 잠시보자. 이렇게 생겼다.

import { AppNavigator } from './AppNavigator'; // 아까 걔임..

const initialState = AppNavigator.router.getStateForAction(
  AppNavigator.router.getActionForPathAndParams('Home') // 액션을 반환한다: {type: 'Navigation/NAVIGATE', 'routeName': 'Home'}
);

const navigatorReducer = (state = initialState, action) => {
  const nextState = AppNavigator.router.getStateForAction(action, state);

  return nextState || state;
};

export { navigatorReducer };

Custom Router API가 사용되어 혼란스러워 보이지만, 초기 상태를 설정하고 라우팅 액션이 들어올 때 마다 새로운 상태를 반환하는 리듀서일 뿐이다. 조금 있다가 이 광경을 Redux 개발자도구로 보면 마음에 평화가 찾아오니 일단 지나가자.

공식문서의 예제에서는 App 컴포넌트를 redux에 연결하고, AppNavigatornavigator 속성을 추가해준다. 이후에 Root 컴포넌트를 새로 만들어서 App 컴포넌트를 렌더링한다. 나 같은 경우에는 App 위에 Root를 두는 대신 NavigatorRoot를 만들었다. 별 차이는 없다. 그리고 Expo는 최상위 컴포넌트를 기본적으로 App.js에 선언하는데, 이걸 변경하려면 app.json에서 entryPoint를 설정하고, 최상위 컴포넌트를 설정하는 파일도 생성해주어야 하니 Root를 쓰려면 번거롭다.

const AppNavigatorWithNavigationState = ({ dispatch, navigator }) => (
  <AppNavigator
    navigation={addNavigationHelpers({
      dispatch,
      state: navigator,
    })}
  />
);

const mapStateToProps = ({ navigator }) => ({ navigator });

const NavigatorRoot = connect(mapStateToProps)(AppNavigatorWithNavigationState);

export { NavigatorRoot };

이럴 수가

늘 한결같은 모습에 반해버림

지금 막 뭔가 대단한 일을 벌인 것 같은데 애석하게도 화면 상의 변화는 없다. 이거 그럼 뭐하러 함!? 이제 준공식을 마쳤으니 삼천포 톨게이트로 차를 보내야겠다. 여기까지 작성하고 커밋한 결과는 여기에 있다.

React Native Debugger 연결하기

redux 개발자도구에서 제대로 redux와 연결되었는지 작동해보고 싶다. React Native Debugger라는 신통방통한 도구가 있어서 써보려고 한다. Electron으로 만들어진 프로그램이라 먼저 릴리스 페이지에 가서 내려받는다.

react-native-debugger-open를 devDependencies에 추가하고 package.json의 scripts에 아래와 같이 명령을 추가한다. XDE에서 실행하면 React Native Debugger가 잘 작동하지 않아서 cli로 돌아갔다. (이제 와서!?) React Native Debugger의 가이드를 보면 postinstall을 사용하게 되어있는데, 제대로 안 찾아봤지만 아마도 메트로 번들러에서 이 명령을 실행하기 때문인 모양인데, Expo는 이 명령을 실행하지 않으니 그냥 start 명령을 추가하기로 했다.

yarn add --dev react-native-debugger-open exp
// package.json
"scripts": {
  "start": "yarn run rndebugger && exp start",
  "rndebugger": "rndebugger-open --revert && rndebugger-open --open --expo"
},

--expo 옵션을 주는 이유는 Expo의 경우 원격 디버깅에 19001 포트를 사용하기 때문이다.

redux 개발자도구에 연결할 수 있도록 store도 약간 수정해야 한다.

import { createStore } from 'redux';
import { reducer } from './reducer';

const store = createStore(
  reducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
);

export { store };

위와 같이 설정하고 이렇게 실행해본다. 뒤에 --lan 옵션은 Expo cli 옵션인데, 로컬 ip로 QR코드를 생성해준다. Expo가 제공하는 터널을 통해서 원격 디버깅을 너무나 느리기 때문에 이렇게 하는 편이 낫다. 다만 PC와 테스트 기기가 같은 무선망에 연결되어 있어야 한다. --lan을 따로 붙이는 이유는 시뮬레이터로 실행한다거나 하는 다른 cli 명령들도 편의대로 추가할 수 있기 때문이다.

$ yarn start --lan

위의 명령을 실행하면 맥에서는 React Native Debugger를 잘 설치했으면 자동으로 실행도 된다. 윈도우는 실행까지는 되지 않으니, 미리 실행해놓아야 한다. 결과를 보자 헤헤.

React Native Debugger

참고로 위 화면에서 안드로이드는 실제 기기를 연결했다. 삼성 스마트폰의 경우 SideSync라는 프로그램을 쓸 수 있다.

여기까지 마치고 커밋한 결과는 여기에서 확인할 수 있다.

내비게이션 액션 처리

NavigationActions.navigate()를 디스패치해주면, redux를 통해서 내비게이션을 변경할 수 있다.

import { NavigationActions } from 'react-navigation';

dispatch(NavigationActions.navigate({ routeName: 'Home', params: {} }));

params의 관리는 물음표인데, 적절한 가이드를 찾지는 못했다. redux를 사용하지 않을 때는 각 스크린에서 this.props.navigation.state.params까지 산넘고 물건너 찾아가야 하는 불편함이 있다. 디스패치한 params 값을 내비게이션 리듀서의 action에서 받을 수도 있기 때문에, 타입이 'Navigation/NAVIGATE'인 액션을 기준으로 적절히 관리해보고 있다. 이 글을 보시는 분 중에 좋은 방법을 알고 계시는 분이 있다면 댓글 좀 부탁드립니다. (굽신굽신…)

그나저나 가벼운 마음으로 글을 쓰려고 시작했는데 생각보다 많은 것(?)을 쓰고 말았다. 오늘은 이쯤에서 얼렁뚱땅 마무리를 짓겠다. 다음 이 시간까지 안녕~

2017년 12월 17일

© 2017 Hyunchul Kwak. All rights reserved.