[React-native] nested navigation, bottom tab modal 적용하기 (navigation v6)

흑수·2021년 8월 6일
3

React-native

목록 보기
2/6

안녕하세요,,

하단 네비게이션을 react navigation v4에서 v6 버전으로 싹 다 고치면서 이것 저것 시도해봤어요.

그 중에 하고 싶었던 것 중 하나가 bottom tab 클릭시 현재 stack에서 modal을 띄우는 형태였는데 며칠 끙끙대다가 드디어 해결했어요.

모바일 유튜브 어플처럼 말이에요!

구조

기본적으로 stack navigation과 tab navigation이 섞여 있는 navigation을 설계해볼게요. 저희 음파도 그렇게 진행을 했답니다.

즉, 큰 틀은 tab navigation이지만 각각의 탭은 stack navigation으로 이루어진,,
어플리케이션 자체가 3개의 하단 탭(Feed, Post, Profile)이 존재한다고 생각해봅시다!

이 때, 어느정도의 플로우가 있는 앱이라면 각각의 탭에서도 다른 화면으로 넘어갈 수 있겠죠? 가령, Feed탭에서 다른 사람의 포스팅과 같은 화면으로, 혹은 Profile 탭에서 프로필 수정과 같은 탭으로 말이에요!

  • 화면 전환이 이루어지면 어떻게 해야할까요?

전에도 이야기했지만 global한 Stack이 없기 때문에 각자의 Stack이 존재한다고 이야기했어요.

더 궁금하신분은 이쪽으로!!
https://velog.io/@blacksooooo/React-native-%EA%B8%B0%EB%B3%B8%EC%A0%81%EC%9D%B8-navigation

그 말은 즉!! 큰 틀은 tab navigation이지만 각각의 탭은 stack navigation !!

조금 이해가 되시나요?

Nested navigation

그럼 이제 위에서 언급한 Feed, Post, Profile로 하단바를 구성해볼게요.

Stack navigation

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { View, Text, Button } from 'react-native';

const FeedStack = createNativeStackNavigator()
const ProfileStack = createNativeStackNavigator()

const Lists = ({ navigation }) => {
  return (
    <View>
      <Text>This is Lists View</Text>
      <Button 
      	title="go to Detail" 
        onPress={() => navigation.navigate('Detail')}
      />
    </View>
  )
}

const Detail = () => {
  return (
    <View>
      <Text>This is Detail View</Text>
    </View>
  )
}

const Edit = ({ navigation }) => {
  return (
    <View>
      <Text>This is Edit View</Text>
      <Button 
      	title="go to MyContents" 
        onPress={() => navigation.navigate('MyContents')}
      />
    </View>
  )
}

const MyContents = () => {
  return (
    <View>
      <Text>This is MyContents View</Text>
    </View>
  )
}

const Feed = () => {
  return (
    <FeedStack.Navigator>
      <FeedStack.Screen name="Lists" component={Lists} />
      <FeedStack.Screen name="Detail" componenet={Detail} />  
    </FeedStack.Navigator>
  )
}


const Profile = () => {
  return (
    <ProfileStack.Navigator>
      <ProfileStack.Screen name="Edit" component={Edit} />
      <ProfileStack.Screen name="MyContents" component={MyContents} />
    </ProfileStack.Navigator>
  )
}

위에서 보면 알 수 있듯이 Feed와 Profile로 이루어지는 간단한 Stack Navigation을 설계했어요.

  • Feed (Lists, Detail)
  • Profile (Edit, MyContents)

이제 Tab navigation을 통해 두개를 합쳐볼게요.

Tab navigation

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { View, Text, Button } from 'react-native';

const FeedStack = createNativeStackNavigator()
const ProfileStack = createNativeStackNavigator()

const Lists = ({ navigation }) => {
  return (
    <View>
      <Text>This is Lists View</Text>
      <Button 
      	title="go to Detail" 
        onPress={() => navigation.navigate('Detail')}
      />
    </View>
  )
}

