앞써 공부한 내용(설치 및 할일목록 만들기)를 토대로, 내비게이션(navigation
)을 공부한다.
- IOS 애뮬레이터 리로딩:
커맨드
+R
- Android 애뮬레이터 리로딩:
R
두번
(키보드 입력이 한글인 상태에서는 해당 커맨드가 안먹는다.)
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;
react-navigation
라이브러리에는 다른 상황에서 사용할 수 있는 다양한 네비게이터가 있지만, RN
앱에서는 화면 전환할 때 브라우저의 History
와 비슷한 사용성을 제공하기 위해 네이티브 스택 내비게이터를 사용한다.
Native Stack Navigator
의 경우 가장 많이 사용되며, 안드로이드에서는 Fragment
, IOS
에서는 UINavigationController
를 사용해 일반 네이티브 앱과 정확히 동일한 방식으로 화면을 관리한다.
$ yarn add @react-navigation/native-stack
화면 컴포넌트를 screens
라는 폴더를 생성하고 그 안에 HomeScreen
과 DetailScreen
을 생성해 보자.
// 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})}
/>
뒤로 가기 기능의 경우 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.Screen
의Props
로 설정하는 것이다. - 두 번째 방법은 화면 컴포넌트에서
navigation.setOptions
함수를 사용하는 것이다.
Stack.Screen
의 Props
로 설정하는 것이다.
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>
);
}
화면 컴포넌트에서 navigation.setOptions
함수를 사용하는 것이다.
function HomeScreen({navigation}) {
useEffect(() => {
navigation.setOptions({title: '홈 화면'});
// cf) navigation 객체가 바뀔 일은 없지만, ESLint 규칙상 내부에서 사용하는 값을 넣어야 하기 때문에 추가
}, [navigation]);
return (...)
참고로 useEffect
를 통해서 설정한 내비게이션 option
은 App
컴포넌트에서 Props
를 통해 설정한 option
을 덮어쓰게 된다.
Stack.Screen
의 Props
로 설정하는 것이다.
<Stack.Screen
name="Detail"
component={DetailScreen}
options={({route}) => ({
title: `상세 정보 - ${route.params.id}`,
})}
/>
화면 컴포넌트에서 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-navigation
에 react-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.Navigator
의 screenOptions
에 headerShown: 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
Props
가 deprecated
되었다.)
<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
의 경우 드로어 영역에 아예 다른 컴포넌트를 보여주고 싶을 때 사용할 수 있으며, 함수 컴포넌트를 넣어주면 된다.
만약 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>
);
}
처음에 해봤듯이 아이콘을 사용하기 위해서는 ios와 android에 각각의 설정이 필요하다.
설정 후 앱 다시 실행
// ios/LearnReactNavigation/Info.plist
// ...
// <key>UIViewControllerBasedStatusBarAppearance</key>
// <false/>
// 맨 아래에 추가
<key>UIAppFonts</key>
<array>
<string>MaterialIcons.ttf</string>
</array>
// </dict>
// </plist>
설정 후 앱 다시 실행
// 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} />
// ...
커스터마이징 또한 다른 탭들과 동일한 방식으로 지원된다. 다만 머티리얼 탭에서만 추가된 속성들도 있으니 [공식 문서]를 참고하자.
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
가 반환되므로 여기에 대한 기본 값을 미리 넣어주도록 만들어야 한다.
리액트 내비게이션은 여러 hook들을 제공해 준다. 이런 훅들을 왜 제공받아야 하는지 궁금할 수 있지만, 우리가 만든 예제에서도 필요한 이유를 찾을 수 있다.
지금은 간단한 애플리케이션을 만들었기 때문에 화면만 존재 하기 때문에 별문제가 안생겼지만, Screen
으로 사용되지 않는 컴포넌트에서 route
와 navigation
을 사용할 수 없다. 물론 Props로 넘겨줘도 되겠지만, 필요한 컴포넌트의 뎁스(depth)가 싶을수록, 필요한 Props가 많을수록 복잡성을 야기하게 된다.
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>
);
}
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
는 화면에 포커스가 잡혔을 때 특정 작업을 할 수 있게 하는 Hook
이다. [공식문서]
이 앱에서는 화면이 사라지는게 아니라, 화면을 쌓으면서 보여주는 것이다. 예를들어 DetailScreen
을 띄운다면 HomeScreen
위에 DetailScreen
을 쌓아서 보여주는 것이다. 그래서 만약 useEffect를 통해서만 어떤 외부효과를 일으키는 것을 하고 싶다면 처음에는 동작하겠지만, 이전 화면으로 돌아왔을때는 실행되지 않는다.
그래서 다른 화면을 열었다가 돌아왔을 때 특정 작업을 하고 싶다면 useFocusEffect
을 사용해야 한다. 또 현재 화면에서 다른 화면으로 넘어갈 때 특정 작업을 하고 싶다면 useFocusEffect
에서 함수를 반환해 주면 된다.
다만 useFocusEffect
을 사용할 때는 꼭 useCallback과 함께 사용해야 한다. 만약 useCallback을 사용하지 않으면 컴포넌트가 리렌더링될 때마다 useFocusEffect
에 등록한 함수가 실행될 것이다.