내비게이션, Hooks

React Native에서 사용되는 내비게이션과 관련 Hooks에 대해 공부해 보자.

앞써 공부한 내용(설치 및 할일목록 만들기)를 토대로, 내비게이션(navigation)을 공부한다.

참고

  • IOS 애뮬레이터 리로딩: 커맨드 + R

  • Android 애뮬레이터 리로딩: R 두번

(키보드 입력이 한글인 상태에서는 해당 커맨드가 안먹는다.)

  • https://reactnavigation.org/docs/getting-started

설치

react-navigation 라이브러리를 이용한다. RN 커뮤니티에서 관리하며 사용률이 가장 높은 라이브러리다. 참고로 Wix에서 관리하는 react-native-navigation 라이브러리도 있다. 차이점은 react-native-navigation의 경우 네이티브로 구현되어 있기 때문에 더욱 네이티브스러운 사용 경험을 제공하지만, 자바스크립트로 구현된 react-navigation가 사용법도 더 쉽고 별도 API가 아닌 리액트 컴포넌트를 사용해 화면을 설정할 수 있다는 장점도 있다.

$ yarn add @react-navigation/native
# 아래 라이브러리도 의존하고 있는 라이브러리이기 때문에 필수로 같이 설치해 주자.
$ yarn add react-native-screens react-native-safe-area-context

다했다면 이젠 익숙한 Pod를 설치해 준다. (아래부턴 특별한 경우 빼곤 해당 내용 생략)

$ cd ios
$ pod install

적용

NavigationContainer 컴포넌트로 앱 전체를 감싸 주면 된다.

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

function App() {
  return <NavigationContainer>{/* 내비게이션 설정 */}</NavigationContainer>;
}

export default App;

Native Stack Navigator

react-navigation 라이브러리에는 다른 상황에서 사용할 수 있는 다양한 네비게이터가 있지만, RN 앱에서는 화면 전환할 때 브라우저의 History와 비슷한 사용성을 제공하기 위해 네이티브 스택 내비게이터를 사용한다.

Native Stack Navigator의 경우 가장 많이 사용되며, 안드로이드에서는 Fragment, IOS에서는 UINavigationController를 사용해 일반 네이티브 앱과 정확히 동일한 방식으로 화면을 관리한다.

$ yarn add @react-navigation/native-stack

기본 사용법

화면 컴포넌트를 screens라는 폴더를 생성하고 그 안에 HomeScreenDetailScreen을 생성해 보자.

// screens/HomeScreen.js
import React from 'react';
import {Button, View} from 'react-native';

function HomeScreen() {
  return (
    <View>
      <Button title="Detail 열기" />
    </View>
  );
}

export default HomeScreen;


// screens/HomeScreen.js
import React from 'react';
import {Text, View} from 'react-native';

function DetailScreen() {
  return (
    <View>
      <Text>Detail</Text>
    </View>
  );
}

export default DetailScreen;

그 다음 App.js를 수정하자.

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Detail" component={DetailScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

여기서 name은 화면의 이름을 설정하는 props로 다른 화면으로 이동하거나 현재 화면이 어떤 화면인지 조회할때 쓰인다. 반드시 대문자로 작성하지 않아도 되지만, 공식 문서에서 대문자로 시작하는 것을 권장하고 있다.

스크린 이동하기

스크린으로 사용된 컴포넌트는 navigation이라는 객체를 Props로 받아올 수 있다. 그리고 이 객체를 아래와 같이 이용하여 화면을 이동할 수 있다.

function HomeScreen({navigation}) {
  return (
    <View>
      <Button
        title="Detail 열기"
        onPress={() => navigation.navigate('Detail')}
        // navigate 함수가 아닌 push를 이용하여 이동도 가능하다.
        // onPress={() => navigation.push('Detail')}
      />
    </View>
  );
}

export default HomeScreen;

실제로 테스트 해보면 우리가 자주 접하던 스크린 효과를 통해 화면이 이동하는걸 볼 수 있다. 참고로 UI와 스크린 전환 효과는 모두 커스터마이징할 수 있으니 일단 넘어가자.

라우트 파라미터

