어서와, 개발은 처음이지?

6.React Native Navigation(v1) 기초 - 3부 기능 살펴보기 본문

React/React Native

6.React Native Navigation(v1) 기초 - 3부 기능 살펴보기

오지고지리고알파고포켓몬고 2018. 8. 5. 22:12
반응형


이 글은

6.React Native Navigation 기초 - 1부 설치하기

6.React Native Navigation 기초 - 2부 화면 등록, 화면 이동(startapp, push, pop)

6.React Native Navigation 기초 - 3부 기능 살펴보기(현재글)로 구성되어 있습니다.


(19.08.05)react-navigation 예제가 추가되었습니다.



이번 글에서는 사이드 메뉴인 Drawer와 그를 통제하기 위한 Deeplink, 그리고 다른 주요 기능에 대해 알아보겠습니다.


1.Drawer


Drawer은 좌측 또는 우측에 숨어있는 메뉴입니다.

이 메뉴는 Navigation으로 앱을 실행하는 부분에서 설정할 수 있습니다.

Navigation.startSingleScreenApp()을 기준으로 아래와 같은 옵션을 가지고 있습니다.

Navigation.startSingleScreenApp({
  screen: {
    screen: 'example.WelcomeScreen', // unique ID registered with Navigation.registerScreen
    title: 'Welcome', // title of the screen as appears in the nav bar (optional)
    navigatorStyle: {}, // override the navigator style for the screen, see "Styling the navigator" below (optional)
    navigatorButtons: {} // override the nav buttons for the screen, see "Adding buttons to the navigator" below (optional)
  },
  drawer: {
    // optional, add this if you want a side menu drawer in your app
    left: {
      // optional, define if you want a drawer from the left
      screen: 'example.FirstSideMenu', // unique ID registered with Navigation.registerScreen
      passProps: {}, // simple serializable object that will pass as props to all top screens (optional)
      disableOpenGesture: false, // can the drawer be opened with a swipe instead of button (optional, Android only)
      fixedWidth: 500 // a fixed width you want your left drawer to have (optional)
    },
    right: {
      // optional, define if you want a drawer from the right
      screen: 'example.SecondSideMenu', // unique ID registered with Navigation.registerScreen
      passProps: {}, // simple serializable object that will pass as props to all top screens (optional)
      disableOpenGesture: false, // can the drawer be opened with a swipe instead of button (optional, Android only)
      fixedWidth: 500 // a fixed width you want your right drawer to have (optional)
    },
    style: {
      // ( iOS only )
      drawerShadow: true, // optional, add this if you want a side menu drawer shadow
      contentOverlayColor: 'rgba(0,0,0,0.25)', // optional, add this if you want a overlay color when drawer is open
      leftDrawerWidth: 50, // optional, add this if you want a define left drawer width (50=percent)
      rightDrawerWidth: 50 // optional, add this if you want a define right drawer width (50=percent)
    },
    type: 'MMDrawer', // optional, iOS only, types: 'TheSideBar', 'MMDrawer' default: 'MMDrawer'
    animationType: 'door', //optional, iOS only, for MMDrawer: 'door', 'parallax', 'slide', 'slide-and-scale'
    // for TheSideBar: 'airbnb', 'facebook', 'luvocracy','wunder-list'
    disableOpenGesture: false // optional, can the drawer, both right and left, be opened with a swipe instead of button
  },
  passProps: {}, // simple serializable object that will pass as props to all top screens (optional)
  animationType: 'slide-down' // optional, add transition animation to root change: 'none', 'slide-down', 'fade'
});

screen에는 처음 시작될 화면이 들어가며 이전 글의 TestProject/index.js에서 본적 있습니다.

drawer은 left, right로 좌 우 원하는 곳에 배치할 수 있으며 drawer.screen에 메뉴로 이용할 화면의 스크린 ID를 넣어줍니다.(예 'yuddomack.MenuScreen')

disableOpenGesture로 제스쳐(좌로 끌기, 우로 끌기)로 메뉴를 열고 닫는 기능을 설정할 수 있습니다.


일반적으로 안드로이드 메뉴가 iOS의 메뉴보다 큰편이며, iOS의 메뉴 크기를 조종하기 위해서는 drawer.style에서 크기 속성을 입력해주어야 합니다.

import React, { Component } from 'react';
import {
  View,
  Text,
  StyleSheet,
} from 'react-native';

export default class MenuScreen extends Component{
  constructor(props){
    super(props);
    this.state = {
      name: '',
    };
  }

