React Native CLI에서 권한 관리는 네이티브(iOS/Android) 설정이 직접 들어가야 해서 처음엔 조금 까다롭게 느껴질 수 있다. 하지만 실무에서는 거의 표준처럼 쓰이는 라이브러리가 있어서, 패턴만 익히면 아주 깔끔하게 구현할 수 있다. 가장 많이 사용하는 react-native-permissions 라이브러리를 활용한 구현 방법을 정리해보자.
pnpm install react-native-permissions
권한은 자바스크립트 코드만 작성한다고 동작하지 않는다. 각 OS의 설정 파일에 "이 앱이 왜 이 권한을 써야 하는지" 명시해야 한다.
setup_permissions([
'AppTrackingTransparency', # 앱 추적 투명성
'LocationAccuracy', # 정확한 현재 위치
'LocationAlways', # 항상 위치 접근
'LocationWhenInUse', # 사용 중일 때 위치 접근
'PhotoLibrary', # 사진 라이브러리
'PhotoLibraryAddOnly', # 사진 라이브러리 추가
'Camera', # 카메라
])cd ios && pod install 을 실행해야한다.<key>NSCameraUsageDescription</key>
<string>사진 촬영을 위해 카메라 권한이 필요합니다.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>현재 위치를 확인하기 위해 위치 권한이 필요합니다.</string><manifest> 태그 안에 권한을 추가하면 된다.권한 요청 로직을 컴포넌트마다 작성하지 않고, 별도의 유틸리티 함수로 분리해서 사용하면 베스트다. 사용자가 권한을 영구적으로 거절(BLOCKED)했을 때 설정 창으로 유도하는 로직까지 포함된 실전형 코드를 살펴보자.
import { Platform, Alert } from 'react-native';
import { check, request, PERMISSIONS, RESULTS, openSettings, Permission } from 'react-native-permissions';
// 1. 지원할 권한 타입 정의
export type AppPermission = 'camera' | 'location' | 'photo';
// 2. OS 및 버전에 따른 권한 매핑 함수
const getPermissionType = (type: AppPermission): Permission | undefined => {
if (Platform.OS === 'ios') {
switch (type) {
case 'camera': return PERMISSIONS.IOS.CAMERA;
case 'location': return PERMISSIONS.IOS.LOCATION_WHEN_IN_USE;
case 'photo': return PERMISSIONS.IOS.PHOTO_LIBRARY;
}
}
if (Platform.OS === 'android') {
switch (type) {
case 'camera': return PERMISSIONS.ANDROID.CAMERA;
case 'location': return PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
case 'photo':
// Android 13(API 33) 이상 분기 처리 (매우 중요)
return Number(Platform.Version) >= 33
? PERMISSIONS.ANDROID.READ_MEDIA_IMAGES
: PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
}
}
return undefined;
};
// 3. 권한 요청 메인 함수
export const requestAppPermission = async (type: AppPermission): Promise<boolean> => {
const permissionToRequest = getPermissionType(type);
if (!permissionToRequest) {
console.warn('해당 플랫폼에서 지원하지 않는 권한입니다.');
return false;
}
try {
const checkResult = await check(permissionToRequest);
if (checkResult === RESULTS.GRANTED) return true;
const requestResult = await request(permissionToRequest);
if (requestResult === RESULTS.GRANTED) return true;
// 권한 거절 또는 다시 묻지 않음(BLOCKED) 상태일 때 설정 창 유도
if (requestResult === RESULTS.BLOCKED || requestResult === RESULTS.DENIED) {
const title = type === 'photo' ? '사진첩' : type === 'camera' ? '카메라' : '위치';
Alert.alert(
`${title} 권한 필요`,
`서비스 이용을 위해 ${title} 권한이 필요합니다. 설정에서 권한을 허용해주세요.`,
[
{ text: '취소', style: 'cancel' },
{ text: '설정으로 이동', onPress: () => openSettings() },
]
);
}
return false;
} catch (error) {
console.error(`[${type}] 권한 요청 중 에러 발생:`, error);
return false;
}
};
아래와 같이 컴포넌트에서 사용할 수 있다.
import React, { useState } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
ActivityIndicator,
} from 'react-native';
import { requestAppPermission, AppPermission } from '../utils/permissions';
type PermissionStatus = 'idle' | 'granted' | 'denied' | 'loading';
interface PermissionItem {
type: AppPermission;
label: string;
icon: string;
description: string;
}
const PERMISSION_LIST: PermissionItem[] = [
{
type: 'camera',
label: '카메라',
icon: '📷',
description: '사진 촬영 및 QR 스캔에 필요합니다.',
},
{
type: 'location',
label: '위치',
icon: '📍',
description: '주변 매장 및 길찾기에 필요합니다.',
},
{
type: 'photo',
label: '사진첩',
icon: '🖼️',
description: '프로필 사진 업로드에 필요합니다.',
},
];
export default function PermissionRequestScreen() {
const [statuses, setStatuses] = useState<Record<AppPermission, PermissionStatus>>({
camera: 'idle',
location: 'idle',
photo: 'idle',
});
const handleRequest = async (type: AppPermission) => {
setStatuses(prev => ({ ...prev, [type]: 'loading' }));
const granted = await requestAppPermission(type);
setStatuses(prev => ({ ...prev, [type]: granted ? 'granted' : 'denied' }));
};
const handleRequestAll = async () => {
for (const { type } of PERMISSION_LIST) {
await handleRequest(type);
}
};
const getStatusStyle = (status: PermissionStatus) => {
switch (status) {
case 'granted': return styles.statusGranted;
case 'denied': return styles.statusDenied;
default: return styles.statusIdle;
}
};
const getStatusText = (status: PermissionStatus) => {
switch (status) {
case 'granted': return '허용됨 ✓';
case 'denied': return '거부됨 ✗';
case 'loading': return '확인 중...';
default: return '미설정';
}
};
const allGranted = Object.values(statuses).every(s => s === 'granted');
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>앱 권한 설정</Text>
<Text style={styles.subtitle}>
원활한 서비스 이용을 위해{'\n'}아래 권한을 허용해주세요.
</Text>
</View>
<View style={styles.list}>
{PERMISSION_LIST.map(({ type, label, icon, description }) => {
const status = statuses[type];
return (
<View key={type} style={styles.card}>
<View style={styles.cardLeft}>
<Text style={styles.icon}>{icon}</Text>
<View>
<Text style={styles.permissionLabel}>{label}</Text>
<Text style={styles.permissionDesc}>{description}</Text>
</View>
</View>
<View style={styles.cardRight}>
<Text style={[styles.statusText, getStatusStyle(status)]}>
{getStatusText(status)}
</Text>
{status !== 'granted' && (
<TouchableOpacity
style={[styles.button, status === 'loading' && styles.buttonDisabled]}
onPress={() => handleRequest(type)}
disabled={status === 'loading'}
>
{status === 'loading' ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.buttonText}>허용</Text>
)}
</TouchableOpacity>
)}
</View>
</View>
);
})}
</View>
<View style={styles.footer}>
{allGranted ? (
<View style={styles.allGrantedBadge}>
<Text style={styles.allGrantedText}>✅ 모든 권한이 허용되었습니다!</Text>
</View>
) : (
<TouchableOpacity style={styles.allButton} onPress={handleRequestAll}>
<Text style={styles.allButtonText}>모든 권한 한 번에 허용</Text>
</TouchableOpacity>
)}
</View>
</SafeAreaView>
);
}