개발 노트/React Native

React Native 통합 앱 프로파일별로 단말기 권한 분리해서 요청하기

pposooj 2026. 5. 20. 14:21

하나의 코드베이스로 여러 개의 앱 프로파일을 분리해서 빌드하다 보면 꼭 권한 문제가 발목을 잡습니다. 어떤 앱은 지도가 들어가서 위치 권한이 필요한데, 어떤 앱은 알림 기능만 있으면 되거든요.

처음에는 귀찮아서 공통 초기화 함수 하나 만들고 앱 켤 때 필요한 권한을 한꺼번에 다 띄우도록 짜놓기 쉽습니다. 저도 처음엔 그렇게 편하게 가려 했습니다. 하지만 실제로 앱을 운영하고 스토어 심사를 넣다 보면 바로 문제가 터집니다. 지도가 없는 앱인데 위치 권한을 물어보니 사용자는 당황하고, 구글이나 애플 심사에서도 불필요한 권한 요구로 리젝트 먹기 딱 좋습니다.

나중에 제가 다시 보거나 작업할 때 까먹지 않으려고, 기능 플래그(Feature Flag)와 권한 정책(Permission Policy)을 분리하고 기능 진입 시점에만 권한을 요청하도록 구조를 바꾼 내용을 정리해 둡니다.

1. 헷갈리지 말아야 할 점: Feature Flag vs Permission Policy

제일 먼저 구조를 잡을 때 짚고 넘어갈 건 기능 플래그와 권한 정책은 엄연히 다르다는 점입니다. 이 둘을 하나로 묶어서 생각하면 코드가 꼬이기 시작합니다.

  • Feature Flag: 특정 프로파일(앱)에서 이 기능을 쓸지 말지 결정 (map: true, push: false)
  • Permission Policy: 해당 기능이 켜졌을 때, 실제 단말기의 어떤 권한이 필요한지 정의 (지도 -> 위치 권한)

예를 들어 map: true라고 해서 앱이 켜지자마자 위치 권한 팝업을 띄울 필요는 없습니다. 사용자가 실제로 지도 메뉴에 진입할 때(onFeatureEnter) 띄우는 게 자연스럽고, 만약 다른 프로파일에서 map: false라면 위치 권한 요청 코드가 아예 실행조차 안 되어야 안전합니다.

2. 기본 구조: Permission Policy 정의하기

저의 경우, 아래처럼 TypeScript로 기능과 권한, 요청 시점과 거부 시 이동할 화면(Fallback UX)을 명시적으로 묶어서 관리합니다. 이렇게 분리해 둬야 프로파일이 늘어나도 설정값만 바꿔서 대응이 가능합니다.

TypeScript
 
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',
      },
    ],
  },
};

기능을 켰다고 해서 필요한 권한이 자동으로 따라오게 간주하면 안 됩니다. 같은 지도 기능이라도 현재 위치를 찍어주는 앱이 있고 그냥 고정된 주소만 보여주는 앱이 있을 수 있으므로, 기능과 권한 정책을 명확하게 분리해 주는 편이 장기적으로 안전합니다.

3. 핵심 로직: 기능 진입 시점에 권한 체크하기

이제 사용자가 특정 기능이나 화면에 진입할 때 공통으로 태울 체크 로직입니다. 기능 활성화 여부를 먼저 보고, 그 다음에 연결된 권한 정책을 검사합니다.

TypeScript
 
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>,
) {
  // 1. 기능 자체가 꺼져 있으면 진입 차단
  if (!hasFeature(profile, feature)) {
    return { allowed: false, reason: 'FEATURE_DISABLED' as const };
  }

  // 2. 권한 정책 확인 (진입 시점 요청 정책이 아니면 그대로 통과)
  const policy = getPermissionPolicy(profile, feature);
  if (!policy || policy.requestTiming !== 'onFeatureEnter') {
    return { allowed: true, reason: 'NO_RUNTIME_PERMISSION_REQUIRED' as const };
  }

  // 3. 실제 단말기 권한 요청 실행
  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 };
}

