앱에서 사용자가 여러 화면(페이지) 사이를 이동하고 탐색할 수 있게 해주는 시스템을 의미한다.
웹에서는 <a> 태그나 URL 변경으로 페이지 이동을 하지만, 모바일 앱은 화면이 하나씩 쌓이거나 탭으로 전환되는 방식이 일반적이기 때문에 네비게이션이 훨씬 더 중요하고 복잡하다.
이때 RN에서 표준적으로 사용되는 라이브러리가 바로 @react-navigation/native 이다.
@react-navigation/native-stack을 통해 iOS의 UINavigationController와 Android의 Fragment를 직접 제어한다. 이를 위해 react-native-screens 라이브러리를 의존성으로 사용한다.react-native-screens 라이브러리를 내부에서 사용하여 OS의 네이티브 네비게이션 primitive를 사용한다.react-native-reanimated와 react-native-gesture-handler를 사용하여 제스처 처리를 최적화한다.useNavigationnavigation 객체에 접근한다.navigate('RouteName', params) getId가 지정된 경우 같은 ID의 화면이 스택에 있으면 그 화면으로 이동하며 params를 갱신.push('RouteName', params) : Stack 전용. 동일 화면이라도 무조건 새로 쌓는다.goBack() : 현재 화면을 닫고 이전 화면으로 돌아간다.pop(count?) : Stack 전용. count만큼 뒤로 간다.popTo('RouteName', params?) : Stack 전용. 스택에 있는 특정 화면까지 한 번에 돌아간다.popToTop() : Stack 전용. 스택의 첫 화면까지 돌아간다.replace('RouteName', params?) : 현재 화면을 새 화면으로 교체. 로그인 → 메인 전환에 자주 사용.reset({ index, routes }) : 네비게이션 히스토리를 통째로 새 상태로 교체.setParams(params) : 현재 화면의 params를 부분 갱신(shallow merge).useRouteroute 객체에 접근한다.route.params : 이전 화면에서 전달받은 매개변수 객체route.name : 현재 화면의 라우트 이름route.key : 화면의 고유 식별자(...전략...)
💡
headerShown: false를 탭에서 끄는 이유는 "탭이라서 헤더가 없어야" 한다기보다,
보통 탭이 RootStack 안에 중첩되어 있어 헤더는 RootStack이 그리고, 안쪽 탭의 헤더는 꺼서 헤더가 두 번 쌓이지 않게 하는 패턴이기 때문이다.
탭 화면별로 다른 헤더가 필요하면 오히려 켜두기도 한다.
React Native에서 하단 탭 메뉴(Bottom Tab Navigation)를 구현하려면 @react-navigation/native 코어 패키지 외에 @react-navigation/bottom-tabs라는 별도의 패키지가 필요하다.
최신 v7에서는 createBottomTabNavigator에 객체 설정값(Configuration Object)을 넘겨서 정의한다.
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createStaticNavigation } from '@react-navigation/native';
import { View, Text } from 'react-native';
// 아이콘 라이브러리 (예: react-native-vector-icons 혹은 lucide-react-native)
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
// 1. 탭에 들어갈 화면 컴포넌트들
const HomeScreen = () => <View><Text>홈 화면</Text></View>;
const SettingsScreen = () => <View><Text>설정 화면</Text></View>;
// 2. 바텀 탭 네비게이터 정의 (객체 방식)
const MyBottomTabs = createBottomTabNavigator({
screenOptions: {
// 탭 바 활성/비활성 색상
tabBarActiveTintColor: '#e91e63', // 선택된 탭 색상
tabBarInactiveTintColor: 'gray', // 선택 안 된 탭 색상
headerShown: false, // 상단 헤더 숨김 (보통 탭에선 숨김)
tabBarStyle: { height: 60, paddingBottom: 5 }, // 탭 바 높이 및 스타일
},
screens: {
Home: {
screen: HomeScreen,
options: {
title: '홈', // 탭 아래 텍스트
// 탭 아이콘 렌더링 함수 (focused 여부에 따라 아이콘 변경 가능)
tabBarIcon: ({ color, size, focused }) => (
<Icon name={focused ? "home" : "home-outline"} color={color} size={size} />
),
},
},
Settings: {
screen: SettingsScreen,
options: {
title: '설정',
tabBarIcon: ({ color, size, focused }) => (
<Icon name={focused ? "cog" : "cog-outline"} color={color} size={size} />
),
tabBarBadge: 3, // 알림 뱃지 (숫자 3 표시)
},
},
},
});
// 3. 네비게이션 생성
const Navigation = createStaticNavigation(MyBottomTabs);
export default function App() {
return <Navigation />;
}
보통 앱은 "로그인(Stack) -> 메인 탭(Tab) -> 상세 페이지(Stack)" 구조로 되어 있다.
따라서 탭 네비게이터를 단독으로 쓰기보다는, 루트 스택(Root Stack)의 한 화면으로 탭 네비게이터를 등록해서 호출한다.
// RootStack (전체 앱 구조)
const RootStack = createNativeStackNavigator({
screens: {
// 1. 로그인 화면 (스택)
Login: LoginScreen,
// 2. 메인 탭 화면 (여기에 위에서 만든 MyBottomTabs를 통째로 넣음)
MainTab: {
screen: MyBottomTabs, // 아까 만든 탭 네비게이터
options: { headerShown: false }, // 탭 자체의 헤더도 숨김
},
// 3. 탭 밖의 상세 페이지 (탭 바를 가리면서 열림)
Detail: DetailScreen,
},
});
Login에서 로그인이 완료되면 navigation.replace('MainTab')을 호출한 후 Bottom Tab이 있는 메인 홈 화면으로 전환된다.
탭 내부에서 아이템을 클릭하면 navigation.navigate('Detail')을 호출한다.
Detail 화면은 탭 네비게이터보다 상위 스택에 있으므로, 탭 바 위로 덮어씌워지며 열린다. (일반적인 앱 UX)
headerShown: false를 탭에서 끄는 이유는 "탭이라서 헤더가 없어야" 한다기보다,
보통 탭이 RootStack 안에 중첩되어 있어 헤더는 RootStack이 그리고, 안쪽 탭의 헤더는 꺼서 헤더가 두 번 쌓이지 않게 하는 패턴이기 때문이다.
탭 화면별로 다른 헤더가 필요하면 오히려 켜두기도 한다.