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

[React Native] 통합 앱에서 프로파일별 권한 요청 정책을 분리하는 방법

by pposooj 2026. 5. 20.

여러 프로파일을 하나의 코드베이스로 관리하면 앱마다 필요한 권한이 달라집니다. 어떤 앱은 지도 기능 때문에 위치 권한이 필요하고, 어떤 앱은 알림 기능만 필요할 수 있습니다. 또 어떤 프로파일은 카메라나 파일 접근이 전혀 필요하지 않을 수도 있습니다.

이때 권한 요청을 공통 앱 시작 지점에서 한 번에 처리하면 사용자 경험이 나빠질 수 있습니다. 지도 기능이 없는 앱에서 위치 권한을 묻거나, 알림 기능이 꺼진 프로파일에서 알림 권한을 요청하면 사용자는 왜 필요한지 이해하기 어렵습니다. 심사나 운영 관점에서도 불필요한 권한 요청은 리스크가 됩니다.

처음에는 권한 요청 코드를 공통 함수 하나로 만들어 앱 실행 시 호출하면 편하기야 하지만 실제로 적용해보면 권한은 기능 진입 시점과 연결해서 요청하는 편이 더 안전한 것 같습니다.

이 글에서는 React Native 통합 앱에서 프로파일별 권한 요청 정책을 분리하는 방법을 정리해보겠습니다. feature flag, permission policy, fallback UX, 회귀 테스트 기준을 중심으로 살펴보겠습니다.

이 글에서 다룰 문제

권한 요청 정책이 정리되지 않으면 다음 문제가 생길 수 있습니다.

  • 위치 기능이 없는 앱에서 위치 권한 팝업이 뜬다.
  • 알림 기능이 꺼진 프로파일에서 알림 권한 요청 코드가 실행된다.
  • 권한을 거부했을 때 어떤 화면으로 돌려보낼지 기준이 없다.
  • feature flag는 꺼져 있는데 권한 요청은 남아 있어 정책이 충돌한다.
  • Android와 iOS 권한 흐름 차이를 한 파일에서 임시 조건문으로 처리한다.
  • 테스트 fixture나 문서에 사용자 식별자, 실제 서버 URL, 내부 설정값이 남는다.

처음에는 권한 요청을 한 곳에서 처리하는 방식이 깔끔해 보일 수 있지만 프로파일이 늘어나면 기능을 사용하지 않는 앱에서도 불필요한 권한 흐름이 실행될 수 있습니다. 권한은 앱 전체 기준보다 기능과 프로파일 기준으로 나누어 보는 편이 더 좋습니다.

feature와 permission은 다르다

feature flagpermission policy는 서로 연결되어 있지만 같은 것은 아닙니다. 기능이 켜져 있다는 사실과 권한을 언제 요청해야 하는지는 분리해서 봐야 합니다.

구분 역할 예시
feature flag 기능 사용 여부 결정 map: true, push: false
permission policy 기능에 필요한 단말 권한 정의 지도 기능은 위치 권한 필요
request timing 권한 요청 시점 결정 앱 시작 시점 또는 기능 진입 시점
fallback UX 권한 거부 시 사용자 흐름 안내 화면, 설정 이동, 기능 제한
server authorization 서버 데이터 접근 권한 API 인증, 사용자 권한 검증

예를 들어 map: true라고 해서 앱 실행 즉시 위치 권한을 요청해야 하는 것은 아닙니다. 사용자가 실제로 지도 화면에 들어갈 때 요청하는 편이 더 자연스러울 수 있습니다. 반대로 map: false라면 위치 권한 요청 자체가 발생하지 않아야 합니다.

기본 구조: permission policy 정의하기

먼저 프로파일별 feature flag와 권한 정책을 분리해서 정의합니다. 

type FeatureKey = 'map' | 'push' | 'cameraScan' | 'fileUpload';
type PermissionKey = 'location' | 'notification' | 'camera' | 'photoLibrary';

type FeatureFlags = Record<FeatureKey, boolean>;

type PermissionPolicy = {
  requiredFor: FeatureKey;
  permissions: PermissionKey[];
  requestTiming: 'onAppStart' | 'onFeatureEnter' | 'manual';
  fallbackRoute: 'Home' | 'Settings' | 'PermissionGuide';
};

type AppProfile = {
  profileId: string;
  displayName: string;
  features: FeatureFlags;
  permissionPolicies: PermissionPolicy[];
};

const profileRegistry: Record<string, AppProfile> = {
  sampleTour: {
    profileId: 'sampleTour',
    displayName: '샘플 관광 앱',
    features: {
      map: true,
      push: false,
      cameraScan: false,
      fileUpload: false,
    },
    permissionPolicies: [
      {
        requiredFor: 'map',
        permissions: ['location'],
        requestTiming: 'onFeatureEnter',
        fallbackRoute: 'PermissionGuide',
      },
    ],
  },
  sampleNotice: {
    profileId: 'sampleNotice',
    displayName: '샘플 공지 앱',
    features: {
      map: false,
      push: true,
      cameraScan: false,
      fileUpload: false,
    },
    permissionPolicies: [
      {
        requiredFor: 'push',
        permissions: ['notification'],
        requestTiming: 'manual',
        fallbackRoute: 'Settings',
      },
    ],
  },
};