4. 플랫폼 차이는 어댑터(Adapter)로 감싸기

React Native를 쓰다 보면 iOS와 Android의 권한 처리 방식이나 버전별(예: Android 13 이상 푸시 알림 권한 등) 세부 대응 때문에 화면단 코드가 지저분해지기 쉽습니다. 각 화면 컴포넌트 내부에서 Platform.OS 분기문을 난사하지 말고, 아래처럼 어댑터 패턴으로 한 번 묶어서 처리하는 것이 깔끔합니다.

TypeScript
 
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;
}

이렇게 추상화해 두면 나중에 react-native-permissions 같은 외부 라이브러리 인터페이스가 바뀌거나 OS 단에서 새로운 권한 정책이 추가되어도 어댑터 파일 한 곳만 수정하면 되기 때문에 유지보수가 아주 수월해집니다.

5. 안전장치: 라우트 가드(Route Guard)와 연동

기능 플래그와 권한 정책을 확실히 고정하려면 라우터(네비게이터) 진입 단계에서 1차로 가드를 세워주는 구조가 좋습니다.

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

기능 플래그가 꺼진 프로파일은 라우트 접근 자체를 원천 차단하고, 기능 플래그가 켜진 프로파일만 화면 진입을 허용한 뒤 앞서 만든 enterFeature 로직을 통해 필요한 시점에 단말 권한을 묻는 2단계 방어벽을 치는 셈입니다.

6. 실제 운영하며 느낀 주의할 점 세 가지

실제로 멀티 프로파일 기반으로 앱들을 빌드하고 마켓 검수를 진행하면서 겪은 주의점들입니다.

1. 권한 거부는 에러 상황이 아니라 정상적인 사용자 흐름입니다 사용자가 권한을 거부하는 것은 당연한 선택지 중 하나입니다. 이때 앱이 튕기거나 먹통이 되면 안 되고, "현재 위치 주변 장소를 확인하려면 위치 권한이 필요합니다"처럼 짧고 명확한 안내 화면(Fallback UX)을 보여주거나, 시스템 설정 화면으로 유도하는 재시도 동선을 반드시 준비해 두어야 합니다.

2. 테스트할 때 '안 뜨는 것'도 교차 검증해야 합니다 수동 테스트나 회귀 테스트를 할 때 기능이 잘 작동하는지만 보기 쉬운데, 기능 플래그가 꺼진 프로파일에서 불필요한 권한 팝업이 절대 뜨지 않는지를 반드시 체크해야 합니다. 안 쓰는 권한 팝업이 슥 올라오는 순간 스토어 심사 리젝트 사유가 됩니다. hasFeature(), getPermissionPolicy() 같은 작은 헬퍼 함수부터 단위 테스트를 묶어두면 실수를 줄일 수 있습니다.

3. 하드코딩된 테스트 데이터 유출을 주의하세요 프로파일별로 권한이나 흐름을 테스트하다 보면 Mock 데이터나 테스트 fixture에 사용자 식별자, 실제 개발 서버 URL, 내부 설정값 등을 임시로 적어두고 깜빡하는 경우가 있습니다. 이런 값들이 소스코드나 문서에 남지 않도록 환경 변수나 빌드 타임 주입 방식을 고수해야 안전합니다.

마무리

처음에는 그냥 권한 요청 코드를 한 곳에 몰아넣고 앱 시작할 때 한 번에 처리하는 게 세상 편해 보입니다. 하지만 프로파일이 늘어나고 장기 운영 체제로 접어들면 결국 기능 진입 시점에 맞추어 철저히 분리하는 구조가 훨씬 안전하고 에러가 적습니다.

유사한 형태로 멀티 프로파일 whitelabel 앱 구조를 고민 중이신 분들께 도움이 되었으면 좋겠습니다.