새로운 화면을 보여줄 때 의존해야 하는 어떤 값이 있다면 객체 타입으로 라우트 파라미터를 설정한다.

// example
navigation.navigate('Detail', {id: 1});
// or
navigation.push('Detail', {id: 1});

스크린으로 사용된 컴포넌트는 route라는 Props도 받아온다. 그리고 내가 넘겨준 라우트 파라미터를 route라는 객체 안에 params에 저장되어 확인할 수 있게 된다. 아래 코드를 통해 확인해 보자.

// HomeScreen.js
function HomeScreen({navigation}) {
  return (
    <View>
      <Button
        title="Detail 1 열기"
        onPress={() => navigation.push('Detail', {id: 1})}
      />
      <Button
        title="Detail 2 열기"
        onPress={() => navigation.push('Detail', {id: 2})}
      />
      <Button
        title="Detail 3 열기"
        onPress={() => navigation.push('Detail', {id: 3})}
      />
    </View>
  );
}

// DetailScreen.js
function DetailScreen({route}) {
  return (
    <View style={styles.block}>
      <Text style={styles.text}>id: {route.params.id}</Text>
    </View>
  );
}

참고

스크린으로 사용되면 어떤 값들을 Props로 가져오는 걸까? 궁금해서 확인해 봤다.

{
  "navigation": {
    "addListener": ["Function addListener"],
    "canGoBack": ["Function canGoBack"],
    "dispatch": ["Function dispatch"],
    "getParent": ["Function getParent"],
    "getState": ["Function anonymous"],
    "goBack": ["Function anonymous"],
    "isFocused": ["Function isFocused"],
    "navigate": ["Function anonymous"], // 익숙!
    "pop": ["Function anonymous"],
    "popToTop": ["Function anonymous"],
    "push": ["Function anonymous"], // 익숙!
    "removeListener": ["Function removeListener"],
    "replace": ["Function anonymous"],
    "reset": ["Function anonymous"],
    "setOptions": ["Function setOptions"],
    "setParams": ["Function anonymous"]
  },
  "route": {
    "key": "Detail-2b1gG14xTSuAmKqedHcYm", // 화면 고유의 ID로 새로운 화면이 나타날 때 자동으로 생성된다.
    "name": "Detail", // 내가 스택 내비게이터를 설정할 때 지정한 name이다.
    "params": {
      // 라우트 파라미터이다.
      "id": 1
    }
  }
}

위에서 화면 전환할 때 사용한 두 함수이다.

  • navigate()의 경우 새로 이동할 화면이 현재 화면과 같으면 새로운 화면을 쌓지 않고 파라미터만 변경한다. 따라서 화면 전환효과도 없고 뒤로 가기를 눌렀을 때 스택으로 안쌓고 있기 때문에 처음 진입 화면으로 돌아갈 것이다.

  • push()navigate()와 반대이다. 아래 그림으로 보는게 훨씬 이해가 빠르다.

// DetailScreen.js
// 아래와 같은 방식으로 다음버튼을 구현하며, push 함수만 변경하면서 테스트 하면 된다.
<Button
  title="다음 (push방식)"
  onPress={() => navigation.push('Detail', {id: route.params.id + 1})}
/>

push 방식

뒤로 가기 구현

뒤로 가기 기능의 경우 navigation 객체의 pop함수와 popToTop함수를 통해 구현이 가능하다.

  • pop(): 뒤로 가기(이전 화면으로 이동)

  • popToTop(): 뒤로 가기(가장 첫 번째 화면으로 이동)

<Button
  title="다음"
  onPress={() => navigation.push('Detail', {id: route.params.id + 1})}
/>
<Button title="뒤로가기" onPress={() => navigation.pop()} />
<Button title="처음으로" onPress={() => navigation.popToTop()} />

헤더 커스터마이징

react-navigation에서는 타이틀 영역을 헤더(Header)라고 부른다.

타이틀 변경하기

Home이라고 표시된 텍스트를 변경해보자.

헤더를 커스터마이징하는 방법은 두 가지가 있다.

  • 첫 번째 방법은 Stack.ScreenProps로 설정하는 것이다.

  • 두 번째 방법은 화면 컴포넌트에서 navigation.setOptions 함수를 사용하는 것이다.