  render(){
    return (
      <View style={styles.container}>
        <View style={styles.menus}>
          <Text style={{fontSize:25}}>반갑습니다 {this.state.name}님!</Text>
        </View>
        <View style={styles.menus}>
          <Text>회원정보수정</Text>
        </View>
        <View style={styles.menus}>
          <Text>설정</Text>
        </View>
      </View>
    )
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    backgroundColor: '#eee',
    paddingTop:20,
  },
  menus: {
    width:'100%',
    padding:10,
    borderBottomWidth:0.5,
    borderColor:'#444',
  }
});

이렇게 MenuScreen.js를 만들고 registerScreens()에 등록을 해준 뒤,

import {name as appName} from './app.json';
import { Navigation } from 'react-native-navigation';

import { registerScreens } from './src/screens';
import { registerComponents } from './src/components';

registerScreens();
registerComponents();

Navigation.startSingleScreenApp({
  screen: {
    screen: 'yuddomack.StartScreen', // unique ID registered with Navigation.registerScreen
    title: 'Welcome', // title of the screen as appears in the nav bar (optional)
    navigatorStyle: {
      navBarHidden: false,
    }, // override the navigator style for the screen, see "Styling the navigator" below (optional)
    navigatorButtons: {} // override the nav buttons for the screen, see "Adding buttons to the navigator" below (optional)
  },
  drawer: {
    right: {
      screen: 'yuddomack.MenuScreen'
    }
  }
});

TestProject/index.js에 위와 같이 drawer을 추가해주면 아래와 같은 화면이 나옵니다.

(응용은 여러분의 몫!)

2.DeepLink


딥 링크는 확성기 같은 기능입니다. 네트워크로 치면 브로드케스팅이라고 할까요?

Navigation에 등록된 모든 Screen에 text를 전달하는 역할을 합니다.


보통 상황에서는 주로 사용할 일이 없으나, Drawer 메뉴에 정보를 전달할 때 유용합니다.


Drawer 메뉴는 TestProject/index.js에서 drawer: {}을 통해 설정해줬습니다.

그렇다면 만약 로그인 후에 Drawer 메뉴에 환영 인사를 적으려면 어떻게할까요?


React Native Navigation 1.1에서는 현재 화면과 Drawer 메뉴는 state와 props가 독립적이기 때문에 뭔가 필요하다면 DeepLink를 통해서 전달해야 합니다.

그럼 일단, DeepLink를 수신하는 방법에 대해 알아보겠습니다.

export default class MenuScreen extends Component{
  constructor(props){
    super(props);
    this.state = {
      name: '',
    };
    this.props.navigator.setOnNavigatorEvent(this._deepLinkListener.bind(this));
  }

  _deepLinkListener(e){
    if (e.type == 'DeepLink') {
      const parts = e.link.split("/");
      if (parts[0] == 'setName') this._setName(parts[1]);
    }
  }

  _setName(name){
    this.setState((prevState, props) => {
      return {name: name};
    })
  }

  render(){
    ...생략
  }
}

DeepLink로 전달받을 값이 필요한 화면(MenuScreen.js)에 setOnNavigatorEvent()라는 함수에 딥링크 리스너를 달아줬습니다.

이제 navigator로 이벤트가 발생할때마다 _deepLinkListner() 함수가 콜백하게 됩니다.


딥링크 이벤트가 발생하면 type을 통해 식별이 가능합니다.

그렇다면 딥링크 이벤트는 어떻게 발생시킬 수 있을까요?

export default class LoginScreen extends Component<Props> {
  _pop(){
    this.props.navigator.pop({
      animated: true, // does the pop have transition animation or does it happen immediately (optional)
      animationType: 'fade', // 'fade' (for both) / 'slide-horizontal' (for android) does the pop have different transition animation (optional)
    });
  }

  _setName(){
    const name = 'yuddomack';
    this.props.navigator.handleDeepLink({
      link: 'setName/'+name,
    });
  }

  render() {
    ... 생략
  }
}

위 코드는 LoginScreen.js의 일부입니다.


18번 줄의 this.props.navigator.handleDeepLink({link: ''})이 DeepLink를 발생시킵니다.

handleDeepLink()를 사용하면 Navigation에 등록된 화면들에 이벤트가 전파되며, setOnNavigatorEvent(callback)의 callback을 통해 수신할 수 있게 됩니다.


결국 LoginScreen.js에서 'setName/yuddomack'이라는 DeepLink를 보내고, MenuScreen.js에서 /을 기준으로 명령을 나눈 뒤, 그를 메뉴에 표시하게 되는 것 입니다.


_setName() 함수는 로그인 화면의 확인 버튼에 달아두었으며 실질적으로 const name에 해당하는 내용은 fetch()를 통해 세션 정보를 받아오거나, 로컬 DB를 통해 가져오게 될 것입니다.


