본문 바로가기
개발 노트/React Native

[React Native] 통합 앱에서 프로파일별 화면 접근을 route guard로 제한하는 방법

by pposooj 2026. 5. 20.

React Native 통합 앱에서 여러 프로파일을 하나의 코드베이스로 관리하면, 앱마다 접근 가능한 화면이 달라질 수 있습니다. 어떤 앱은 지도 화면을 사용하고, 어떤 앱은 리스트만 사용하고, 또 다른 앱은 설정 화면이나 외부 링크 기능을 제한해야 할 수도 있습니다.

처음에는 메뉴에서 버튼만 숨기면 충분할 것 같았습니다. 하지만 정리해보니 화면 접근 경로는 메뉴 클릭만 있는 것이 아니었습니다. 딥링크, 푸시 알림 클릭, 내부 navigation 호출, 이전 화면 복원 같은 경로로도 특정 화면에 접근할 수 있습니다.

그래서 프로파일별로 허용된 route를 정의하고, 화면 이동 전에 한 번 더 확인하는 route guard 구조가 필요합니다. 이 글에서는 React Native 통합 앱에서 enabledRoutesroute guard를 설계할 때 확인하면 좋은 기준을 정리해보겠습니다.

이 글에서 다룰 문제

프로파일별 화면 접근 기준이 없으면 다음 문제가 생길 수 있습니다.

  • 메뉴에서는 숨긴 화면이 딥링크로는 열린다.
  • 지도 기능이 없는 앱에서 알림 클릭으로 지도 상세 화면이 열린다.
  • selectedProfiles에는 포함되지 않은 앱의 route가 내부 navigation에 남아 있다.
  • 비활성 기능 화면에 접근했을 때 빈 화면이나 크래시가 발생한다.
  • fallback route가 없어 접근 차단 후 이동 기준이 모호해진다.
  • route guard를 서버 권한 검증처럼 오해해 실제 API 권한 검증을 놓친다.

초기에는 profileId를 기준으로 화면마다 조건문을 넣기 쉽습니다. 한두 화면에서는 빠르게 처리되는 것처럼 보이지만, 앱과 화면이 늘어나면 조건이 흩어져 관리하기 어려워집니다. route guard는 이런 접근 기준을 한 곳에서 확인하기 위한 구조로 보면 좋을 것 같습니다.

route guard의 개념

route guard는 화면 이동 전에 현재 프로파일에서 이 화면에 접근해도 되는지 확인하는 단계입니다. 웹 라우터의 guard와 비슷하게 볼 수 있지만, React Native에서는 네비게이션, 딥링크, 알림 클릭, 앱 상태 복원까지 함께 고려해야 합니다.

접근 경로 예시 guard가 필요한 이유
메뉴 클릭 탭, Drawer, 홈 버튼 메뉴 노출 조건과 실제 이동 조건 일치
내부 navigation navigation.navigate('Map') 코드에서 직접 호출하는 route 검증
딥링크 myapp://map/detail/1 외부 진입 경로 제한
푸시 알림 알림 클릭 후 상세 화면 이동 payload의 route 검증
상태 복원 앱 재실행 후 이전 화면 복구 비활성 route 복원 방지

중요한 점은 route guard가 보안의 전부는 아니라는 것입니다. 앱 화면 접근을 제한할 수는 있지만, 서버 API 권한 검증을 대신할 수는 없습니다. 사용자가 앱에서 화면을 열 수 없더라도 API는 별도로 보호되어야 합니다.

기본 구조: enabledRoutes 정의하기

먼저 profile registry에 프로파일별 접근 가능한 route를 정의합니다. 아래 예시는 실제 프로젝트에 썼던 코드에서 민감 정보를 제거한 코드입니다.

type RouteName = 'Home' | 'Map' | 'MapDetail' | 'Notice' | 'Settings' | 'WebView';

type AppProfile = {
  profileId: string;
  displayName: string;
  initialRoute: RouteName;
  enabledRoutes: RouteName[];
  fallbackRoute: RouteName;
};

const profileRegistry: Record<string, AppProfile> = {
  sampleTour: {
    profileId: 'sampleTour',
    displayName: '샘플 관광 앱',
    initialRoute: 'Home',
    enabledRoutes: ['Home', 'Map', 'MapDetail', 'Notice', 'Settings'],
    fallbackRoute: 'Home',
  },
  sampleInfo: {
    profileId: 'sampleInfo',
    displayName: '샘플 정보 앱',
    initialRoute: 'Notice',
    enabledRoutes: ['Home', 'Notice', 'Settings', 'WebView'],
    fallbackRoute: 'Notice',
  },
};

function getProfile(profileId: string): AppProfile {
  const profile = profileRegistry[profileId];

  if (!profile) {
    throw new Error(`Unknown profileId: ${profileId}`);
  }

  return profile;
}

function canAccessRoute(profile: AppProfile, routeName: RouteName): boolean {
  return profile.enabledRoutes.includes(routeName);
}

이 구조의 장점은 화면 접근 정책을 데이터로 확인할 수 있다는 점입니다. 메뉴, 네비게이션, 딥링크 처리에서 같은 기준을 재사용할 수 있습니다.

