[React Native] React Navigation - 16. Nesting Navigators(화면 구조 설계)

2022. 1. 23. 17:17React Native/Basic

 

 


 


 


15. Nesting Navigators(화면 구조 설계)

 

Stack, Drawer, Tab 여러 개의 navigator를 같이 쓰는 방법을 알아보자.

 

react navigation에서는 navigator를 nesting한다고 표현한다. Navigator들과 스크린 간의 구조를 잡는 건데 어떤 structure를 잡느냐에 따라 다양한 설계가 가능하다.

 

1. stack, tab nesting

stack navigator는 하위에 stack screen을 갖는데 여기서 하나의 stack screen에 컴포넌트로 tab navigator와 tab screen을 넣으면 된다. 자세한 구조는 아래 App.js 에서 주석으로 적어뒀다.

MainScreen은 tab navigator와 하위의 tab screen들을 리턴하도록 만든 함수로 전체가 stack navigator의 컴포넌트로 들어간다.

render 함수에 리턴되는 값은 Stack.Navigator 함수로 감싸고 안에 Stack.Screen 태그가 들어가는데 컴포넌트로 방금 만든 MainScreen을 가진다.

그리고 다른 Stack.Screen의 컴포넌트로 다른 stack screen 을 지정해주면 된다. 이 Stack Screen은 스택으로 이동하기 때문에 버튼을 만들어 이동할 수 있도록 onPress에 this.props.navigation.navigate()를 해줘야 한다.

 

실행 결과를 보면 Tab Navigator임에도 불구하고 위에 header bar가 생겼다. 이는 Stack Screen의 컴포넌트이기 때문이다. 각각의 탭을 눌러봐도 헤더바는 바뀌지 않고 똑같이 존재한다. 모든 탭 스크린이 하나의 스택 스크린 컴포넌트로 묶여 있어서 그런 것이다. 여기서 버튼을 눌러 다른 스택 스크린으로 이동하게 되면 헤더바가 다른 스택 스크린에서 지정한 헤더바로 변한다.

 

+) react navigation ver.5에서는 tab navigator에서 헤더바가 안 보인 것 같지만 ver.6에서는 기본적으로 헤더바가 보이게 된다. 따라서 스택 스크린의 헤더바와 탭 스크린의 헤더바가 둘 다 나타나 2개의 헤더바가 생기게 되는데 탭스크린의 헤더바를 숨기고 싶다면 아래 코드와 같이 작성하면 된다.

<Tab.Navigator screenOptions={{ headerShown: false }}>

 

스택과 탭을 nesting 했는데 화면 이동 뿐 아니라 params를 주고받을 수도 있다. 스택 스크린의 컴포넌트로 정의된 home.js를 보면 params를 넘기고 있다. params를 전달받는 user_tab.js에서 console.warn 함수를 통해 params를 확인하게 되면 warning 창으로 전달했던 params를 모두 확인할 수 있다. 즉, navigator를 nesting 할 때 params 값들을 패싱하는 것도 충분히 가능하다.

 

home.js

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow strict-local
 */

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

import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';

class HomeScreen extends Component {
  render() {
    return (
      <View
        style={{
          flex: 1,
          alignItems: 'center',
          justifyContent: 'center',
        }}>
        <Text>Home Screen</Text>
        <Button
          title="To User Screen"
          onPress={() => {
            this.props.navigation.navigate('User', {
              userIdx: 100,
              userName: 'Gildong',
              userLastName: 'Hong',
            });
          }}
        />
        <Button
          title="Change the title"
          onPress={() => {
            this.props.navigation.setOptions({
              title: 'Changed!!!',
              headerStyle: {
                backgroundColor: 'pink',
              },
              headerTintColor: 'red',
            });
          }}></Button>
      </View>
    );
  }
}

const styles = StyleSheet.create({});

export default HomeScreen;

user_tab.js

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow strict-local
 */

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

import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import Logo from './assets/pics/home_icon.png';