const Detail = () => {
  return (
    <View>
      <Text>This is Detail View</Text>
    </View>
  )
}

const Edit = ({ navigation }) => {
  return (
    <View>
      <Text>This is Edit View</Text>
      <Button 
      	title="go to MyContents" 
        onPress={() => navigation.navigate('MyContents')}
      />
    </View>
  )
}

const MyContents = () => {
  return (
    <View>
      <Text>This is MyContents View</Text>
    </View>
  )
}

const Feed = () => {
  return (
    <FeedStack.Navigator
      screenOptions={{
        headerShown: false
      }}
    >
      <FeedStack.Screen name="Lists" component={Lists} />
      <FeedStack.Screen name="Detail" component={Detail} />  
    </FeedStack.Navigator>
  )
}


const Profile = () => {
  return (
    <ProfileStack.Navigator
      screenOptions={{
        headerShown: false
      }}
    >
      <ProfileStack.Screen name="Edit" component={Edit} />
      <ProfileStack.Screen name="MyContents" component={MyContents} />
    </ProfileStack.Navigator>
  )
}

const Post = () => null

const Tab = createBottomTabNavigator()

const App = () => {

  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen name="Feed" component={Feed} />
        <Tab.Screen name="Post" component={Post}/>
        <Tab.Screen name="Profile" component={Profile}/>
      </Tab.Navigator>
    </NavigationContainer>
  );
};

export default App;

이런식으로 stack과 tab이 혼용된 네비게이션을 구성할 수 있어요.

Options을 따로 주지 않는다면, stack 네비게이션의 경우 tab name과 stack name 2개의 헤더가 노출이 된답니다.

이를 방지하기 위해 Feed와 Profile navigator에 screenOptions
"headerShown: false" 확인해주세요!

Bottom tab Modal

자 궁극적으로 하고 싶었던 하단바를 클릭해서 모달창을 띄우는 것을 해보도록 할게요.

문제는, 현재 보고 있는 화면에서 바로 모달창을 띄어야 한다는 것!

그래서 검색을 많이 했지만, 마땅한 해결법이 보이지 않았는데 알고보니
공식 문서에 적혀있더라구요. 하하,,

우선, 하단바를 클릭했을 때 화면이 이동하지 않게 방지해야해요.

<NavigationContainer>
  <Tab.Navigator>
    <Tab.Screen name="Feed" component={Feed} />
    <Tab.Screen 
      name="Post" 
      component={Post}
      listeners={({ navigation }) => ({
        tabPress: (e) => {
          e.preventDefault();
        }
      })}
    />
    <Tab.Screen name="Profile" component={Profile}/>
  </Tab.Navigator>
</NavigationContainer>

이렇게 TabScreen에 listeners options을 추가적으로 주어야 해요.
listeners? 듣겠다는거에요. 무엇을? 반응을!!

tabPress는 말그대로 탭을 눌렀을 때, 반응하는 콜백함수를 작성해주면 된답니다.
지금의 인자로 넘겨주는 e는 event의 약자로, e.preventDefault()는 이벤트를 실행시키지 않는다는 이야기에요.

즉, 우리는 탭을 누르면 이벤트가 발생하니 이를 방지하겠다!

기존의 탭 키를 누르면 그 화면(Posts)으로 이동하게 되는데, 탭 키를 누른건 인지하지만 이벤트를 막았으니 화면 전환을 시키지는 않겠다! 이런 이야기에요.

이를 통해 현재 화면이 고정된 채로, 모달창을 띄울 수 있게 됩니다.

그럼 이제 추가적으로 Modal 컴포넌트로 navigate 시켜주어야 합니다!
왜냐? 우리는 화면을 고정 시켰으니 우리의 목적인 Modal창을 띄어야 하기에,,

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