처음에는 route마다 조건문을 직접 넣는 방식이 더 간단해 보일 수 있습니다. 하지만 화면이 늘어나면 어떤 프로파일이 어떤 route를 사용할 수 있는지 한눈에 보기 어려워집니다. enabledRoutes를 기준으로 모아두면 나중에 수정할 때 훨씬 덜 헷갈리는 것 같습니다.

안전한 navigation helper 만들기

화면마다 직접 navigation.navigate()를 호출하면 guard를 빠뜨리기 쉽습니다. 그래서 공통 helper를 만들고, 화면 이동 전에 프로파일의 enabledRoutes를 확인하는 방식이 좋습니다.

type NavigateParams = {
  routeName: RouteName;
  params?: Record<string, unknown>;
};

type NavigationLike = {
  navigate: (routeName: string, params?: Record<string, unknown>) => void;
  replace?: (routeName: string, params?: Record<string, unknown>) => void;
};

export function navigateWithGuard(
  navigation: NavigationLike,
  profile: AppProfile,
  target: NavigateParams,
) {
  if (canAccessRoute(profile, target.routeName)) {
    navigation.navigate(target.routeName, target.params);
    return;
  }

  const fallbackRoute = profile.fallbackRoute || profile.initialRoute;

  if (navigation.replace) {
    navigation.replace(fallbackRoute);
    return;
  }

  navigation.navigate(fallbackRoute);
}

이 helper는 단순하지만 효과가 있습니다. 허용되지 않은 route로 이동하려고 하면 빈 화면으로 보내지 않고 fallbackRoute로 돌려보냅니다.

실제 앱에서는 여기에 로깅을 추가해 어떤 프로파일에서 어떤 route 접근이 차단됐는지도 남길 수 있습니다. 처음 구현할 때는 fallback만 있어도 충분해 보일 수 있지만, 나중에 원인을 찾으려면 차단 로그가 꽤 도움이 됩니다.

메뉴 노출과 route guard를 함께 맞추기

route guard가 있어도 메뉴가 계속 보이면 사용자 입장에서는 이상하게 느껴질 수 있습니다. 따라서 메뉴를 렌더링할 때도 같은 enabledRoutes 기준을 사용해야 합니다.

type MenuItem = {
  label: string;
  routeName: RouteName;
};

const allMenuItems: MenuItem[] = [
  {label: '홈', routeName: 'Home'},
  {label: '지도', routeName: 'Map'},
  {label: '공지', routeName: 'Notice'},
  {label: '설정', routeName: 'Settings'},
];

function getVisibleMenuItems(profile: AppProfile): MenuItem[] {
  return allMenuItems.filter((item) => canAccessRoute(profile, item.routeName));
}

핵심은 메뉴와 실제 이동 조건이 같은 기준을 보도록 만드는 것입니다. 메뉴에서는 숨겼는데 딥링크로는 열리거나, 메뉴에는 보이는데 클릭하면 fallback으로 이동하는 상태는 사용자 경험이 좋지 않습니다.

이 부분은 처음에는 놓치기 쉽습니다. 메뉴만 숨기면 화면이 막힌 것처럼 느껴지지만, 내부 이동이나 외부 진입 경로가 남아 있을 수 있기 때문입니다.

딥링크와 알림 클릭 처리

딥링크와 푸시 알림 클릭은 route guard에서 특히 중요합니다. 외부에서 들어오는 payload는 앱 내부 버튼보다 더 조심해서 검증해야 합니다.

type IncomingRoutePayload = {
  routeName?: string;
  id?: string;
};

function isRouteName(value: unknown): value is RouteName {
  return ['Home', 'Map', 'MapDetail', 'Notice', 'Settings', 'WebView'].includes(String(value));
}

export function resolveIncomingRoute(
  profile: AppProfile,
  payload: IncomingRoutePayload,
): NavigateParams {
  const routeName = payload.routeName;

  if (!isRouteName(routeName)) {
    return {routeName: profile.fallbackRoute};
  }

  if (!canAccessRoute(profile, routeName)) {
    return {routeName: profile.fallbackRoute};
  }

  return {
    routeName,
    params: payload.id ? {id: payload.id} : undefined,
  };
}

푸시 payload나 딥링크 값은 신뢰하지 않는 편이 안전합니다. route 이름이 실제로 존재하는지 확인하고, 현재 프로파일에서 허용되는지도 확인해야 합니다. 필요하다면 id 형식이나 route별 필수 파라미터도 검증하는 것이 좋습니다.

처음에는 알림 payload에 route 이름이 들어오면 그대로 이동해도 될 것처럼 보일 수 있습니다. 하지만 통합 앱에서는 프로파일마다 허용된 화면이 다르기 때문에, 외부 진입 값도 현재 프로파일 기준으로 다시 확인해야 합니다.

route guard와 서버 권한 검증은 다르다

route guard는 앱 화면 접근을 제어하는 장치입니다. 하지만 서버 데이터 접근을 보호하는 기능은 아닙니다. 이 차이를 분리해서 보는 것이 중요합니다.