첫 번째 방법으로 Home 텍스트 변경하기

Stack.ScreenProps로 설정하는 것이다.

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{title: '홈'}} // <--
        />
        <Stack.Screen name="Detail" component={DetailScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

두 번째 방법으로 Home 텍스트 변경하기

화면 컴포넌트에서 navigation.setOptions 함수를 사용하는 것이다.

function HomeScreen({navigation}) {

  useEffect(() => {
    navigation.setOptions({title: '홈 화면'});
  // cf) navigation 객체가 바뀔 일은 없지만, ESLint 규칙상 내부에서 사용하는 값을 넣어야 하기 때문에 추가
  }, [navigation]);

  return (...)

참고로 useEffect를 통해서 설정한 내비게이션 optionApp 컴포넌트에서 Props를 통해 설정한 option을 덮어쓰게 된다.


첫 번째 방법으로 Detail 텍스트 변경하기

Stack.ScreenProps로 설정하는 것이다.

<Stack.Screen
  name="Detail"
  component={DetailScreen}
  options={({route}) => ({
    title: `상세 정보 - ${route.params.id}`,
  })}
/>

두 번째 방법으로 Detail 텍스트 변경하기

화면 컴포넌트에서 navigation.setOptions 함수를 사용하는 것이다.

헤더 스타일 변경하기

아래 예시 코드는 홈화면의 헤드 스타일을 수정하는 코드이다. 예시코드보다 훨씬 많은 커스터마이징 요소를 제공하니 나중에 문서를 참고하자.

<Stack.Screen
  name="Home"
  component={HomeScreen}
  options={{
    title: '홈',
    // Header 블록에 대한 스타일
    headerStyle: {
      backgroundColor: '#29b6f6',
    },
    // Header의 텍스트, 버튼들 색상
    headerTintColor: '#fff',
    // 타이틀 텍스트 스타일
    headerTitleStyle: {
      fontWeight: 'bold',
      fontSize: 20,
    },
  }}
/>

헤더의 좌측, 타이틀, 우측 영역에 다른 컴포넌트 보여주기

예시 코드의 상세 화면을 변경해 보았다.

// App.js
const headerLeft = ({onPress}) => (
  <TouchableOpacity onPress={onPress}>
    <Text>Left</Text>
  </TouchableOpacity>
);

const headerTitle = ({children}) => (
  <View>
    <Text>{children}</Text>
  </View>
);

const headerRight = () => (
  <View>
    <Text>Right</Text>
  </View>
);

// ...

<Stack.Screen
  name="Detail"
  component={DetailScreen}
  options={({route}) => ({
    title: `상세 정보 - ${route.params.id}`,
    headerLeft,
    headerTitle,
    headerRight,
  })}
/>;

ios와 다르게 안드로이드에서는 <- 좌측 화살표 표시가 나타나고 있다. 이를 없애고 싶다면 headerBackVisible 옵션을 false로 지정하자.

<Stack.Screen
  name="Detail"
  component={DetailScreen}
  options={({route}) => ({
    title: `상세 정보 - ${route.params.id}`,
    headerBackVisible: false,
    headerLeft,
    headerTitle,
    headerRight,
  })}
/>

헤더 숨기기

헤더가 없는 화면이 필요할 때 사용한다. 실제로 이전 회사에서 특정화면에서 헤더를 없애달라는 요구사항이 있어 그때는 네이티브 개발자와 URL Scheme을 사용하여 해결한 경험이 있긴하다. RN에서 직접 없애보자.

먼저 헤더가 없는 스크린 컴포넌트를 추가한다.

// screens/HeaderlessScreen.js
function HeaderlessScreen({navigation}) {
  return (
    <View>
      <Text>Header 없는 화면</Text>
      <Button onPress={() => navigation.pop()} title="뒤로가기" />
    </View>
  );
}

export default HeaderlessScreen;

스택 네비게이션의 헤더리스 스크린을 등록하고, 홈화면에서 이동 버튼을 생성후 이동해 보면, 안드로이드에서는 화면이 잘 나오지만, ios에서는 StatusBar 영역을 침범해서 화면에 보여진다.

이럴때는 이전에서 배웠던 SafeAreaView 컴포넌트를 이용해 해결하면 된다. 참고로 react-navigationreact-native-safe-area-context가 내장되어 있기 때문에 react-native가 아닌 react-native-safe-area-context에서 불러와도 상관없다.

import React from 'react';
import {Button, Text, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';

function HeaderlessScreen({navigation}) {
  return (
    <SafeAreaView>
      <View>
        <Text>Header 없는 화면</Text>
        <Button onPress={() => navigation.pop()} title="뒤로가기" />
      </View>
    </SafeAreaView>
  );
}

export default HeaderlessScreen;

만약 전체 화면에서 헤더를 없애고 싶다면?

Stack.NavigatorscreenOptionsheaderShown: false을 지정해 주면 된다.

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="Home"
        screenOptions={{headerShown: false}}>
        // ...
      </Stack.Navigator>
    </NavigationContainer>
  );
}

드로어 내비게이터

사이드바를 모바일 앱에서는 드로어라고 부르며, 드로어 내비게이터(Drawer Navigator)는 좌측 또는 우측에 사이드바를 만들고 싶을 때 사용하는 내비게이터이다.

설치

3가지 라이브러리를 설치해 주자.

$ yarn add @react-navigation/drawer react-native-gesture-handler react-native-reanimated
  • react-native-gesture-handler는 드로어 내비게이터에서 사용자 제스처를 인식하기 위해 내부적으로 사용하는 라이브러리

  • react-native-reanimated는 내장된 애니메이션 효과보다 더 개선된 성능으로 애니메이션 효과를 구현해 주는 라이브러리

이 글 위에서 만들었던 내비게이터 기능의 상당 부분이 드로어 내비게이터에서는 동작하지 않으므로 다시 코드를 작성해 보자.

기본 사용법

스택 내비게이터와 유사한 사용 방식을 가진다. 드로어 내비게이터를 설정한 방향에서 반대 방향으로 스와이프 해도 드로어 내비게이터가 나온다.

const Drawer = createDrawerNavigator();

function App() {
  return (
    <NavigationContainer>
      <Drawer.Navigator
        initialRouteName="Home"
        // 2021년 12월 2일 기준 최신 버전 6.1.8 기준
        screenOptions={{drawerPosition: 'right'}} // default value: 'left'
        backBehavior="history">
        <Drawer.Screen name="Home" component={HomeScreen} />
        <Drawer.Screen name="Setting" component={SettingScreen} />
      </Drawer.Navigator>
    </NavigationContainer>
  );
}

드로어 내비게이터의 위치를 과거 버전(정확한 것은 자신이 사용하는 버전을 확인하자. 나의 경우 6.1.8이다.)에서는 아래와 같이 변경해야 한다. 내가 설치한 버전의 경우 위와 같이 적용해 주면 된다. (최신 버전으로 넘어오면서 drawerPosition Propsdeprecated 되었다.)

<NavigationContainer>
  <Drawer.Navigator
    initialRouteName="Home"
    // 과거 버전에서는...
    drawerPosition="right" // default value: 'left'
    backBehavior="history">
    // ...
  </Drawer.Navigator>
</NavigationContainer>

참고로 drawerPosition을 변경해준 경우 앱을 다시 실행시켜야 제대로 반영된다.

backBehavior의 경우 아래와 같은 값을 지정할 수 있다.

  • initialRoute: 가장 첫 번째 화면을 보여준다.

  • order: Drawer.Screen 컴포넌트를 사용한 순서에 따라 현재 화면의 이전 화면을 보여준다.

  • history: 현재 화면을 열기 직전에 봤던 화면을 보여준다.

  • none: 뒤로가기 기능을 막는다.

  • firstRoute: 제일 먼저 사용된 Drawer.Screen 컴포넌트를 보여준다.

드로어 커스터마이징

  • Screen 컴포넌트에 options Props를 통해 수정한다.

  • Drawer.Navigator 컴포넌트에 screenOptions Props를 설정한다.

// App.js 간단한 수정 예
function App() {
  return (
    <NavigationContainer>
      <Drawer.Navigator
        initialRouteName="Home"
        backBehavior="history"
        screenOptions={{
          drawerActiveBackgroundColor: '#fb8c00',
          drawerActiveTintColor: '#fff',
        }}>
        <Drawer.Screen
          name="Home"
          component={HomeScreen}
          options={{title: '홈'}}
        />
        <Drawer.Screen
          name="Setting"
          component={SettingScreen}
          options={{title: '설정'}}
        />
      </Drawer.Navigator>
    </NavigationContainer>
  );
}

drawerContent

drawerContent의 경우 드로어 영역에 아예 다른 컴포넌트를 보여주고 싶을 때 사용할 수 있으며, 함수 컴포넌트를 넣어주면 된다.

만약 ios도 지원하는 앱이라면 drawerContent를 지정할 때 SafeAreaView도 꼭 사용해야 StatusBar영역과 상단 영역이 겹치는 현상을 방지할 수 있다.

function App() {
  return (
    <NavigationContainer>
      <Drawer.Navigator
        // ...
        drawerContent={({navigation}) => (
          <SafeAreaView>
            <Text>커스텀 드로어 content</Text>
            <Button title="닫기" onPress={() => navigation.closeDrawer()} />
          </SafeAreaView>
        )}>
        // ...
      </Drawer.Navigator>
    </NavigationContainer>
  );
}

하단 탭 내비게이터

페이스북의 하단 내비게이터 같은 앱 하단에 나오는 내비게이터를 말한다.

설치

아이콘도 추가할 예정이기 때문에 같이 설치해 준다.

$ yarn add @react-navigation/bottom-tabs react-native-vector-icons

기본 사용법

위에 내비게이터와 익숙한 방식으로 설정하면 된다. 화면만 등록해 줘도 아래와 같은 화면이 바로 생기는걸 볼 수 있다.

const Tab = createBottomTabNavigator();

function HomeScreen() {
  return <Text>Home</Text>;
}

function SearchScreen() {
  return <Text>Search</Text>;
}

function NotificationScreen() {
  return <Text>Notification</Text>;
}

function MessageScreen() {
  return <Text>Message</Text>;
}

function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator initialRouteName="Home">
        <Tab.Screen name="Home" component={HomeScreen} />
        <Tab.Screen name="Search" component={SearchScreen} />
        <Tab.Screen name="Notification" component={NotificationScreen} />
        <Tab.Screen name="Message" component={MessageScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

icon을 사용하기 위한 설정

처음에 해봤듯이 아이콘을 사용하기 위해서는 ios와 android에 각각의 설정이 필요하다.

ios

설정 후 앱 다시 실행

// ios/LearnReactNavigation/Info.plist
// ...
// <key>UIViewControllerBasedStatusBarAppearance</key>
//   <false/>
// 맨 아래에 추가
	<key>UIAppFonts</key>
	<array>
		<string>MaterialIcons.ttf</string>
	</array>
// </dict>
// </plist>

android

설정 후 앱 다시 실행

// android/app/build.gradle
// ...
// apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle");
// applyNativeModulesAppBuildGradle(project)
// 맨 아래에 추가
project.ext.vectoricons = [
    iconFontNames: ['MaterialIcons.ttf']
]

apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"

설정이 끝났다면 아래와 같이 아이콘을 추가해 주자.

<Tab.Screen
  name="Home"
  component={HomeScreen}
  options={{
    title: '홈',
    tabBarIcon: ({color, size}) => (
      <Icon name="home" color={color} size={size} />
    ),
  }}
/>
<Tab.Screen
  name="Search"
  component={SearchScreen}
  options={{
    title: '검색',
    tabBarIcon: ({color, size}) => (
      <Icon name="search" color={color} size={size} />
    ),
  }}
/>
<Tab.Screen
  name="Notification"
  component={NotificationScreen}
  options={{
    title: '알림',
    tabBarIcon: ({color, size}) => (
      <Icon name="notifications" color={color} size={size} />
    ),
  }}
/>
<Tab.Screen
  name="Message"
  component={MessageScreen}
  options={{
    title: '메시지',
    tabBarIcon: ({color, size}) => (
      <Icon name="message" color={color} size={size} />
    ),
  }}
/>

tabBarIcon에는 함수 컴포넌트를 받는데 size, color, focused를 Props로 받는다. 사용을 안할거라면 생략해도 되며, 생략해 보면서 어떤게 변하는지 테스트 해봐도 좋다.

하단 탭 커스텀마이징

  • 다른 내비게이터와 동일하게 screenOptions를 통해 커스텀 마이징할 수 있다. [공식문서]

  • 참고로 예전 버전에서는 탭에 관한 설정은 tapBarOptions를 통해 한다. 버전에 따라 제공되는 속성의 이름도 다를 수 있으니 이점을 꼭 확인하자.

<NavigationContainer>
  <Tab.Navigator
    initialRouteName="Home"
    screenOptions={{
      tabBarShowLabel: false,
      tabBarActiveTintColor: '#fb8c00',
    }}></Tab.Navigator>
</NavigationContainer>

스택 내비게이터와 하단 탭 내비게이터 같이 사용하기

commit log: bf6850534717a3be4e3013d52fd8a25248f54d9b

기본 사용법

// App.js
const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="Home"
        screenOptions={{
          tabBarShowLabel: false,
          tabBarActiveTintColor: '#fb8c00',
        }}>
        <Stack.Screen
          name="Main"
          component={MainScreen} // <-- MainScreen.js 참고
          // * 이 설정을 추가하지 않으면 헤더가 두개가 보이는 현상이 나타난다.
          // * 하단 탭 내비게이터를 스택 내비게이터 내부에서 사용하게 될 때 이 설정을 꼭 해주어야 한다.
          options={{headerShown: false}}
        />
        <Stack.Screen name="Detail" component={DetailScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

머티리얼 내비게이터

머티리얼 상단 탭과 하단 탭 내비게이터에 대해 알아보자. 머티리얼 탭의 특징은 구글 머티리얼 디자인 특유의 리플(ripple) 효과가 나타나며, 스와이프를 통한 이동을 지원한다. [공식 문서]

물론 안드로이드에서만 리플 효과가 나타나며, 안드로이드 5.0 이상에서만 나타난다.

머티리얼 상단 내비게이터 설치

설치 후 앱 재실행

$ yarn add @react-navigation/material-top-tabs react-native-tab-view react-native-pager-view

머티리얼 상단 내비게이터 기본 사용법

기존 코드의 MainScreen.js에서 Tab을 생성하는 함수와 size만 변경해 줬다(머티리얼 상단 탭 내비게이터의 경우 아이콘 사이즈를 직접 지정해줘야 한다.).

const Tab = createMaterialTopTabNavigator();
// ...
<Tab.Screen
  name="Home"
  component={HomeScreen}
  options={{
    title: '홈',
    tabBarIcon: ({color}) => <Icon name="home" color={color} size={24} />,
  }}
/>;
// ...

그리고 기존에 다뤘던 내비게이터와 달리 상단 헤더를 보여주지 않기 때문에 App.js에서 options={{headerShown: false}}로 설정했던 부분을 지워주자.

// ...
<Stack.Screen name="Main" component={MainScreen} />
// ...

머티리얼 상단 내비게이터 커스터마이징

커스터마이징 또한 다른 탭들과 동일한 방식으로 지원된다. 다만 머티리얼 탭에서만 추가된 속성들도 있으니 [공식 문서]를 참고하자.

commit log

fc1a7d5358a6d08ed770cf24ca6d2e92ddc2be0d


머티리얼 하단 내비게이터 설치

설치 후 앱 재실행

$ yarn add @react-navigation/material-bottom-tabs react-native-paper

머티리얼 하단 내비게이터 커스터마이징

활성화된 탭에 따라 탭의 배경색을 변경할 수 있다.

<Tab.Screen
  name="Home"
  component={HomeScreen}
  options={{
    title: '홈',
    tabBarIcon: ({color}) => <Icon name="home" color={color} size={24} />,
    tabBarColor: 'black', // <--
  }}
/>

이외에도 많은 옵션이 있으며, [공식문서]를 참고하자.

머티리얼 상단 타이틀과 하단 탭 동기화 시키기

위에서 만든 탭을 자세히 보면 상단 타이틀과 하단 탭의 데이터가 동기화 되지 않는 것을 볼 수 있다.

import {
  NavigationContainer,
  getFocusedRouteNameFromRoute,
} from '@react-navigation/native';
// ...

function getHeaderTitle(route) {
  const routeName = getFocusedRouteNameFromRoute(route) ?? 'Home';
  return routeName;
}

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="Home"
        screenOptions={{
          tabBarShowLabel: false,
          tabBarActiveTintColor: '#fb8c00',
        }}>
        <Stack.Screen
          name="Main"
          component={MainScreen}
          options={({route}) => getHeaderTitle(route)} // <--
        />
        <Stack.Screen name="Detail" component={DetailScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

options Props에 일반 객체가 아닌 객체를 반환하는 함수를 넣었다. 이렇게 하면 내비게이션의 상태가 바뀔때마다 함수를 다시 실행하여 화면의 options 객체를 생성하게 된다.

여기서 사용한 getFocusedRouteNameFromRoute() 함수의 경우 route 객체를 통해 현재 포커스된 화면의 이름을 조회한다. 여기서 주의할 점은 "내비게이션의 상태가 바뀔때마다"이기 때문에 초기에는 undefined가 반환되므로 여기에 대한 기본 값을 미리 넣어주도록 만들어야 한다.


내비게이션 Hooks

리액트 내비게이션은 여러 hook들을 제공해 준다. 이런 훅들을 왜 제공받아야 하는지 궁금할 수 있지만, 우리가 만든 예제에서도 필요한 이유를 찾을 수 있다.

지금은 간단한 애플리케이션을 만들었기 때문에 화면만 존재 하기 때문에 별문제가 안생겼지만, Screen으로 사용되지 않는 컴포넌트에서 routenavigation을 사용할 수 없다. 물론 Props로 넘겨줘도 되겠지만, 필요한 컴포넌트의 뎁스(depth)가 싶을수록, 필요한 Props가 많을수록 복잡성을 야기하게 된다.

useNavigation

Screen으로 사용되고 있지 않는 컴포넌트에서도 navigation 객체를 사용할 수 있다.

// MainScreen.js
function OpenDetailButton() {
  const navigation = useNavigation();
  return (
    <Button
      title="Detail 1 열기"
      onPress={() => navigation.push('Detail', {id: 1})}
    />
  );
}

function HomeScreen() {
  return (
    <View>
      <Text>Home</Text>
      <OpenDetailButton />
    </View>
  );
}

useRoute

Screen으로 사용되고 있지 않는 컴포넌트에서도 route 객체를 사용할 수 있다.

function IDText() {
  const route = useRoute();
  return <Text>id: {route.params.id}</Text>;
}

function DetailScreen({navigation, route}) {
  useEffect(() => {
    navigation.setOptions({
      title: `상세 정보 -${route.params.id}`,
    });
  }, [navigation, route]);

  return (
    <View>
      <Text>상세 화면</Text>
      <IDText />
      <Button title="뒤로가기" onPress={() => navigation.pop()} />
    </View>
  );
}

useFocusEffect

useFocusEffect는 화면에 포커스가 잡혔을 때 특정 작업을 할 수 있게 하는 Hook이다. [공식문서]

이 앱에서는 화면이 사라지는게 아니라, 화면을 쌓으면서 보여주는 것이다. 예를들어 DetailScreen을 띄운다면 HomeScreen 위에 DetailScreen을 쌓아서 보여주는 것이다. 그래서 만약 useEffect를 통해서만 어떤 외부효과를 일으키는 것을 하고 싶다면 처음에는 동작하겠지만, 이전 화면으로 돌아왔을때는 실행되지 않는다.

그래서 다른 화면을 열었다가 돌아왔을 때 특정 작업을 하고 싶다면 useFocusEffect을 사용해야 한다. 또 현재 화면에서 다른 화면으로 넘어갈 때 특정 작업을 하고 싶다면 useFocusEffect에서 함수를 반환해 주면 된다.

다만 useFocusEffect을 사용할 때는 꼭 useCallback과 함께 사용해야 한다. 만약 useCallback을 사용하지 않으면 컴포넌트가 리렌더링될 때마다 useFocusEffect에 등록한 함수가 실행될 것이다.

Last updated