const CreatePosts = ({ navigation }) => {
  return (
      <View style={{ flex: 1 }}>
        <Pressable
          style={[
            StyleSheet.absoluteFill,
            { backgroundColor: 'rgba(0, 0, 0, 0.3)' },
          ]}
          onPress={navigation.goBack}
        />
        <View style={{width: '100%', height: '30%', position: 'absolute', bottom: 0, backgroundColor: 'white'}}>
          <Text style={{textAlign: 'center'}}>Create Posts !! This is Modal</Text>
        </View>
      </View>
  )
}

Modal Components 구성은 각자 Custom해서 하면 되겠지만 유튜브처럼 배경이 어둡게 깔리고 하단에 올라오는 형태로 하기 위해 이런식으로 만들었어요.

Pressable을 보면 아시겠지만, 배경을 눌렀을 때 뒤로갈 수 있도록 navigation.goBack을 넣어줬답니다.

그렇지 않으면 각자 상황에 맞게 커스텀하면 될 것 같아요!!

자, 이제 우리는 navigation을 재구성해야해요

const Feed = () => {
  return (
    <FeedStack.Navigator
      screenOptions={{
        headerShown: false
      }}
    >
      <FeedStack.Screen name="Lists" component={Lists} />
      <FeedStack.Screen name="Detail" component={Detail} />  
      <FeedStack.Screen name="CreatePosts0" component={CreatePosts}
        options={{
          presentation: "transparentModal",
      }}/>
    </FeedStack.Navigator>
  )
}

const Profile = () => {
  return (
    <ProfileStack.Navigator
      screenOptions={{
        headerShown: false
      }}
    >
      <ProfileStack.Screen name="Edit" component={Edit} />
      <ProfileStack.Screen name="MyContents" component={MyContents} />
      <ProfileStack.Screen name="CreatePosts2" component={CreatePosts}
        options={{
          presentation: "transparentModal",
      }}/>
    </ProfileStack.Navigator>
  )
}

const App = () => {
  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen name="Feed" component={Feed} />
        <Tab.Screen 
          name="Post" 
          component={Post}
          listeners={({ navigation }) => ({
            tabPress: (e) => {
                e.preventDefault();
                navigation.navigate(`CreatePosts${navigation.getState().index}`)
            }
          })}
        />
        <Tab.Screen name="Profile" component={Profile}/>
      </Tab.Navigator>
    </NavigationContainer>
  );
};

다른점이 보이시나요?

우선 탭을 눌렀을 때, navigate가 되게끔 설정했어요. 근데 이동하게 되는 name이 이상하죠?

근본적인 이유를 먼저 이야기 하자면 탭을 구분하기 위함이에요!

그렇기에 우선 navigation.getState().index을 이해해야할 필요가 있어요.

navigate.getState()를 콘솔로 찍어보면 다양한 값들이 찍히지만,
index 프로퍼티를 살펴보면 각 탭에 해당하는 값을 가지고 있다는 것을 알 수 있어요. 하단바 순서대로 0~n-1으로 찍힌답니다.

우리의 예제의 경우, Feed(0), Post(1), Profile(2) 이렇게 되는거에요! 이해 되시나요?

그렇다면 왜 탭을 분류해야 할까요?
그냥 CreatePosts로 통일하면 되는거 아니야?! 라고 생각할 수 있어요.
저도 여기서 한참 애를 먹었답니다,,

기본적으로, 현재 화면에서 네비게이션이 되기 때문에 각 Stack에 CreatePosts를 추가해야해요.
Feed와 Profile을 보면 알겠지만 CreatePosts0, CreatePosts2라는 name을 갖는 Stack이 새로 생겼어요.

왜 CreatePosts로 통일을 안시켰냐면,, 이유는 잘 모르겠지만(ㅎㅎ) 가장 나중에 눌러진(?) 스택의 화면으로 돌아가게 되더라구요.

예를 들어 profile 화면을 한번이라도 클릭했다면, 메인에서 Post tab을 눌러도 화면이 profile에 있는 CreatePosts 모달이 불러지기에 화면이동이 이루어지더라구요.

그렇기에 이를 방지하기 위해서, 네이밍을 다르게 해서 현재 Stack의 CreatePosts로 가게끔 설계를 했답니다.

