Navigation 심화 — 중첩 네비게이션과 딥링크
실제 앱의 네비게이션은 여러 네비게이터가 중첩된 복잡한 구조입니다.
기초편에서 Stack, Tab을 개별적으로 배웠다면, 이번에는 실제 앱처럼 여러 네비게이터를 조합 하는 방법을 정리합니다.
중첩 네비게이터 설계
일반적인 앱 구조
App
├── Auth Stack (로그인 전)
│ ├── Login
│ └── Register
└── Main Stack (로그인 후)
├── Bottom Tabs
│ ├── Home Stack
│ │ ├── HomeScreen
│ │ └── DetailScreen
│ ├── Search Stack
│ │ ├── SearchScreen
│ │ └── ResultScreen
│ └── Profile Stack
│ ├── ProfileScreen
│ └── EditProfileScreen
└── Modal Screens
├── SettingsScreen
└── NotificationScreen
타입 정의
// 전체 네비게이션 타입을 한 곳에서 관리
export type AuthStackParamList = {
Login: undefined;
Register: { inviteCode?: string };
};
export type HomeStackParamList = {
HomeScreen: undefined;
DetailScreen: { id: number };
};
export type SearchStackParamList = {
SearchScreen: undefined;
ResultScreen: { query: string };
};
export type ProfileStackParamList = {
ProfileScreen: undefined;
EditProfileScreen: undefined;
};
export type MainTabParamList = {
HomeTab: undefined;
SearchTab: undefined;
ProfileTab: undefined;
};
export type RootStackParamList = {
Auth: undefined;
MainTabs: undefined;
Settings: undefined;
Notification: undefined;
};
네비게이터 구현
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const RootStack = createNativeStackNavigator<RootStackParamList>();
const AuthStack = createNativeStackNavigator<AuthStackParamList>();
const HomeStack = createNativeStackNavigator<HomeStackParamList>();
const Tab = createBottomTabNavigator<MainTabParamList>();
// 인증 스택
function AuthNavigator() {
return (
<AuthStack.Navigator screenOptions={{ headerShown: false }}>
<AuthStack.Screen name="Login" component={LoginScreen} />
<AuthStack.Screen name="Register" component={RegisterScreen} />
</AuthStack.Navigator>
);
}
// 홈 스택 (탭 안에 들어감)
function HomeNavigator() {
return (
<HomeStack.Navigator>
<HomeStack.Screen name="HomeScreen" component={HomeScreen} />
<HomeStack.Screen name="DetailScreen" component={DetailScreen} />
</HomeStack.Navigator>
);
}
// 메인 탭
function MainTabNavigator() {
return (
<Tab.Navigator>
<Tab.Screen
name="HomeTab"
component={HomeNavigator}
options={{ headerShown: false }}
/>
<Tab.Screen name="SearchTab" component={SearchNavigator} />
<Tab.Screen name="ProfileTab" component={ProfileNavigator} />
</Tab.Navigator>
);
}
// 루트 네비게이터
function App() {
const isLoggedIn = useAuth();
return (
<NavigationContainer>
<RootStack.Navigator screenOptions={{ headerShown: false }}>
{isLoggedIn ? (
<>
<RootStack.Screen name="MainTabs" component={MainTabNavigator} />
<RootStack.Screen
name="Settings"
component={SettingsScreen}
options={{ presentation: 'modal' }}
/>
</>
) : (
<RootStack.Screen name="Auth" component={AuthNavigator} />
)}
</RootStack.Navigator>
</NavigationContainer>
);
}
로그인 여부에 따라 Auth/Main 스택을 조건부로 렌더링하는 패턴은 거의 모든 앱에서 사용됩니다. 조건부 렌더링이므로
navigate가 아닌 상태 변경으로 화면이 전환 됩니다.
중첩 네비게이터에서의 이동
// 중첩된 화면으로 이동
navigation.navigate('HomeTab', {
screen: 'DetailScreen',
params: { id: 42 },
});
// 깊이 중첩된 경우
navigation.navigate('MainTabs', {
screen: 'SearchTab',
params: {
screen: 'ResultScreen',
params: { query: 'react native' },
},
});
부모 네비게이터 접근
// 자식 네비게이터에서 부모의 navigation 사용
function DetailScreen({ navigation }: DetailProps) {
return (
<Pressable
onPress={() => {
// 부모 Stack의 navigate 사용
navigation.getParent()?.navigate('Settings');
}}
>
<Text>설정으로</Text>
</Pressable>
);
}
딥링크 설정
앱 외부에서 특정 화면으로 직접 이동하는 기능입니다.
const linking = {
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
MainTabs: {
screens: {
HomeTab: {
screens: {
HomeScreen: 'home',
DetailScreen: 'detail/:id',
},
},
SearchTab: {
screens: {
SearchScreen: 'search',
ResultScreen: 'search/:query',
},
},
ProfileTab: 'profile',
},
},
Settings: 'settings',
Auth: {
screens: {
Login: 'login',
Register: 'register',
},
},
},
},
};
function App() {
return (
<NavigationContainer linking={linking} fallback={<LoadingScreen />}>
{/* 네비게이터 */}
</NavigationContainer>
);
}
딥링크 테스트
# iOS 시뮬레이터
xcrun simctl openurl booted "myapp://detail/42"
# Android 에뮬레이터
adb shell am start -W -a android.intent.action.VIEW -d "myapp://detail/42"
# Expo
npx uri-scheme open "myapp://detail/42" --ios
네비게이션 상태 관리
import {
useNavigation,
useRoute,
useNavigationState,
useFocusEffect,
useIsFocused,
} from '@react-navigation/native';
function MyComponent() {
// 네비게이션 객체
const navigation = useNavigation();
// 현재 라우트 정보
const route = useRoute();
// 네비게이션 상태
const currentRouteName = useNavigationState(
(state) => state.routes[state.index].name
);
// 화면 포커스 여부
const isFocused = useIsFocused();
// 포커스 이벤트
useFocusEffect(
useCallback(() => {
// 화면이 포커스될 때 데이터 새로고침
fetchData();
return () => cleanup();
}, [])
);
return <View />;
}
화면 전환 애니메이션
<Stack.Navigator
screenOptions={{
// iOS 스타일 슬라이드
animation: 'slide_from_right',
// animation: 'slide_from_bottom', // 모달 스타일
// animation: 'fade', // 페이드
// animation: 'none', // 애니메이션 없음
}}
>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="Modal"
component={ModalScreen}
options={{
presentation: 'modal', // 모달로 표시
animation: 'slide_from_bottom',
}}
/>
<Stack.Screen
name="Fullscreen"
component={FullscreenScreen}
options={{
presentation: 'fullScreenModal',
animation: 'fade',
}}
/>
</Stack.Navigator>
네비게이션 가드 (화면 이탈 방지)
import { useCallback } from 'react';
function EditScreen({ navigation }: EditProps) {
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// 저장하지 않은 변경사항이 있으면 이탈 방지
React.useEffect(() => {
const unsubscribe = navigation.addListener('beforeRemove', (e) => {
if (!hasUnsavedChanges) return;
// 이탈 방지
e.preventDefault();
// 확인 다이얼로그
Alert.alert(
'변경사항 저장',
'저장하지 않은 변경사항이 있습니다. 정말 나가시겠습니까?',
[
{ text: '취소', style: 'cancel' },
{
text: '나가기',
style: 'destructive',
onPress: () => navigation.dispatch(e.data.action),
},
]
);
});
return unsubscribe;
}, [navigation, hasUnsavedChanges]);
return <View />;
}
정리
- 실제 앱은 Auth/Main 분기 + Tab + Stack 형태의 중첩 구조가 일반적입니다
- 중첩 화면 이동 시
navigate('TabName', { screen: 'ScreenName' })패턴을 사용합니다 - 딥링크는
linking설정으로 URL 패턴과 화면을 매핑합니다 beforeRemove이벤트로 화면 이탈을 방지할 수 있습니다- TypeScript로
ParamList를 정의하면 잘못된 화면 이동이나 파라미터 타입 오류를 컴파일 타임에 잡을 수 있습니다
댓글 로딩 중...