class TabUserScreen extends Component {
  render() {
    console.warn(this.props.route);
    return (
      <View
        style={{
          flex: 1,
          alignItems: 'center',
          justifyContent: 'center',
        }}>
        <Text>User Screen</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({});

export default TabUserScreen;

 

App.js

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow strict-local
 */

import React, {Component} from 'react';
import {StyleSheet, Text, View, Image, Button, Linking} from 'react-native';
import {DrawerActions, NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {
  createDrawerNavigator,
  DrawerContentScrollView,
  DrawerItem,
  DrawerItemList,
} from '@react-navigation/drawer';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import StackHomeScreen from './src/home';
import UserScreen from './src/user';
import LogoTitle from './src/logo';
import DrawerHomeScreen from './src/home_drawer';
import DrawerUserScreen from './src/user_drawer';
import PictogramHome from './src/assets/pics/home_icon.png';
import SideDrawer from './src/my_drawer';
import TabHomeScreen from './src/home_tab';
import TabUserScreen from './src/user_tab';
import TabMessageScreen from './src/message_tab';
import Icon from 'react-native-vector-icons/dist/Ionicons';
import Ionicons from 'react-native-vector-icons/dist/Ionicons';

const Stack = createNativeStackNavigator();
const Drawer = createDrawerNavigator();
const Tab = createBottomTabNavigator();

/*
  Stack Navigator
    - Tab Navigator
      - Tab Screen D
      - Tab Screen E
      - Tab Screen F
    - Stack Screen B
    - Stack Screen C
*/

MainScreen = () => {
  return (
    <Tab.Navigator
      initialRouteName="Home"
      screenOptions={({route}) => ({
        tabBarLabel: route.name,
        tabBarIcon: ({focused}) => TabBarIcon(focused, route.name),

        tabBarActiveBackgroundColor: 'skyblue',
        tabBarInactiveBackgroundColor: '#c6cbef',
        tabBarActiveTintColor: 'blue',
        tabBarInactiveTintColor: '#fff',
        // tabBarLabelPosition: 'beside-icon',
        tabBarLabelPosition: 'below-icon',
        headerShown: false
      })}>
      <Tab.Screen name="Home" component={TabHomeScreen} />
      <Tab.Screen name="User" component={TabUserScreen} />
      <Tab.Screen name="Message" component={TabMessageScreen} />
    </Tab.Navigator>
  );
};

const TabBarIcon = (focused, name) => {
  let iconImagePath;
  let iconName, iconSize;

  if (name == 'Home') {
    iconName = 'home-outline';
    // iconImagePath = require('./src/assets/pics/home_icon.png');
  } else if (name == 'User') {
    iconName = 'people-outline';
    // iconImagePath = require('./src/assets/pics/user_icon.png');
  } else if (name == 'Message') {
    iconName = 'mail-outline';
    // iconImagePath = require('./src/assets/pics/message_icon.png');
  }
  iconSize = focused ? 30 : 20;
  return (
    <Ionicons name={iconName} size={iconSize} />
    // <Image
    //   style={{
    //     width: focused ? 30 : 20,
    //     height: focused ? 30 : 20,
    //   }}
    //   source={iconImagePath}
    // />
  );
};
// CustomDrawerContent = props => {
//   return (
//     <DrawerContentScrollView {...props}>
//       <DrawerItemList {...props} />
//       <DrawerItem
//         label="Help"
//         onPress={() => Linking.openURL('http://www.google.com')}
//         icon={() => <LogoTitle />}
//       />
//       <DrawerItem label="Info" onPress={() => alert('Info window')} />
//     </DrawerContentScrollView>
//   );
// };

class App extends Component {
  ////////////////////////////////////////stack navigator의 일부
  // logoTitle = () => {
  //   return (
  //     <Image
  //       style={{width: 40, height: 40}}
  //       source={require('./src/assets/pics/home_icon.png')}
  //     />
  //   );
  // };

  render() {
    return (
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen name="Main" component={MainScreen}/>
          <Stack.Screen name="Home_Stack" component={StackHomeScreen}/>

          </Stack.Navigator>
      </NavigationContainer>

      ////////////////////////////////////drawer navigator
      // <NavigationContainer>
      //   <Drawer.Navigator
      //     initialRouteName="Home"
      //     screenOptions={{
      //       drawerType: 'front',
      //       drawerPosition: 'right',
      //       drawerStyle: {
      //         backgroundColor: '#c6cbef',
      //         width: 200,
      //       },
      //       drawerActiveTintColor: 'red',
      //       drawerActiveBackgroundColor: 'skyblue',
      //     }}
      //     drawerContent={props => <SideDrawer {...props} />}>
      //     <Drawer.Screen
      //       name="Home"
      //       component={DrawerHomeScreen}
      //       options={{
      //         drawerIcon: () => {
      //           <Image
      //             source={PictogramHome}
      //             style={{width: 40, height: 40}}
      //           />;
      //         },
      //       }}
      //     />
      //     <Drawer.Screen name="User" component={DrawerUserScreen} />
      //   </Drawer.Navigator>
      // </NavigationContainer>

      ////////////////////////////////////////stack navigator
      // <NavigationContainer>
      //   <Stack.Navigator
      //     initialRouteName="Home"
      //     screenOptions={{
      //       headerStyle: {
      //         backgroundColor: '#a4511e',
      //       },
      //       headerTintColor: '#ffffff',
      //       headerTitleStyle: {
      //         fontWeight: 'bold',
      //         color: '#f3d612',
      //       },
      //     }}>
      //     <Stack.Screen
      //       name="Home"
      //       component={HomeScreen}
      //       options={{
      //         title: 'Home Screen',
      //         headerTitle: () => <this.logoTitle />,
      //         headerRight: () => (
      //           <Button
      //             title="info"
      //             onPress={() => alert('I am a button!!')}
      //             color="orange"
      //           />
      //         ),
      //       }}
      //     />
      //     <Stack.Screen
      //       name="User"
      //       component={UserScreen}
      //       initialParams={{
      //         userIdx: 50,
      //         userName: 'Gildong',
      //         userLastName: 'Go',
      //       }}
      //       options={{
      //         title: 'User Screen',
      //         headerStyle: {
      //           backgroundColor: 'pink',
      //         },
      //         headerTintColor: 'red',
      //         headerTitleStyle: {
      //           fontWeight: 'bold',
      //           color: 'purple',
      //         },
      //       }}
      //     />
      //   </Stack.Navigator>
      // </NavigationContainer>
    );
  }
}

const styles = StyleSheet.create({});

export default App;

2. stack, drawer, tab nesting

위에서 다뤘던 구조에서 Tab Navigator 자리에 Drawer Navigator를 넣고 Drawer Screen을 그 하위에 배치한다. 그리고 하나의 Drawer Screen을 Tab Navigator로 바꾼 뒤 그 하위에 또 Tab Screen을 두자. 근데 Drawer Content랑 linking 되는 자리에 Drawer Screen 자리에 Tab Navigator가 들어가는 게 떠올리기 어렵다. 엄밀히 얘기하면 Drawer Content와 각 Screen을 linking 하는 컴포넌트는 Drawer Navigator의 Drawer Content 프로퍼티를 이용해서 구현할 것이기 때문에 각 Drawer Screen은 Drawer Navigator에 걸리게 된다.

/*
  Stack Navigator
    - Drawer Navigator
      - Drawer Screen D
      - Drawer Screen E
      - Tab Navigator
        - Tab Screen F
        - Tab Screen G
    - Stack Screen B
    - Stack Screen C
*/

즉, 위의 구조는 아래 구조와 같이 생각하면 된다. 그리고 하위에 Tab Navigator만이 Drawer Screen으로 구현이 되는데 그 하위에 Tab Screen 들이 있는 것이다.

/*
  Stack Navigator
    - Drawer Navigator w/ Drawer Screen C, D, ...
      - Tab Navigator
        - Tab Screen F
        - Tab Screen G
    - Stack Screen B
    - Stack Screen C
*/

 

예전에 만든 Drawer Navigator를 DrawerComponent라는 함수로 새로 만들어 준다. 여기서 Home 스크린을 하나 지우고 User 스크린의 이름을 Route로 바꾼 뒤 컴포넌트를 TabComponent 라고 한다. 그리고 이전에 만들었던 MainScreen의 이름을 TabComponent로 바꿔준다. 우리가 만든 side drawer는 첫번째 스택 스크린 안에서만 유효하다.

 

앱을 실행해보면 스택 스크린끼리 이동도 잘 된다. 첫 번째 스택 스크린 안에서만 Drawer Navigator가 잘 동작하도록 설계했기 때문에 첫번째 스택 스크린에서만 drawer navigator가 적용되고 그 안에서 Tab 도 잘 이동하는 걸 확인할 수 있다.

 

한 가지 옵션을 더 주자면 스택 스크린이니까 헤더바에 옵션을 줄 수 있는데 버튼을 하나 만들어 버튼을 눌렀을 때 side drawer 가 열리게 만들어 보자. 사실 react navigation ver.6에서는 drawer navigator가 자동으로 헤더바를 지원해주지만 headerShown을 이용해 이걸 지우고 일단 강의를 따라가 보자.

 

DrawerComponent라는 컴포넌트를 갖는 Stack Screen의 options에서 headerRight라는 값을 줘서 <HeaderRight/>라는 이름의 커스텀 함수를 넣어준다. DrawerActions, useNavigation을 import 하고, 새로운 변수를 만들어 해당 변수가 navigation이라는 props를 가질 수 있도록 useNavigation을 함수를 할당해준다. <View> 안에 <TouchableOpacity> 태그를 쓰고 그 안에 <Text>Open</Text> 를 작성한다. View 에 style은 flexDirection 값은 row로 주고, paddingRight를 할당한다. 그리고 <TouchableOpacity> 안에 onPress를 통해 navigation.dispatch(DrawerAction.openDrawer())를 실행하도록 한다. openDrawer라는 DrawerAction을 navigation이 dispatch 해서 state를 업데이트하는 것이다.

 

위 내용은 어려울 수 있지만 이 내용은 redux를 이해한 뒤에 들으면 보다 더 잘 이해할 수 있다. 

 

위의 코드를 작성하면 Open 텍스트 버튼을 누르면 side drawer 가 잘 열리고 각각의 ContentItem을 누르면 이동도 잘 동작함을 확인할 수 있다.

 

 

App.js

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow strict-local
 */

import React, {Component} from 'react';
import {StyleSheet, Text, View, Image, Button, Linking} from 'react-native';
import {
  DrawerActions,
  NavigationContainer,
  useNavigation,
} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {
  createDrawerNavigator,
  DrawerContentScrollView,
  DrawerItem,
  DrawerItemList,
} from '@react-navigation/drawer';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import StackHomeScreen from './src/home';
import UserScreen from './src/user';
import LogoTitle from './src/logo';
import DrawerHomeScreen from './src/home_drawer';
import DrawerUserScreen from './src/user_drawer';
import PictogramHome from './src/assets/pics/home_icon.png';
import SideDrawer from './src/my_drawer';
import TabHomeScreen from './src/home_tab';
import TabUserScreen from './src/user_tab';
import TabMessageScreen from './src/message_tab';
import Icon from 'react-native-vector-icons/dist/Ionicons';
import Ionicons from 'react-native-vector-icons/dist/Ionicons';
import {TouchableOpacity} from 'react-native-gesture-handler';

const Stack = createNativeStackNavigator();
const Drawer = createDrawerNavigator();
const Tab = createBottomTabNavigator();

/*
  Stack Navigator
    - Drawer Navigator w/ Drawer Screen C, D, ...
      - Drawer Screen D
      - Drawer Screen E
      - Tab Navigator
        - Tab Screen F
        - Tab Screen G
    - Stack Screen B
    - Stack Screen C
*/

const TabComponent = () => {
  return (
    <Tab.Navigator
      initialRouteName="Home"
      screenOptions={({route}) => ({
        tabBarLabel: route.name,
        tabBarIcon: ({focused}) => TabBarIcon(focused, route.name),

        tabBarActiveBackgroundColor: 'skyblue',
        tabBarInactiveBackgroundColor: '#c6cbef',
        tabBarActiveTintColor: 'blue',
        tabBarInactiveTintColor: '#fff',
        // tabBarLabelPosition: 'beside-icon',
        tabBarLabelPosition: 'below-icon',
        headerShown: false,
      })}>
      <Tab.Screen name="Home" component={TabHomeScreen} />
      <Tab.Screen name="User" component={TabUserScreen} />
      <Tab.Screen name="Message" component={TabMessageScreen} />
    </Tab.Navigator>
  );
};

const TabBarIcon = (focused, name) => {
  let iconImagePath;
  let iconName, iconSize;

  if (name == 'Home') {
    iconName = 'home-outline';
    // iconImagePath = require('./src/assets/pics/home_icon.png');
  } else if (name == 'User') {
    iconName = 'people-outline';
    // iconImagePath = require('./src/assets/pics/user_icon.png');
  } else if (name == 'Message') {
    iconName = 'mail-outline';
    // iconImagePath = require('./src/assets/pics/message_icon.png');
  }
  iconSize = focused ? 30 : 20;
  return (
    <Ionicons name={iconName} size={iconSize} />
    // <Image
    //   style={{
    //     width: focused ? 30 : 20,
    //     height: focused ? 30 : 20,
    //   }}
    //   source={iconImagePath}
    // />
  );
};
// CustomDrawerContent = props => {
//   return (
//     <DrawerContentScrollView {...props}>
//       <DrawerItemList {...props} />
//       <DrawerItem
//         label="Help"
//         onPress={() => Linking.openURL('http://www.google.com')}
//         icon={() => <LogoTitle />}
//       />
//       <DrawerItem label="Info" onPress={() => alert('Info window')} />
//     </DrawerContentScrollView>
//   );
// };

const DrawerComponent = () => {
  return (
    <Drawer.Navigator
      initialRouteName="Home"
      screenOptions={{
        drawerType: 'front',
        drawerPosition: 'right',
        drawerStyle: {
          backgroundColor: '#c6cbef',
          width: 200,
        },
        drawerActiveTintColor: 'red',
        drawerActiveBackgroundColor: 'skyblue',
        headerShown: false,
      }}
      drawerContent={props => <SideDrawer {...props} />}>
      <Drawer.Screen name="Route" component={TabComponent} />
    </Drawer.Navigator>
  );
};

const HeaderRight = () => {
  const navigation = useNavigation();
  return (
    <View style={{flexDirection: 'row', paddingRight: 15}}>
      <TouchableOpacity
        onPress={() => {
          navigation.dispatch(DrawerActions.openDrawer());
        }}>
        <Text>Open</Text>
      </TouchableOpacity>
    </View>
  );
};

class App extends Component {
  ////////////////////////////////////////stack navigator의 일부
  // logoTitle = () => {
  //   return (
  //     <Image
  //       style={{width: 40, height: 40}}
  //       source={require('./src/assets/pics/home_icon.png')}
  //     />
  //   );
  // };

  render() {
    return (
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen
            name="Main"
            component={DrawerComponent}
            options={{
              headerRight: ({}) => <HeaderRight />,
            }}
          />
          <Stack.Screen name="Home_Stack" component={StackHomeScreen} />
        </Stack.Navigator>
      </NavigationContainer>

      ////////////////////////////////////drawer navigator
      // <NavigationContainer>
      //   <Drawer.Navigator
      //     initialRouteName="Home"
      //     screenOptions={{
      //       drawerType: 'front',
      //       drawerPosition: 'right',
      //       drawerStyle: {
      //         backgroundColor: '#c6cbef',
      //         width: 200,
      //       },
      //       drawerActiveTintColor: 'red',
      //       drawerActiveBackgroundColor: 'skyblue',
      //     }}
      //     drawerContent={props => <SideDrawer {...props} />}>
      //     <Drawer.Screen
      //       name="Home"
      //       component={DrawerHomeScreen}
      //       options={{
      //         drawerIcon: () => {
      //           <Image
      //             source={PictogramHome}
      //             style={{width: 40, height: 40}}
      //           />;
      //         },
      //       }}
      //     />
      //     <Drawer.Screen name="User" component={DrawerUserScreen} />
      //   </Drawer.Navigator>
      // </NavigationContainer>

      ////////////////////////////////////////stack navigator
      // <NavigationContainer>
      //   <Stack.Navigator
      //     initialRouteName="Home"
      //     screenOptions={{
      //       headerStyle: {
      //         backgroundColor: '#a4511e',
      //       },
      //       headerTintColor: '#ffffff',
      //       headerTitleStyle: {
      //         fontWeight: 'bold',
      //         color: '#f3d612',
      //       },
      //     }}>
      //     <Stack.Screen
      //       name="Home"
      //       component={HomeScreen}
      //       options={{
      //         title: 'Home Screen',
      //         headerTitle: () => <this.logoTitle />,
      //         headerRight: () => (
      //           <Button
      //             title="info"
      //             onPress={() => alert('I am a button!!')}
      //             color="orange"
      //           />
      //         ),
      //       }}
      //     />
      //     <Stack.Screen
      //       name="User"
      //       component={UserScreen}
      //       initialParams={{
      //         userIdx: 50,
      //         userName: 'Gildong',
      //         userLastName: 'Go',
      //       }}
      //       options={{
      //         title: 'User Screen',
      //         headerStyle: {
      //           backgroundColor: 'pink',
      //         },
      //         headerTintColor: 'red',
      //         headerTitleStyle: {
      //           fontWeight: 'bold',
      //           color: 'purple',
      //         },
      //       }}
      //     />
      //   </Stack.Navigator>
      // </NavigationContainer>
    );
  }
}

const styles = StyleSheet.create({});

export default App;

 

다음 강부터는 Phone Resource 에 대해서 배워보자.


참고 자료

https://www.inflearn.com/course/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-%EA%B8%B0%EC%B4%88/dashboard

 

iOS/Android 앱 개발을 위한 실전 React Native - Basic - 인프런 | 강의

Mobile App Front-End 개발을 위한 React Native의 기초 지식 습득을 목표로 하고 있습니다. 진입장벽이 낮은 언어/API의 활용을 통해 비전문가도 쉽게 Native Mobile App을 개발할 수 있도록 제작된 강의입니다

www.inflearn.com