이 구조에서는 기능과 권한을 한 줄로 묶지 않고, 어떤 기능 때문에 어떤 권한이 필요한지 명시합니다. 이렇게 하면 나중에 권한 요청 이유를 문서화하거나 테스트할 때도 기준이 분명해집니다.

처음에는 기능이 켜져 있으면 필요한 권한도 자동으로 따라온다고 생각하기 쉽습니다. 하지만 같은 지도 기능이라도 현재 위치 기반인지, 단순 지도 표시인지에 따라 권한 필요 여부가 달라질 수 있습니다. 그래서 기능과 권한 정책을 따로 정의해두는 편이 더 안전합니다.

기능 진입 시 권한 요청하기

권한은 가능하면 사용 맥락이 생겼을 때 요청하는 것이 좋습니다. 예를 들어 지도 화면에 들어갈 때 위치 권한을 확인하는 방식입니다.

function hasFeature(profile: AppProfile, feature: FeatureKey): boolean {
  return profile.features[feature] === true;
}

function getPermissionPolicy(
  profile: AppProfile,
  feature: FeatureKey,
): PermissionPolicy | undefined {
  return profile.permissionPolicies.find((policy) => policy.requiredFor === feature);
}

async function enterFeature(
  profile: AppProfile,
  feature: FeatureKey,
  requestPermissions: (permissions: PermissionKey[]) => Promise<boolean>,
) {
  if (!hasFeature(profile, feature)) {
    return {allowed: false, reason: 'FEATURE_DISABLED' as const};
  }

  const policy = getPermissionPolicy(profile, feature);
  if (!policy || policy.requestTiming !== 'onFeatureEnter') {
    return {allowed: true, reason: 'NO_RUNTIME_PERMISSION_REQUIRED' as const};
  }

  const granted = await requestPermissions(policy.permissions);

  return granted
    ? {allowed: true, reason: 'PERMISSION_GRANTED' as const}
    : {allowed: false, reason: 'PERMISSION_DENIED' as const, fallbackRoute: policy.fallbackRoute};
}

이 예제에서 중요한 점은 권한 거부를 예외로만 보지 않는 것입니다. 사용자가 권한을 거부할 수 있다는 전제를 두고, fallback UX를 함께 준비해야 합니다.

처음에는 권한이 없으면 에러로 처리하면 된다고 생각하기 쉽습니다. 하지만 권한 거부는 사용자가 선택할 수 있는 정상적인 흐름입니다. 그래서 안내 화면, 다시 시도, 설정 이동 같은 흐름을 미리 준비하는 편이 좋습니다.

adapter로 플랫폼 차이 감싸기

React Native에서는 Android와 iOS의 권한 흐름이 다를 수 있습니다. 이 차이를 화면마다 직접 처리하면 코드가 금방 복잡해집니다. 그래서 권한 요청 adapter를 두는 방식이 좋습니다.

type PermissionAdapter = {
  request: (permission: PermissionKey) => Promise<boolean>;
  openSettings: () => Promise<void>;
};

async function requestAllPermissions(
  adapter: PermissionAdapter,
  permissions: PermissionKey[],
): Promise<boolean> {
  for (const permission of permissions) {
    const granted = await adapter.request(permission);
    if (!granted) {
      return false;
    }
  }

  return true;
}

실제 구현에서는 Android 버전별 알림 권한, iOS 권한 상태, 다시 묻지 않음 상태 등을 더 세밀하게 처리해야 할 수 있습니다. 이 글에서는 구조 설명에 집중하기 위해 단순화했습니다.

플랫폼 차이를 화면 코드에서 계속 처리하면 나중에 같은 조건문이 여러 화면에 반복되기 쉽습니다. adapter로 감싸두면 권한 요청 결과를 앱 내부에서 같은 형태로 다룰 수 있어서 흐름이 조금 더 단순해집니다.

권한 거부 시 fallback UX

권한 요청에서 중요한 것은 거부 이후의 흐름입니다. 권한이 없으면 기능을 사용할 수 없다는 안내를 하고, 사용자가 다시 시도하거나 설정 화면으로 이동할 수 있게 해야 합니다.

상황 권장 UX
처음 거부 기능 사용 이유를 짧게 안내하고 대체 화면 제공
다시 묻지 않음 앱 설정 이동 버튼 제공
기능 비활성 프로파일 권한 요청 없이 메뉴/route 차단
임시 오류 재시도 버튼과 기본 화면 이동 제공
필수 권한 누락 안전한 fallback route로 이동