또한 아래쪽에서 모달창이 올라오게끔 하기 위해 옵션을 주었어요.

options={{
  presentation: "transparentModal",
}}

react-navigation v6에 추가된 옵션인데 이를 통해 가능해집니다!

전체 코드

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { View, Text, Button, Pressable, StyleSheet } from 'react-native';

const FeedStack = createNativeStackNavigator()
const ProfileStack = createNativeStackNavigator()

const Lists = ({ navigation }) => {
  return (
    <View>
      <Text>This is Lists View</Text>
      <Button title="go to Detail" onPress={() => navigation.navigate('Detail')}/>
    </View>
  )
}

const Detail = () => {
  return (
    <View>
      <Text>This is Detail View</Text>
    </View>
  )
}

const Edit = ({ navigation }) => {
  return (
    <View>
      <Text>This is Edit View</Text>
      <Button title="go to MyContents" onPress={() => navigation.navigate('MyContents')}/>
    </View>
  )
}

const MyContents = () => {
  return (
    <View>
      <Text>This is MyContents View</Text>
    </View>
  )
}

const Feed = () => {
  return (
    <FeedStack.Navigator
      screenOptions={{
        headerShown: false
      }}
    >
      <FeedStack.Screen name="Lists" component={Lists} />
      <FeedStack.Screen name="Detail" component={Detail} />  
      <FeedStack.Screen name="CreatePosts0" component={CreatePosts}
        options={{
          presentation: "transparentModal",
      }}/>
    </FeedStack.Navigator>
  )
}

const Profile = () => {
  return (
    <ProfileStack.Navigator
      screenOptions={{
        headerShown: false
      }}
    >
      <ProfileStack.Screen name="Edit" component={Edit} />
      <ProfileStack.Screen name="MyContents" component={MyContents} />
      <ProfileStack.Screen name="CreatePosts2" component={CreatePosts}
        options={{
          presentation: "transparentModal",
      }}/>
    </ProfileStack.Navigator>
  )
}

const Post = () => null
const CreatePosts = ({ navigation }) => {
  return (
      <View style={{ flex: 1 }}>
        <Pressable
          style={[
            StyleSheet.absoluteFill,
            { backgroundColor: 'rgba(0, 0, 0, 0.3)' },
          ]}
          onPress={navigation.goBack}
        />
        <View style={{width: '100%', height: '30%', position: 'absolute', bottom: 0, backgroundColor: 'white'}}>
          <Text style={{textAlign: 'center'}}>Create Posts !! This is Modal</Text>
        </View>
      </View>
  )
}
const Tab = createBottomTabNavigator()

const App = () => {

  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen name="Feed" component={Feed} />
        <Tab.Screen 
          name="Post" 
          component={Post}
          listeners={({ navigation }) => ({
            tabPress: (e) => {
                e.preventDefault();
                navigation.navigate(`CreatePosts${navigation.getState().index}`)
            }
          })}
        />
        <Tab.Screen name="Profile" component={Profile}/>
      </Tab.Navigator>
    </NavigationContainer>
  );
};

export default App;

제 생각이 많이 담겨,, 틀렸을 수도 있지만 도움이 되셨으면..

번외 (+++추가)

작업을 하면서 알게 된 것인데, 모달을 띄어서 새로운 스크린으로 이동하는 경우에 navigation.goBack() 을 통해 이전에 열었던 모달 스크린을 뒤로 백 시켜줘야 새롭게 이동한 스크린에서 작업이 가능합니다. 그렇지 않으면 화면에서 아무것도 할 수 없는 불상사가 생겨요,,, 화이팅!!

참고
https://reactnavigation.org/docs/screen-options-resolution
https://reactnavigation.org/docs/stack-navigator/#transparent-modals
https://reactnavigation.org/docs/navigation-events/

profile
기록용

1개의 댓글

comment-user-thumbnail
2022년 11월 27일

제가 딱 원하던 기능입니다. 많은 도움 되었습니다 감사합니다!!

답글 달기