초기 상태에선 사용자 이름이 보이지 않지만 확인 버튼을 누르면 DeepLink를 호출하고, 이를 MenuScreen이 수신받아서 사용자 이름을 렌더링 한 모습을 볼 수 있습니다.




3.Modal


다음은 모달입니다.

push, pop이 화면의 전환을 제어했다면 모달은 팝업창 같은 친구입니다.


모달에서도 push, pop이 가능하기 때문에 적절한 화면 흐름을 구성한다면 좋은 UX를 구성할 수 있겠습니다.

사용법은 아래와 같습니다.

export default class JoinScreen extends Component<Props> {
  _showNotice(){
    this.props.navigator.showModal({
      screen: "yuddomack.NoticeScreen", // unique ID registered with Navigation.registerScreen
      title: "Notice", // title of the screen as appears in the nav bar (optional)
      passProps: {}, // simple serializable object that will pass as props to the modal (optional)
      navigatorStyle: {
        //navBarHidden: true,
      }, // override the navigator style for the screen, see "Styling the navigator" below (optional)
      animationType: 'slide-up' // 'none' / 'slide-up' , appear animation for the modal (optional, default 'slide-up')
    });
  }

  render() {
    ... 생략
  }
}

JoinScreen.js에서 회원가입 시 공지사항(뜬금없이?)을 보여주도록 했습니다.

this.props.navigator.showModal()을 사용하며, screen에 원하는 스크린 ID를 넣어주면 이 함수를 실행 했을 때 모달이 등장하게 됩니다.


만약 상단 타이틀 바를 가리고 싶다면 navigatorStyle에 navBarHidden 속성을 설정하면 됩니다.


그렇다면 NoticeScreen.js는?

아래와 같습니다.

import React, { Component } from 'react';
import {
  View,
  Text,
  StyleSheet,
} from 'react-native';
import CustomButton from '../components/CustomButton';

export default class NoticeScreen extends Component{
  _dismiss(){
    this.props.navigator.dismissModal({
      animationType: 'slide-down' // 'none' / 'slide-down' , dismiss animation for the modal (optional, default 'slide-down')
    });
  }

  render(){
    return (
      <View style={styles.container}>
        <View style={styles.content}>
          <Text style={{fontSize: 40}}>공지사항</Text>
        </View>
        <View style={styles.footer}>
          <CustomButton
            title={'닫기'}
            onPress={this._dismiss.bind(this)}/>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 10,
  },
  content: {
    flex: 9,
  },
  footer: {
    flex: 1,
  }
})

안드로이드 폰에는 뒤로가기 버튼이 있기 때문에 쉽게 모달을 닫을 수 있지만 아이폰에서는 제스처로 닫을 수 없기 때문에 닫는 버튼을 만들어줬습니다.

this.props.navigator.dismissModal()을 호출하면 모달이 닫히게 됩니다. 

4.Custom TopBar


Custom TopBar란 이전의 화면에서 title에 해당하는 영역을 사용자가 원하는 모습으로 커스터마이징 할 수 있는 속성입니다.

우선 CustomTopBar.js를 만들어 줍니다.

import React, {Component} from 'react';
import {
  StyleSheet,
  View,
  Text,
  TouchableOpacity,
  Platform
} from 'react-native';
import CustomButton from '../components/CustomButton';

export default class CustomTopBar extends Component {

  render() {
    return (
      <View style={styles.container}>
        <CustomButton
          title="홈"/>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    alignItems: 'center',
    flex: 1,
    backgroundColor: 'yellow',
  },
});

이렇게하면 title 영역 한가운데에 홈 이라는 버튼이 만들어질 것 입니다.


원래는 react-native-vector-icon 같은 라이브러리를 사용해서 여러가지 아이콘으로 메뉴를 구성할 수 있지만, 갈수록 집중력이 떨어져서 그냥 CustomButton으로 해버렸습니다ㅠㅠ


이제 CustomTopBar를 사용하고 싶은 Screen으로 이동합니다.

여기서는 StartScreen.js에 적용해보도록 하겠습니다.

export default class StartScreen extends Component<Props> {
  static navigatorStyle = {
    navBarCustomView: 'yuddomack.CustomTopBar',
    navBarComponentAlignment: 'fill', // center, fill
  };

  _pushJoin(){
    this.props.navigator.push({
      screen: 'yuddomack.JoinScreen', // unique ID registered with Navigation.registerScreen
      title: 'Join', // navigation bar title of the pushed screen (optional)
      passProps: {}, // Object that will be passed as props to the pushed screen (optional)
    });
  }

  _pushLogin(){
    this.props.navigator.push({
      screen: 'yuddomack.LoginScreen', // unique ID registered with Navigation.registerScreen
      title: 'Login', // navigation bar title of the pushed screen (optional)
      passProps: {}, // Object that will be passed as props to the pushed screen (optional)
    });
  }