구분 역할 예시
route guard 앱 내부 화면 이동 제한 지도 기능이 없는 앱에서 Map route 차단
feature flag 기능 노출 정책 메뉴, 버튼, 탭 표시 여부
selectedProfiles 빌드 대상 제한 이번 빌드에 포함할 프로파일 선택
서버 권한 검증 실제 데이터 접근 보호 API 인증, 사용자 권한, 토큰 검증

앱에서 메뉴를 숨겼다고 해서 API가 안전해지는 것은 아닙니다. 실제 권한은 서버에서 다시 검증해야 합니다. route guard는 사용자 경험과 앱 내부 접근 정책을 안정화하는 장치로 보는 편이 좋습니다.

fallback route 기준

허용되지 않은 route에 접근했을 때 어디로 보낼지도 정해야 합니다. 무조건 Home으로 보내면 모든 앱에 Home route가 있어야 하고, 프로파일별 첫 화면이 다른 경우 어색할 수 있습니다.

상황 추천 fallback
route가 존재하지 않음 프로파일의 fallbackRoute
권한 없는 화면 접근 initialRoute 또는 안내 화면
파라미터 오류 목록 화면 또는 오류 안내 화면
딥링크 오류 안전한 기본 화면
알림 payload 오류 알림 목록 또는 홈 화면

운영 관점에서는 fallback 발생 로그를 남기는 것도 좋습니다. 같은 route가 반복해서 차단된다면 메뉴 조건, 알림 payload, 딥링크 설정 중 하나가 잘못됐을 가능성이 있습니다.

회귀 테스트에서 확인할 항목

route guard를 추가한 뒤에는 아래 항목을 함께 확인하는 편이 좋습니다. 처음에는 화면 이동만 확인하면 된다고 생각하기 쉽지만, 실제로는 메뉴, 딥링크, 알림, fallback까지 같이 봐야 합니다.

  • enabledRoutes에 없는 메뉴가 화면에 보이지 않는지 확인한다.
  • enabledRoutes에 없는 route로 내부 이동 시 fallback 되는지 확인한다.
  • 딥링크로 비활성 route를 열었을 때 안전한 화면으로 이동하는지 확인한다.
  • 푸시 알림 payload의 routeName이 검증되는지 확인한다.
  • fallbackRoute가 실제 존재하는 route인지 확인한다.
  • initialRouteenabledRoutes에 포함되어 있는지 확인한다.
  • feature flagenabledRoutes 기준이 서로 충돌하지 않는지 확인한다.
  • 서버 API 권한 검증을 route guard로 대체하지 않았는지 확인한다.

처음 구현할 때는 initialRouteenabledRoutes에 빠지는 실수가 꽤 치명적입니다. 앱을 실행하자마자 fallback이 반복되거나 빈 화면이 될 수 있기 때문입니다.

자주 하는 실수

1. 메뉴만 숨기고 navigation 호출은 그대로 두는 경우가 있습니다. 화면 버튼은 사라졌지만 딥링크나 내부 코드에서는 여전히 접근할 수 있습니다. 메뉴 노출 조건과 실제 route 접근 조건을 같은 기준으로 맞추는 것이 좋습니다.

2. fallbackRoute를 정하지 않는 경우도 있습니다. 차단된 route가 있을 때 어디로 이동해야 하는지 기준이 없으면 사용자에게 빈 화면이나 오류만 보일 수 있습니다.

3. route guard를 보안 기능으로 과신하는 경우가 있습니다. 앱 화면 접근을 막는 것과 서버 데이터 접근을 막는 것은 다릅니다. API 권한 검증은 서버에서 별도로 처리해야 합니다.

4. 푸시 payload를 그대로 믿는 경우도 있습니다. 알림 클릭으로 이동하는 화면도 현재 프로파일에서 허용되는지 확인해야 합니다. 외부에서 들어오는 값은 한 번 더 검증하는 습관이 필요합니다.

결론

React Native 통합 앱에서 route guard는 프로파일별 화면 접근 기준을 명확히 하기 위한 장치입니다. 메뉴에서 숨기는 것만으로는 부족하고, 내부 navigation, 딥링크, 푸시 알림 클릭, 상태 복원까지 같은 기준으로 확인해야 합니다.

좋은 구조는 profile registryenabledRoutesfallbackRoute를 정의하고, 공통 navigation helper에서 접근 가능 여부를 확인하는 방식입니다. 여기에 메뉴 렌더링, 딥링크 처리, 알림 payload 검증을 연결하면 프로파일별 화면 접근 정책이 훨씬 일관됩니다.

다만 route guard는 보안의 전부가 아닙니다. 서버 API 권한 검증, 인증, 토큰 검증은 별도로 필요합니다. route guard는 사용자 경험과 앱 내부 접근 정책을 안정화하는 도구로 보고, 실제 데이터 보호는 서버에서 다시 확인하는 구조가 안전합니다.

참고 자료

  • React Navigation 공식 문서의 navigation, deep linking 관련 문서
  • React Native Linking 관련 공식 문서
  • Android intent/deep link 처리 관련 공식 문서
  • 프로젝트 내부 profile registry, selectedProfiles, route guard, push payload 문서