권한이 필요한 이유는 길게 설명하기보다 사용자가 이해할 수 있는 맥락에서 짧게 보여주는 편이 좋습니다. 예를 들어 현재 위치 주변 장소를 보여주기 위해 위치 권한이 필요하다는 식으로 기능과 연결해서 안내할 수 있습니다.

권한 요청과 route guard 연결하기

권한이 필요한 기능은 route guard와도 연결되어야 합니다. 예를 들어 지도 route는 map feature가 켜져 있어야 하고, 진입 시 위치 권한 정책을 확인해야 합니다.

type RouteName = 'Home' | 'Map' | 'Settings' | 'PermissionGuide';

const routeFeatureMap: Partial<Record<RouteName, FeatureKey>> = {
  Map: 'map',
};

function canAccessRoute(profile: AppProfile, routeName: RouteName): boolean {
  const requiredFeature = routeFeatureMap[routeName];

  if (!requiredFeature) {
    return true;
  }

  return hasFeature(profile, requiredFeature);
}

이렇게 하면 지도 기능이 없는 프로파일에서 지도 route 자체를 차단할 수 있습니다. 이후 실제 지도 화면 진입 시에는 위치 권한을 별도로 확인합니다. route guardpermission policy가 서로 다른 역할을 갖는다는 점이 중요합니다.

이 부분은 처음 구현할 때 헷갈리기 쉽습니다. route 접근을 허용하는 것과 권한을 요청하는 것은 연결되어 있지만 같은 처리는 아닙니다. 접근 가능 여부와 권한 요청 시점을 나눠두면 문제를 더 쉽게 찾을 수 있습니다.

테스트 기준

권한 정책은 단위 테스트와 수동 테스트를 함께 가져가는 것이 좋습니다. 특히 권한 요청이 발생하지 않는 것도 테스트 대상입니다. 지도 기능이 없는 프로파일에서 위치 권한 팝업이 뜨지 않는지 확인해야 합니다.

  • feature flag가 false인 기능은 권한 요청이 발생하지 않는지 확인한다.
  • feature flag가 true인 기능만 permission policy를 갖는지 확인한다.
  • onFeatureEnter 정책은 실제 기능 진입 시점에만 요청되는지 확인한다.
  • 권한 거부 시 fallbackRoute로 이동하는지 확인한다.
  • Android/iOS adapter가 같은 결과 모델을 반환하는지 확인한다.
  • route guardpermission policy가 충돌하지 않는지 확인한다.

처음부터 모든 조합을 자동화하기는 어렵습니다. 그래도 hasFeature(), getPermissionPolicy(), canAccessRoute()처럼 작은 helper부터 테스트해두면 회귀를 줄이는 데 도움이 될 것 같습니다.

자주 하는 실수

1. 앱 시작 시 모든 권한을 한 번에 요청하는 경우가 있습니다. 사용자가 아직 기능을 쓰지도 않았는데 위치, 알림, 카메라 권한을 묻는 흐름은 부담스럽습니다. 권한은 가능한 한 기능 사용 맥락에 맞춰 요청하는 편이 좋습니다.

2. feature flag만 보고 권한 정책을 자동으로 가정하는 경우도 있습니다. map: true여도 현재 위치 기반 지도가 아니라 단순 지도 보기라면 위치 권한이 필요하지 않을 수 있습니다.

3. 권한 거부를 예외 상황으로만 보는 경우가 있습니다. 권한 거부는 정상적인 사용자 선택입니다. 안내 화면, 설정 이동, 대체 기능을 준비해야 합니다.

4. 권한 요청 정책을 보안 권한과 혼동하는 경우도 있습니다. 단말 권한은 카메라, 위치, 알림 같은 기기 기능 접근이고, 서버 데이터 권한은 API 인증과 사용자 권한 검증으로 처리해야 합니다.

결론

React Native 통합 앱에서 권한 요청 정책은 프로파일별 기능 차이와 함께 설계해야 합니다. 모든 앱에서 같은 권한을 요청하면 사용자 경험이 나빠지고, 불필요한 심사 리스크도 생길 수 있습니다.

좋은 구조는 feature flag, permission policy, route guard를 분리하되 서로 연결하는 것입니다. feature flag는 기능 사용 여부를 정하고, permission policy는 필요한 단말 권한과 요청 시점을 정하며, route guard는 해당 화면에 접근해도 되는지 확인합니다.

처음에는 권한 요청 코드를 한 곳에 몰아두는 방식이 쉬워 보일 수 있습니다. 하지만 프로파일이 늘어나는 통합 앱이라면 기능 진입 시점에 필요한 권한만 요청하고, 거부 시 fallback UX를 준비하는 방식이 더 안정적인 것 같습니다.

참고 자료

  • React Native 권한 처리 관련 라이브러리 문서
  • Android 런타임 권한과 알림 권한 공식 문서
  • iOS Privacy 권한 안내와 설정 이동 관련 문서
  • 프로젝트 내부 profile registry, feature flag, permission policy 문서