반응형
해당 글에서는 안드로이드 12 이하 버전에서 디바이스 알림 권한 체크가 안됨을 확인하였습니다. 이에 따라 이를 대체하는 방법에 대해 알아봅니다.
1) 현상
1. 개발 현상 파악
💡 개발 현상 파악
- 기존 ‘안드로이드 13’이상 버전에서 사용되던 알람에 대한 권한이 ‘안드로이드 12’ 내에서는 POST_NOTIFICATIONS 속성을 이용하여 알림에 대한 권한을 체크를 하였을 때 이에 대해 통제가 되지 않는 현상이 발생하였습니다.
💡 [참고] 권한에 대한 정보
- 안드로이드 13이상 버전에서는 ‘POST_NOTIFICATIONS’ 권한을 이용하여서 알람 허용에 대한 권한을 체크하고 있었습니다.
종류 | 설명 | Andriod 키 값 |
notification | 푸시 알림 사용 권한 | POST_NOTIFICATIONS |
2. 문제 파악
💡 문제 파악-1
- 해당 문제에서는 안드로이드 12 이하에서는 POST_NOTIFICATIONS 속성으로 알람에 대한 권한 체크가 불가능함이 확인되었습니다.
- 아래와 같이 앱에 대한 여러 가지 권한을 체크하는 로직입니다. 해당 로직에서 ‘granted’ 상태가 아닌 경우에는 배열을 반환해 주는 형태입니다.
/**
* 권한 체크에서 모두 '허용(granted)'을 하였는지 여부를 체크합니다.
*
* @param {Permission[} permsCodeArr
* @returns {Promise<Permission[]>} 권한 중 '허용'을 모두 한 경우 빈 배열이며 '허용'을 하지 않으면 배열로 반환합니다.
*/
const cmmPermsArrCheck = async (permsCodeArr: Permission[]): Promise<Permission[]> => {
let notGrantedArr: Permission[] = [];
// [STEP1] 전달 받은 배열을 순회하면서 권한을 허용했는지 체크합니다.
for (let permsItem of permsCodeArr) {
// [STEP2] 동일한 플랫폼(andriod, ios)의 코드가 아니면 undefined가 발생합니다. 이를 제외하고 수행합니다.
if (permsItem != undefined) {
//[STEP3] react-native-permissions 함수를 이용해 권한을 체크합니다.
let permsCheck = await check(permsItem);
switch (permsCheck) {
// [CASE1] 권한 상태가 수락(granted) 상태인 경우
case "granted":
break;
// [CASE2] 권한 상태가 수락되지 않은 상태 : 배열을 저장합니다.
case "blocked":
case "denied":
case "limited":
case "unavailable":
notGrantedArr.push(permsItem);
break;
}
}
}
return notGrantedArr;
}
💡 아래와 같이 알람에 대한 권한에 대해 분기 처리 없이 POST_NOTIFICATIONS를 확인한 뒤 통과 여부를 반환 받는것을 확인하고 있습니다.
const _alarmPermission = APP_PERMISSION_CODE.alarm
const permisAlarmArr = await PermissionUtil.cmmPermsArrCheck([..._alarmPermission]);
if (permisAlarmArr.length === 0) isGrantedAlarm = true;
💡 안드로이드 12의 경우 해당 부분에 대한 권한 처리가 어떻게 되었는지 확인을 해보면 아래와 같이 ‘unavailable’ 상태가 됩니다. 앱에 대한 권한을 허용했음에도 아래와 같은 콘솔이 출력되고 있습니다.
💡 권한을 요청했음에도 ‘unavailable’ 상태는 아래와 같이 ‘기능 자체를 디바이스에서 사용이 불가능함’을 알수 있습니다.
💡 [참고] Andriod 12, Andriod 13의 알람 권한에 대한 확인
- 우선적으로 안드로이드 13 이상 버전의 경우는 아래의 문서와 같이 알림에 대한 권한이 중지된 상태로 시작이 됩니다.
- 즉 안드로이드 12에서는 앱을 설치하면 앱에 대한 알림 권한이 켜진 상태가 되었지만, 안드로이드 13에서는 앱을 설치하여 실행하면 앱에 대한 알림 권한이 꺼진 상태로 시작이 됩니다.
2) 해결 방법
1. checkNotifications() 메서드를 이용하여 권한 확인
💡 checkNotifications() 메서드를 이용하여 권한 확인
- 해당 라이브러리 내에 checkNotifications() 함수를 지원해주고 있습니다. 이를 이용하여 ‘안드로이드 12 이하 버전’의 경우는 분기 처리를 하여 권한을 체크합니다.
💡 해당 함수에서는 안드로이드이며 12 이하 버전의 경우 checkNotifications() 함수를 통해 status 상태를 확인합니다.
const devicesOS = DeviceInfo.getSystemName(); // 디바이스의 시스템 이름을 반환받습니다.(Android or iOS)
const devicesOsVer = parseInt(DeviceInfo.getSystemVersion()); // 디바이스의 OS 버전을 확인합니다.
if (deviceOS === "Android" && deviceOsVer < 13) {
await checkNotifications()
.then(({ status, settings }) => {
console.log("안드로이드 12 버전의 경우 알람 권한 체크 상태 :: ", status)
});
}
1.1. 결과 확인
💡 결과 확인
- 안드로이드 12 버전의 경우 POST_NOTIFICATIONS로 권한 체크를 했던 부분은 여전히 ‘unavailable’이 출력이 되었지만 checkNotifications()를 이용하여 체크하였을 시 권한 허용(granted)나 권한 차단(blocked)이 출력이 잘됨을 확인되었습니다.
2. 알람에 대한 요청처리 : 설정 페이지로 이동
💡 알람에 대한 요청처리 : 설정 페이지로 이동
- 안드로이드 12 이하에서는 알람 속성 인 ‘POST_NOTIFICATIONS’에 대해 사용할 수 없으므로 권한을 확인하면 설정하는 페이지로 이동하여 알람을 허용하도록 해야합니다.
💡 toggleAlarm
1. 안드로이드 12 이하일 때의 버전처리
- react-native-permissions 라이브러리의 checkNotifications() 메서드를 호출하여 권한을 한번 더 체크합니다.
- 만약 권한이 허용상태가 아니면 Linking.openSettings()를 통해서 설정 페이지로 이동시킵니다.
2. 안드로이드 13 이상일 때의 버전 처리
- android.permission.POST_NOTIFICATIONS 권한에 따라 권한을 체크하고 각각에 따라 처리를 수행합니다.
/**
* 알람 스위치를 눌렀을때의 동작 처리
*/
const toggleAlarm = async () => {
const devicesOS = DeviceInfo.getSystemName(); // 디바이스의 시스템 이름을 반환받습니다.(Android or iOS)
const devicesOsVer = parseInt(DeviceInfo.getSystemVersion()); // 디바이스의 OS 버전을 확인합니다.
// 안드로이드 일 경우 수행합니다.
if (devicesOS === "Android") {
// [CASE1] Andriod 12 버전인 경우 : 다시한번 권한을 체크하고 권한 허용이 안된 경우 핸드폰 상 설정 페이지를 띄어준다.
if (devicesOsVer < 13) {
await checkNotifications()
.then(({ status, settings }) => {
if (status === "granted") {
setPermisStateObj({ camera: permisStateObj.camera, alarm: !permisStateObj.alarm });
} else {
Linking.openSettings(); // 핸드폰 상 설정 페이지
setPermisStateObj({ camera: permisStateObj.camera, alarm: !permisStateObj.alarm, });
}
});
}
// [CASE2] Android 13 이상 버전인 경우 : POST_NOTIFICATIONS 를 이용하여 권한 체크
else {
await requestMultiple(["android.permission.POST_NOTIFICATIONS"])
.then((resPremissInfo) => {
const resultStatus = resPremissInfo["android.permission.POST_NOTIFICATIONS"];
if (resultStatus === "granted") {
setPermisStateObj({ camera: permisStateObj.camera, alarm: !permisStateObj.alarm });
}
else {
Linking.openSettings(); // 핸드폰 상 설정 페이지
}
});
}
}
}
3. 설정 페이지 이동 후 Switch 버튼 처리방법 : AppState 활용
💡 설정 페이지 이동 후 Switch 버튼 처리방법 : AppState 활용
- 설정 페이지에서 권한을 ‘허용’한 뒤에 앱으로 돌아왔을 때, Switch 버튼이 움직어야 합니다.
- 그때 AppState를 이용하여 앱으로 다시 복귀했을 때 권한을 체크하여서 permisStateObj의 State를 갱신해 주어서 Switch를 on으로 변경 처리하기 위한 방법입니다.
import React, { useEffect, useRef, useState } from "react";
import { AppState, Linking, Switch, Text, View } from "react-native";
import DeviceInfo from "react-native-device-info";
import { checkNotifications, requestMultiple } from "react-native-permissions";
const TestComponent = () => {
// 현재의 상태를 알려주는것
const appState = useRef(AppState.currentState);
// 권한의 종류 및 상태 관리
const [permisStateObj, setPermisStateObj] = useState({
camera: false, // 카메라 권한
alarm: false // 알람 권한
})
/**
* 앱이 foregroud / background인 경우에 따라서 처리
*/
useEffect(() => {
// 사용자가 앱의 상태가 변경 되었을 경우 실행이 된다.
const appStateListener = AppState.addEventListener('change', handleAppStateChange);
return () => {
// 리스너 수행 이후 소멸시킵니다.
return appStateListener.remove();
}
}, [])
/**
* 앱 권한 팝업이 열려 있을때 수행이 됩니다.
* '알람' 권한을 처리하며 설정에서 해당 권한 처리를 변경한 이후에 State를 갱신시켜줍니다.
* @param {any} nextAppState
* @return {Promise<void>}
*/
const handleAppStateChange = async (nextAppState: any): Promise<void> => {
console.log("appState.current ::: ", appState?.current);
console.log("nextAppState ::: ", nextAppState)
// 설정 이후 다시 앱으로 들어오는 경우 권한을 다시 한번 체크해서 갱신한다.
if (appState.current === "active" && nextAppState === "active") {
// 해당 값이 존재하지 않으면 권한이 허용된 상태, 아니면 권한 미 허용
const permisCameraArr = await PermissionUtil.cmmPermsArrCheck([...APP_PERMISSION_CODE.camera])
// [STEP2] 카메라 권한 및 알람 권한을 체크합니다.
checkNotifications()
.then(({ status, settings }) => {
status === "granted"
? setPermisStateObj({ camera: permisCameraArr.length === 0, alarm: true })
: setPermisStateObj({ camera: permisCameraArr.length === 0, alarm: false })
});
}
}
return (
<View>
<View>
<View >
<Text >카메라 (필수)</Text>
<Switch
onValueChange={() => permissionHandler.toggleSwitch("camera")}
value={permisStateObj.camera}
/>
</View>
</View>
<View>
<View >
<Text >알람 (선택)</Text>
<Switch
onValueChange={() => permissionHandler.toggleSwitch("alarm")}
value={permisStateObj.alarm}
/>
</View>
</View>
</View>
)
}
3) 최종 소스코드
💡 최종 소스코드
- 해당 소스코드에서는 카메라와 알람에 대한 권한을 체크하는 예시입니다.
1. permissionHandler.checkPermission()
- 최초 앱이 실행되면 권한 체크를 합니다.
2. permissionHandler.toggleSwitch()
- 스위치를 변경하면 수행이 되며, 카메라와 알람에 대한 권한을 체크합니다.
3. handleAppStateChange()
- 앱의 상태에 따라서 수행하며 background 상태에서 foreground 상태로 돌아오는 경우에 수행이 됩니다.
import { APP_PERMISSION_CODE } from "common/utils/codes/CommonCode";
import DeviceInfoUtil from "common/utils/DeviceInfoUtil";
import PermissionUtil from "common/utils/PermissionUtil";
import React, { useEffect, useRef, useState } from "react";
import { AppState, Linking, Switch, Text, View } from "react-native";
import DeviceInfo from "react-native-device-info";
import { checkNotifications, requestMultiple } from "react-native-permissions";
const TestComponent = () => {
// 현재의 상태를 알려주는것
const appState = useRef(AppState.currentState);
// 권한의 종류 및 상태 관리
const [permisStateObj, setPermisStateObj] = useState({
camera: false, // 카메라 권한
alarm: false // 알람 권한
})
/**
* 앱이 foregroud / background인 경우에 따라서 처리
*/
useEffect(() => {
// [STEP1] 권한을 체크합니다.
permissionHandler.checkPermission();
// 사용자가 앱의 상태가 변경 되었을 경우 실행이 된다.
const appStateListener = AppState.addEventListener('change', handleAppStateChange);
return () => {
// 리스너 수행 이후 소멸시킵니다.
return appStateListener.remove();
}
}, [])
/**
* 앱 권한 팝업이 열려 있을때 수행이 됩니다.
* '알람' 권한을 처리하며 설정에서 해당 권한 처리를 변경한 이후에 State를 갱신시켜줍니다.
* @param {any} nextAppState
* @return {Promise<void>}
*/
const handleAppStateChange = async (nextAppState: any): Promise<void> => {
console.log("appState.current ::: ", appState?.current);
console.log("nextAppState ::: ", nextAppState)
// 설정 이후 다시 앱으로 들어오는 경우 권한을 다시 한번 체크해서 갱신한다.
if (appState.current === "active" && nextAppState === "active") {
// 해당 값이 존재하지 않으면 권한이 허용된 상태, 아니면 권한 미 허용
const permisCameraArr = await PermissionUtil.cmmPermsArrCheck([...APP_PERMISSION_CODE.camera])
// [STEP2] 카메라 권한 및 알람 권한을 체크합니다.
checkNotifications()
.then(({ status, settings }) => {
status === "granted"
? setPermisStateObj({ camera: permisCameraArr.length === 0, alarm: true })
: setPermisStateObj({ camera: permisCameraArr.length === 0, alarm: false })
});
}
}
/**
* 디바이스 권한을 관리하는 Handler입니다.
*/
const permissionHandler = (() => {
const _cameraPermission = APP_PERMISSION_CODE.camera
const _alarmPermission = APP_PERMISSION_CODE.alarm
const deviceOS = DeviceInfo.getSystemVersion();
const deviceOsVer = parseInt(DeviceInfo.getSystemVersion())
return {
/**
* 최초 앱 접근 시 디바이스의 접근 권한을 체크합니다.
*/
checkPermission: async () => {
let isGrantedCamera = false;
let isGrantedAlarm = false;
// [STEP1] 카메라 권한을 체크합니다.
const permisCameraArr = await PermissionUtil.cmmPermsArrCheck([..._cameraPermission])
if (permisCameraArr.length === 0) isGrantedCamera = true;
// [STEP] 안드로이드 알람 권한을 12 이하와 13이상으로 분리하여 체크합니다.
if (deviceOS === "Android") {
// Android 12이하 버전인 경우 요청 처리 및 상태 변경
if (deviceOsVer < 13) {
await checkNotifications()
.then(({ status, settings }) => {
if (status === "granted") isGrantedAlarm = true;
});
}
// Andriod 13이상 버전인 경우 요청 처리 및 상태 변경
else {
const permisAlarmArr = await PermissionUtil.cmmPermsArrCheck([..._alarmPermission]);
if (permisAlarmArr.length === 0) isGrantedAlarm = true;
}
}
setPermisStateObj({ camera: isGrantedCamera, alarm: isGrantedAlarm });
},
/**
* 권한 허용에 대한 스위치를 관리합니다
* true로 바꿨을 경우 요청 팝업을 띄웁니다.
* false로 바꿨을때 처리 없음.
* @param toggleId
*/
toggleSwitch: async (toggleId: "camera" | "alarm") => {
// [CASE1] 카메라 권한 체크
if (toggleId === "camera") {
if (permisStateObj.camera === false) {
await requestMultiple([..._cameraPermission])
.then((resPremissInfo) => {
const resultStatus = resPremissInfo["android.permission.CAMERA"];
if (resultStatus === "granted") {
setPermisStateObj({
camera: !permisStateObj.camera,
alarm: permisStateObj.alarm
});
}
});
}
}
// [CASE2] 알람 권한 체크
else if (toggleId === "alarm") {
if (permisStateObj.alarm === false) {
const osVer = parseInt(DeviceInfoUtil.getDeviceOsVer())
if (osVer < 13) {
await checkNotifications()
.then(({ status, settings }) => {
if (status === "granted") {
setPermisStateObj({
camera: permisStateObj.camera,
alarm: !permisStateObj.alarm,
});
} else {
Linking.openSettings(); // 핸드폰 상 설정 페이지
setPermisStateObj({
camera: permisStateObj.camera,
alarm: !permisStateObj.alarm,
});
}
});
} else {
await requestMultiple(["android.permission.POST_NOTIFICATIONS"])
.then((resPremissInfo) => {
const resultStatus = resPremissInfo["android.permission.POST_NOTIFICATIONS"];
if (resultStatus === "granted") {
setPermisStateObj({
camera: permisStateObj.camera,
alarm: !permisStateObj.alarm,
});
}
else {
Linking.openSettings(); // 핸드폰 상 설정 페이지
}
});
}
} else {
Linking.openSettings(); // 핸드폰 상 설정 페이지
}
}
}
}
})();
return (
<View>
<View>
<View >
<Text >카메라 (필수)</Text>
<Switch
onValueChange={() => permissionHandler.toggleSwitch("camera")}
value={permisStateObj.camera}
/>
</View>
</View>
<View>
<View >
<Text >알람 (선택)</Text>
<Switch
onValueChange={() => permissionHandler.toggleSwitch("alarm")}
value={permisStateObj.alarm}
/>
</View>
</View>
</View>
)
}
export default TestComponent;
오늘도 감사합니다. 😀
반응형