React & React Native/이해하기

[RN] React Native Android 12 알림 권한 체크 및 권한 요청 방법 : react-native-permission

adjh54 2024. 4. 5. 10:11
728x170
해당 글에서는 안드로이드 12 이하 버전에서 디바이스 알림 권한 체크가 안됨을 확인하였습니다. 이에 따라 이를 대체하는 방법에 대해 알아봅니다.



1) 현상


 

1. 개발 현상 파악


💡 개발 현상 파악

- 기존 ‘안드로이드 13’이상 버전에서 사용되던 알람에 대한 권한이 ‘안드로이드 12’ 내에서는 POST_NOTIFICATIONS 속성을 이용하여 알림에 대한 권한을 체크를 하였을 때 이에 대해 통제가 되지 않는 현상이 발생하였습니다.

 

 

💡 [참고] 권한에 대한 정보

- 안드로이드 13이상 버전에서는 ‘POST_NOTIFICATIONS’ 권한을 이용하여서 알람 허용에 대한 권한을 체크하고 있었습니다.

 

 

종류 설명 Andriod 키 값 
notification 푸시 알림 사용 권한 POST_NOTIFICATIONS
 

[RN] 앱 접근 권한 관리 이해하고 설정하기: react-native-permissions

해당 글에서는 앱의 접근 권한을 관리하는 라이브러리인 react-native-permissions에 대해 이해를 돕기 위해 작성한 글입니다. 1) react-native-permissions 💡 react-native-permissions 란? - React Native 앱에서 권한을

adjh54.tistory.com

 

 

 

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’ 상태는 아래와 같이 ‘기능 자체를 디바이스에서 사용이 불가능함’을 알수 있습니다.

https://github.com/zoontek/react-native-permissions

 

 

 

 💡 [참고] Andriod 12, Andriod 13의 알람 권한에 대한 확인

- 우선적으로 안드로이드 13 이상 버전의 경우는 아래의 문서와 같이 알림에 대한 권한이 중지된 상태로 시작이 됩니다.
- 즉 안드로이드 12에서는 앱을 설치하면 앱에 대한 알림 권한이 켜진 상태가 되었지만, 안드로이드 13에서는 앱을 설치하여 실행하면 앱에 대한 알림 권한이 꺼진 상태로 시작이 됩니다.

https://developer.android.com/develop/ui/views/notifications/notification-permission?hl=ko#new-apps

 

 

 

2) 해결 방법


 

1. checkNotifications() 메서드를 이용하여 권한 확인


💡 checkNotifications() 메서드를 이용하여 권한 확인

- 해당 라이브러리 내에 checkNotifications() 함수를 지원해주고 있습니다. 이를 이용하여 ‘안드로이드 12 이하 버전’의 경우는 분기 처리를 하여 권한을 체크합니다.

https://github.com/zoontek/react-native-permissions?tab=readme-ov-file#checknotifications

 

 

 

 

💡 해당 함수에서는 안드로이드이며 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;

 

 

 

 

 

오늘도 감사합니다. 😀

 

 

 

그리드형