실제 앱의 네비게이션은 여러 네비게이터가 중첩된 복잡한 구조입니다.

기초편에서 Stack, Tab을 개별적으로 배웠다면, 이번에는 실제 앱처럼 여러 네비게이터를 조합 하는 방법을 정리합니다.


중첩 네비게이터 설계

일반적인 앱 구조

PLAINTEXT
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

타입 정의

TSX
// 전체 네비게이션 타입을 한 곳에서 관리
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;
};

네비게이터 구현

TSX
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가 아닌 상태 변경으로 화면이 전환 됩니다.


중첩 네비게이터에서의 이동

TSX
// 중첩된 화면으로 이동
navigation.navigate('HomeTab', {
  screen: 'DetailScreen',
  params: { id: 42 },
});

// 깊이 중첩된 경우
navigation.navigate('MainTabs', {
  screen: 'SearchTab',
  params: {
    screen: 'ResultScreen',
    params: { query: 'react native' },
  },
});

부모 네비게이터 접근

TSX
// 자식 네비게이터에서 부모의 navigation 사용
function DetailScreen({ navigation }: DetailProps) {
  return (
    <Pressable
      onPress={() => {
        // 부모 Stack의 navigate 사용
        navigation.getParent()?.navigate('Settings');
      }}
    >
      <Text>설정으로</Text>
    </Pressable>
  );
}

딥링크 설정

앱 외부에서 특정 화면으로 직접 이동하는 기능입니다.

TSX
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>
  );
}

딥링크 테스트

BASH
# 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

네비게이션 상태 관리

TSX
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 />;
}

화면 전환 애니메이션

TSX
<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>

네비게이션 가드 (화면 이탈 방지)

TSX
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를 정의하면 잘못된 화면 이동이나 파라미터 타입 오류를 컴파일 타임에 잡을 수 있습니다
댓글 로딩 중...