사용자가 앱을 처음 깔았을 때, "앨범 접근 허용할까요?" 이런 팝업이 나타나죠? 이게 그냥 뜨는 게 아니에요. 개발자가 정교하게 설계한 흐름이 있어요. 오늘은 그 뒤에 숨은 "권한 요청" 로직을 풀어볼게요.
권한 상태값(PermissionStatus)이란?
앱이 특정 권한(카메라, 마이크, 위치, 사진첩 등)에 대해 요청할 때마다 상태값이 존재해요. 이 상태값에 따라 다음 액션이 결정되는 거죠.
안드로이드와 iOS, 왜 달라?
먼저 중요한 포인트: 안드로이드와 iOS는 권한 시스템이 완전히 다르다는 거예요.
🍎 iOS에만 있는 상태들
iOS는 권한의 세부 상태를 더 자세하게 분류해요.
상태 | 의미 | 예시 |
|---|---|---|
| 권한 허용됨 | 사용자가 "허용" 탭함 |
| 권한 거부됨 | 사용자가 "거부" 탭함 |
| 아직 요청 전 | 팝업이 한 번도 안 뜬 상태 |
| 시스템 차원에서 차단 | 자녀보호 설정, MDM(회사 기기) 정책 |
| 일부만 허용 | 사진첩에서 특정 사진만 선택해서 허용 |
🤖 안드로이드는 단순해요
안드로이드는 기술적 특성상, 앱이 권한을 요청한 적 없으면 그냥 denied로 취급해요. undetermined나 restricted 같은 상태는 없어요.
상태 | 의미 |
|---|---|
| 권한 허용됨 |
| 권한 거부됨 (또는 아직 요청 전) |
정리하면: iOS는 섬세하고, 안드로이드는 심플하다!
canAskAgain — 재요청이 가능한가?
사용자가 권한을 거부했을 때(denied), 바로 다시 물어볼 수 있을까요? 여기서 canAskAgain 플래그가 나타나요.
canAskAgain = true → 시스템 팝업 재요청 가능 (아직 기회 남음)
canAskAgain = false → 시스템 팝업 못 띄움 (사용자가 "다시 묻지 말기" 선택)
사용자가 팝업을 거부하고 "다시 묻지 마세요"를 눌렀을 때 canAskAgain이 false로 바뀌어요. 이때는 시스템 팝업을 다시 띄울 수 없고, 대신 설정 앱으로 유도해야 해요.
실전: 권한 요청 흐름도
권한 요청 시작
↓
권한 상태 확인 getPermissionAsync()
├─→ status = 'granted' → ✅ 기능 실행
│
├─→ status = 'denied'
│ ├─→ canAskAgain = true → 🔄 재요청 팝업 띄우기
│ └─→ canAskAgain = false → ⚙️ 설정 앱으로 유도
│
├─→ status = 'undetermined' → (iOS만) 커스텀 Alert 후 요청
│
└─→ status = 'limited' → (iOS만) 기능 부분 실행
실제 코드로 구현하면 이래요:
const { status, canAskAgain } = await requestPermissionsAsync(); if (status === 'granted') { // ✅ 권한 있음 → 기능 실행 } else if (canAskAgain) { // 🔄 재요청 가능 → 다시 한 번 물어보기 } else { // ⚙️ 재요청 불가 → 설정 앱 열기 Linking.openSettings(); }
권한을 요청하는 베스트 타이밍
"언제 권한 요청 팝업을 띄울까?" 이것도 중요한 결정이에요.
❌ 앱 시작할 때 바로 요청
앱을 깔자마자 "카메라 접근 허용?", "마이크 접근 허용?" 이렇게 터져 나오면?
-
사용자는 맥락이 없어서 일단 거부해요
-
거부율이 엄청 높아요
-
첫인상이 별로예요
✅ 기능을 사용하려는 순간에 요청
사용자가 "사진 올리기" 버튼을 눌렀을 때:
-
사용자가 왜 권한이 필요한지 이해해요
-
심리적 저항감이 훨씬 낮아요
-
승인율이 훨씬 높아요
당신이 유튜브를 깔았을 때, 바로 카메라를 달라고 하지 않죠? 라이브 방송을 하려고 탭했을 때 물어본다는 거예요.
iOS 특별 전략: Pre-permission 패턴
여기가 중요해요. iOS의 시스템 팝업은 한 번만 뜬다는 게 핵심이에요.
사용자가 처음 팝업에서 "거부"를 탭하면? 다시는 그 팝업이 안 떠요. 개발자가 어떻게 해도 못 띄워요. 시스템이 보호하는 거죠.
그래서 앱 개발자들은 이런 전략을 써요:
[Step 1] 커스텀 Alert 띄우기
"앨범 접근이 필요합니다"
사용자 선택 ↓
[Step 2-A] "허용할게요" 탭
→ 시스템 팝업 표시
[Step 2-B] "괜찮아요" 탭
→ 아무것도 안 함 (팝업 기회 보존!)
핵심: 커스텀 Alert에서 거부했으면, 시스템 팝업을 띄우지 않는다! 그럼 다음번에 또 물어볼 기회가 남잖아요.
코드로 보면
// iOS에서 상태가 'undetermined' (= 팝업 미표시 상태)라면 if (Platform.OS === 'ios' && status === 'undetermined') { // 커스텀 Alert를 먼저 띄운다 await new Promise<void>((resolve) => { Alert.alert( '사진 접근 안내', '게시물에 사진을 첨부하려면 앨범 접근이 필요합니다.', [ { text: '괜찮아요', style: 'cancel', onPress: () => resolve() // 아무것도 안 하고 종료 }, { text: '허용할게요', onPress: async () => { // 여기서만 시스템 팝업 띄운다 await ImagePicker.requestMediaLibraryPermissionsAsync(); resolve(); }, }, ], ); }); return; // 함수 끝 }
이게 왜 중요한가?
-
첫 번째 기회를 낭비하지 않아요
-
사용자가 거부했을 때 다시 물어볼 수 있어요
-
승인율을 높일 수 있어요
안드로이드는 어떻게?
안드로이드는 iOS와 다르게 권한을 여러 번 요청할 수 있어요. 그래서 iOS처럼 복잡한 전략이 필요 없어요.
대신 안드로이드는 이런 특성이 있어요:
-
사용자가 거부하고 "다시 묻지 말기" 선택 →
canAskAgain = false -
이때는 설정 앱으로 유도
-
근데 여러 번 요청할 수 있으니까, "지금은 필요 없나?"라고 물어보고 나중에 다시 물어볼 수도 있어요
권한 요청 플로우: 실전 체크리스트
앱에 권한 요청 로직을 추가할 때 이 순서로 확인하세요:
-
[ ] Step 1 — 사용자가 기능을 실제로 사용하려고 탭했는가? (앱 시작 시 아님)
-
[ ] Step 2 — 현재 권한 상태를 확인했는가? (
granted,denied,undetermined등) -
[ ] Step 3 — iOS인가? 그럼
undetermined상태에서 커스텀 Alert를 먼저 띄웠는가? -
[ ] Step 4 —
status === 'granted'라면 기능을 실행했는가? -
[ ] Step 5 —
canAskAgain === true라면 재요청 팝업을 띄웠는가? -
[ ] Step 6 —
canAskAgain === false라면 설정 앱으로 유도했는가? (Linking.openSettings())
마치며
권한 요청 흐름이 복잡하게 보이는 이유는, iOS와 안드로이드가 완전히 다른 철학을 갖고 있기 때문이에요.
-
iOS: 사용자 보호를 최우선 → 팝업 한 번만, 신중하게
-
안드로이드: 사용자 선택권 → 여러 번 물어볼 수 있어, 유연하게
둘 다 이해하고 대응할 수 있으면, 사용자 경험도 좋고 권한 승인율도 높은 앱을 만들 수 있어요.
다음 편에서는 권한 거부 후 사용자 재참여 전략을 다뤄볼게요! 👋
저자 소개
DevOps Engineer. 금융과 여행에 관심이 많습니다.