앞써 공부한 내용(설치 및 할일목록 만들기)를 토대로, 내비게이션(navigation)을 공부한다.
참고
IOS 애뮬레이터 리로딩: 커맨드 + R
Android 애뮬레이터 리로딩: R 두번
(키보드 입력이 한글인 상태에서는 해당 커맨드가 안먹는다.)
https://reactnavigation.org/docs/getting-started
설치
react-navigation 라이브러리를 이용한다. RN 커뮤니티에서 관리하며 사용률이 가장 높은 라이브러리다. 참고로 Wix에서 관리하는 react-native-navigation 라이브러리도 있다. 차이점은 react-native-navigation의 경우 네이티브로 구현되어 있기 때문에 더욱 네이티브스러운 사용 경험을 제공하지만, 자바스크립트로 구현된 react-navigation가 사용법도 더 쉽고 별도 API가 아닌 리액트 컴포넌트를 사용해 화면을 설정할 수 있다는 장점도 있다.
$yarnadd@react-navigation/native# 아래 라이브러리도 의존하고 있는 라이브러리이기 때문에 필수로 같이 설치해 주자.$yarnaddreact-native-screensreact-native-safe-area-context
다했다면 이젠 익숙한 Pod를 설치해 준다. (아래부턴 특별한 경우 빼곤 해당 내용 생략)
{"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 } }}
navigation.navigate() vs navigation.push()
위에서 화면 전환할 때 사용한 두 함수이다.
navigate()의 경우 새로 이동할 화면이 현재 화면과 같으면 새로운 화면을 쌓지 않고 파라미터만 변경한다. 따라서 화면 전환효과도 없고 뒤로 가기를 눌렀을 때 스택으로 안쌓고 있기 때문에 처음 진입 화면으로 돌아갈 것이다.
push()는 navigate()와 반대이다. 아래 그림으로 보는게 훨씬 이해가 빠르다.
// DetailScreen.js// 아래와 같은 방식으로 다음버튼을 구현하며, push 함수만 변경하면서 테스트 하면 된다.<Buttontitle="다음 (push방식)"onPress={() =>navigation.push('Detail', {id:route.params.id +1})}/>
push 방식
navigate 방식
뒤로 가기 구현
뒤로 가기 기능의 경우 navigation 객체의 pop함수와 popToTop함수를 통해 구현이 가능하다.
functionHomeScreen({navigation}) {useEffect(() => {navigation.setOptions({title:'홈 화면'});// cf) navigation 객체가 바뀔 일은 없지만, ESLint 규칙상 내부에서 사용하는 값을 넣어야 하기 때문에 추가 }, [navigation]);return (...)
참고로 useEffect를 통해서 설정한 내비게이션 option은 App 컴포넌트에서 Props를 통해 설정한 option을 덮어쓰게 된다.
첫 번째 방법으로 Detail 텍스트 변경하기
Stack.Screen의 Props로 설정하는 것이다.
<Stack.Screenname="Detail"component={DetailScreen}options={({route}) => ({ title:`상세 정보 - ${route.params.id}`, })}/>
두 번째 방법으로 Detail 텍스트 변경하기
화면 컴포넌트에서 navigation.setOptions 함수를 사용하는 것이다.
헤더 스타일 변경하기
아래 예시 코드는 홈화면의 헤드 스타일을 수정하는 코드이다. 예시코드보다 훨씬 많은 커스터마이징 요소를 제공하니 나중에 문서를 참고하자.
<Stack.Screenname="Home"component={HomeScreen}options={{ title:'홈',// Header 블록에 대한 스타일 headerStyle: { backgroundColor:'#29b6f6', },// Header의 텍스트, 버튼들 색상 headerTintColor:'#fff',// 타이틀 텍스트 스타일 headerTitleStyle: { fontWeight:'bold', fontSize:20, }, }}/>
스택 네비게이션의 헤더리스 스크린을 등록하고, 홈화면에서 이동 버튼을 생성후 이동해 보면, 안드로이드에서는 화면이 잘 나오지만, ios에서는 StatusBar 영역을 침범해서 화면에 보여진다.
이럴때는 이전에서 배웠던 SafeAreaView 컴포넌트를 이용해 해결하면 된다. 참고로 react-navigation에 react-native-safe-area-context가 내장되어 있기 때문에 react-native가 아닌 react-native-safe-area-context에서 불러와도 상관없다.
react-native-gesture-handler는 드로어 내비게이터에서 사용자 제스처를 인식하기 위해 내부적으로 사용하는 라이브러리
react-native-reanimated는 내장된 애니메이션 효과보다 더 개선된 성능으로 애니메이션 효과를 구현해 주는 라이브러리
이 글 위에서 만들었던 내비게이터 기능의 상당 부분이 드로어 내비게이터에서는 동작하지 않으므로 다시 코드를 작성해 보자.
기본 사용법
스택 내비게이터와 유사한 사용 방식을 가진다. 드로어 내비게이터를 설정한 방향에서 반대 방향으로 스와이프 해도 드로어 내비게이터가 나온다.
constDrawer=createDrawerNavigator();functionApp() {return ( <NavigationContainer> <Drawer.NavigatorinitialRouteName="Home"// 2021년 12월 2일 기준 최신 버전 6.1.8 기준screenOptions={{drawerPosition:'right'}} // default value: 'left'backBehavior="history"> <Drawer.Screenname="Home"component={HomeScreen} /> <Drawer.Screenname="Setting"component={SettingScreen} /> </Drawer.Navigator> </NavigationContainer> );}
드로어 내비게이터의 위치를 과거 버전(정확한 것은 자신이 사용하는 버전을 확인하자. 나의 경우 6.1.8이다.)에서는 아래와 같이 변경해야 한다. 내가 설치한 버전의 경우 위와 같이 적용해 주면 된다. (최신 버전으로 넘어오면서 drawerPositionProps가 deprecated 되었다.)
<NavigationContainer> <Drawer.NavigatorinitialRouteName="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 간단한 수정 예functionApp() {return ( <NavigationContainer> <Drawer.NavigatorinitialRouteName="Home"backBehavior="history"screenOptions={{ drawerActiveBackgroundColor:'#fb8c00', drawerActiveTintColor:'#fff', }}> <Drawer.Screenname="Home"component={HomeScreen}options={{title:'홈'}} /> <Drawer.Screenname="Setting"component={SettingScreen}options={{title:'설정'}} /> </Drawer.Navigator> </NavigationContainer> );}
drawerContent
drawerContent의 경우 드로어 영역에 아예 다른 컴포넌트를 보여주고 싶을 때 사용할 수 있으며, 함수 컴포넌트를 넣어주면 된다.
만약 ios도 지원하는 앱이라면 drawerContent를 지정할 때 SafeAreaView도 꼭 사용해야 StatusBar영역과 상단 영역이 겹치는 현상을 방지할 수 있다.
처음에 해봤듯이 아이콘을 사용하기 위해서는 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"
// App.jsconstStack=createNativeStackNavigator();functionApp() {return ( <NavigationContainer> <Stack.NavigatorinitialRouteName="Home"screenOptions={{ tabBarShowLabel:false, tabBarActiveTintColor:'#fb8c00', }}> <Stack.Screenname="Main"component={MainScreen} // <-- MainScreen.js 참고// * 이 설정을 추가하지 않으면 헤더가 두개가 보이는 현상이 나타난다.// * 하단 탭 내비게이터를 스택 내비게이터 내부에서 사용하게 될 때 이 설정을 꼭 해주어야 한다.options={{headerShown:false}} /> <Stack.Screenname="Detail"component={DetailScreen} /> </Stack.Navigator> </NavigationContainer> );}
머티리얼 내비게이터
머티리얼 상단 탭과 하단 탭 내비게이터에 대해 알아보자. 머티리얼 탭의 특징은 구글 머티리얼 디자인 특유의 리플(ripple) 효과가 나타나며, 스와이프를 통한 이동을 지원한다. [공식 문서]
options Props에 일반 객체가 아닌 객체를 반환하는 함수를 넣었다. 이렇게 하면 내비게이션의 상태가 바뀔때마다 함수를 다시 실행하여 화면의 options 객체를 생성하게 된다.
여기서 사용한 getFocusedRouteNameFromRoute() 함수의 경우 route 객체를 통해 현재 포커스된 화면의 이름을 조회한다. 여기서 주의할 점은 "내비게이션의 상태가 바뀔때마다"이기 때문에 초기에는 undefined가 반환되므로 여기에 대한 기본 값을 미리 넣어주도록 만들어야 한다.
내비게이션 Hooks
리액트 내비게이션은 여러 hook들을 제공해 준다. 이런 훅들을 왜 제공받아야 하는지 궁금할 수 있지만, 우리가 만든 예제에서도 필요한 이유를 찾을 수 있다.
지금은 간단한 애플리케이션을 만들었기 때문에 화면만 존재 하기 때문에 별문제가 안생겼지만, Screen으로 사용되지 않는 컴포넌트에서 route와 navigation을 사용할 수 없다. 물론 Props로 넘겨줘도 되겠지만, 필요한 컴포넌트의 뎁스(depth)가 싶을수록, 필요한 Props가 많을수록 복잡성을 야기하게 된다.
useNavigation
Screen으로 사용되고 있지 않는 컴포넌트에서도 navigation 객체를 사용할 수 있다.
useFocusEffect는 화면에 포커스가 잡혔을 때 특정 작업을 할 수 있게 하는 Hook이다. [공식문서]
이 앱에서는 화면이 사라지는게 아니라, 화면을 쌓으면서 보여주는 것이다. 예를들어 DetailScreen을 띄운다면 HomeScreen 위에 DetailScreen을 쌓아서 보여주는 것이다. 그래서 만약 useEffect를 통해서만 어떤 외부효과를 일으키는 것을 하고 싶다면 처음에는 동작하겠지만, 이전 화면으로 돌아왔을때는 실행되지 않는다.
그래서 다른 화면을 열었다가 돌아왔을 때 특정 작업을 하고 싶다면 useFocusEffect을 사용해야 한다. 또 현재 화면에서 다른 화면으로 넘어갈 때 특정 작업을 하고 싶다면 useFocusEffect에서 함수를 반환해 주면 된다.
다만 useFocusEffect을 사용할 때는 꼭 useCallback과 함께 사용해야 한다. 만약 useCallback을 사용하지 않으면 컴포넌트가 리렌더링될 때마다 useFocusEffect에 등록한 함수가 실행될 것이다.