  render() {
    ... 생략
  }
}

일단 CustomTopBar는 React Native Navigation v1.1에서 잡음이 많은 기능이니 여러가지를 짚고 화면을 보도록 하겠습니다.


- 적용 방법


CustomTopBar를 적용하는 방법은 2가지가 있는데, 이렇게 static 변수로 설정하는 방법과 this.props.push(...navigatorStyle: { })를 통해 이전화면에서 설정을 해준 채로 전달해주는 방법입니다.

(Navigation으로 등록된 컴포넌트는 static 변수로 네비게이터 스타일(navigatorStyle)과 버튼(navigatorButton)을 설정할 수 있습니다.)


경험상으로 전달해주는 방법이 render 과정이 깔끔했습니다. static 변수로 설정하면 가끔 뷰가 로드 된 다음 navigatorStyle이 적용되는게 보일 때가 있었습니다.

하지만 안드로이드에서 CustomTopBar이 적용됐다 안됐다 하는 현상이 있는데 static으로 설정했을 때 좀 더 안정적이였습니다.


- CustomTopBar에 props를 전달하는 방법


navigatorStyle에 navBarCustomViewInitialProps: {key, value}를 추가해주면 됩니다.

static navigatorStyle = {
  navBarCustomView: 'yuddomack.CustomTopBar',
  navBarComponentAlignment: 'fill', // center, fill
  navBarCustomViewInitialProps: {
    name: 'yuddomack'
  }
};
// 혹은
this.props.navigator.push({
  screen: 'example.ScreenThree', // unique ID registered with Navigation.registerScreen
  title: undefined, // navigation bar title of the pushed screen (optional)
  navigatorStyle: {
    navBarCustomViewInitialProps:{
      navigator: this.props.navigator
    }
  }, // override the navigator style for the pushed screen (optional)
});

navBarCustomView로 설정된 screen에서는 navigator 객체가 존재하지 않습니다.

설정해준 채로 전달해주는 방법에서만 가능하지만 위와 같이 navigator도 전달할 수 있습니다.

이런 방법을 사용하면 CustomTopBar.js에서 홈 버튼을 누르면 처음 화면으로 가도록(pushToRoot) 제어할 수 있습니다.


- navBarComponentAlignment 속성


navBarComponentAlignment속성은 fill과 center로 구성되어있는데, fill은 title 영역을 꽉 채우는 형태이고 center은 가운데 정렬과 같습니다.

CustomTopBar의 container속성을 flex: 1로 하고 배경색을 넣으신 다음 두 속성을 비교해보시면 차이를 느낄 수 있습니다.


여기 특이점이 하나 있는데 fill 속성으로하면 안드로이드에서 보였다 안보였다 하는 현상이 발생합니다.


- navigatorButton 속성


navigatorButton은 CustomTopBar처럼 사용자가 커스텀해서 좌 우측 버튼을 지정할 수 있는 속성입니다.

보통 우측에 메뉴 아이콘을 놓고 Drawer 메뉴를 열 수 있도록 합니다.

다만 안드로이드에서 좌측 버튼은 무조건 뒤로가기가 위치하기 때문에 이 부분을 주의하셔야합니다.


RNN은 안드로이드하고 잘 안맞는 것 같네요..

CustomTopBar를 적용한 모습은 위와 같습니다.

navBarComponentAlignment 속성을 fill로 해준 상태이고, title영역을 구분할 수 있도록 노란색 배경을 넣어줬습니다.


만약 center로 설정한다면 자식 컴포넌트 크기만큼만의 노란색 배경을 볼 수 있습니다.




5.요약


* Drawer은 좌 우측에 배치 가능한 사이드 버튼. state 변경이나 이벤트가 필요할 땐 deeplink를 사용해야함

* Modal은 팝업형태

* CustomTopBar는 Navbar을 사용자가 커스텀할 수 있도록 제공하는 속성. 다만 v1.1에선 여러가지 잡음이 있음



6.마무리


3장에 거쳐 React Native Navigation의 기초에 대해 알아봤습니다.

소개되지 않은 여러가지 기능, 속성이 있으니 공식 문서를 보시고 적용해보시기 바랍니다.


만약 '하고싶은게 있는데 제공되는 기능인지' 같은 질문이나 궁금한 사항이 있으면 글 남겨주세요!

저도 rnn에 대해 완벽히 아는게 아니라서 바로 대답은 못드리겠지만 최대한 빨리 알아보도록 하겠습니다 m_m


다음 글부터는 Animated를 사용해서 애니메이션을 만들어보도록 하겠습니다.


Comming Soon!




Comments