Android 개발/트러블슈팅

Android 17 Contact Picker로 갈아탄 자리 — READ_CONTACTS 막히기 전 마이그레이션 일지

stackD 2026. 7. 31. 18:00
반응형

 

연락처 전체 권한을 버리니까 오히려 코드가 깔끔해졌습니다. 권한 요청 다이얼로그, 거부 안내 UI, 설정 화면 유도 코드까지 한꺼번에 사라지더라고요.

 

지난주에 운영 중인 사이드 프로젝트 두 개를 안드로이드 17(Android 17)의 Contact Picker로 옮겨봤는데, 권한 처리 쪽에서만 300줄 가까이 빠졌습니다. 2026년 10월 28일부터 플레이 콘솔(Play Console) 정책이 강제되니까, 그때 부딪힌 지점들을 순서대로 풀어보겠습니다.

 

Photo Picker가 닦아둔 길, 이번엔 연락처 차례입니다

Scoped Storage(2020), Photo Picker(2023) 흐름을 따라가본 분이라면 이번 변화가 익숙하실 거예요. "전체 권한 → 사용자가 고른 항목에 임시 URI 발급" 모델이 이번엔 연락처에 들어왔습니다.

 

2026년 10월 28일부터 안드로이드 17(API 37) 이상을 타겟으로 하는 앱에 정책이 발효된다고 합니다. READ_CONTACTS 권한을 매니페스트에 선언하려면 백업, 다이얼러 같은 핵심 기능 앱이어야 하고, 그 외에는 플레이 콘솔에 Play 개발자 선언서(Developer Declaration)를 따로 내야 한다는 얘기입니다.

 

저처럼 "친구 초대" 정도 용도로 연락처를 쓰던 앱이라면 선언서 통과 가능성은 낮은 편이에요. 개인적으로는 그냥 Contact Picker로 갈아타는 쪽이 빠르다고 봅니다.

 

 

ACTION_PICK_CONTACTS로 필요한 필드만 골라 받기

Picker는 권한 선언 없이 Intent로 시스템 UI를 띄우고, 사용자가 고른 연락처에 대한 세션 단위 임시 URI를 돌려줍니다. 매니페스트에서 READ_CONTACTS 줄을 통째로 지울 수 있다는 뜻이에요.

 

요청 필드는 EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS로 좁힐 수 있는데, 이게 의외로 전환율에 영향을 줍니다. 전화번호만 필요한 화면이면 Phone.CONTENT_ITEM_TYPE만 요청해 두세요. 시스템 다이얼로그에 "전화번호 공유"가 명시되어서, 사용자가 망설이는 비율이 줄어들더라고요.

 

import android.content.Intent
import android.provider.ContactsContract.CommonDataKinds.Phone

val intent = Intent(Intent.ACTION_PICK_CONTACTS).apply {
    putStringArrayListExtra(
        EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
        arrayListOf(Phone.CONTENT_ITEM_TYPE)
    )
    putExtra(EXTRA_PICK_CONTACTS_SELECTION_LIMIT, 20)
}
contactPicker.launch(intent)

 

EXTRA_PICK_CONTACTS_SELECTION_LIMIT으로 다중 선택 상한선을 한 번에 잡을 수 있어요. 예전엔 전체 연락처를 가져와 앱 내에서 페이징하던 코드가 길었는데, 그 부분이 통째로 사라집니다.

 

 

기존 READ_CONTACTS 코드를 단계적으로 걷어낸 기록

크게 3단계로 끊어 갔습니다.

 

  1. 권한 요청 로직(requestPermissions), 거부 시 안내 UI, 설정 화면 유도 코드를 전부 삭제합니다.
  2. Picker가 돌려준 세션 URI(data.data)로 ContentResolver 쿼리를 다시 짭니다. 결과 수신 직후 한 번만 가능합니다.
  3. grep으로 레거시 ACTION_PICK 호출을 모두 찾아 Intent.ACTION_PICK_CONTACTS로 명시 교체합니다.

 

특히 3번이 무섭습니다. 안드로이드 17에서 기존 ACTION_PICK도 새 UI로 자동 전환되긴 하는데, 반환값이 세션 URI로 바뀌어서 옛 ContentResolver 쿼리 코드가 권한 만료로 조용히 터지는 케이스가 생길 수 있는 것으로 보입니다. 운영 앱이 새 OS 기기에서만 깨지는 사고로 이어질 수 있어서, 개인적으로는 3번을 가장 먼저 잡으시라고 권합니다.

 

 

세션 URI 만료와 듀얼 경로에서 챙길 예외 처리

세션 URI는 앱 프로세스가 죽으면 같이 만료됩니다. SharedPreferences에 URI 문자열을 저장해뒀다가 다음에 다시 꺼내 쓰는 코드가 있다면, 그건 전부 못 쓴다고 보시면 되겠습니다.

 

지속적으로 동기화해야 하는 화면이라면, Picker 결과를 받자마자 이름·전화번호·이메일을 앱 DB에 스냅샷으로 저장해야 해요. 저는 Room 테이블 하나를 따로 파서 Picker 콜백 안에서 INSERT OR REPLACE로 넣고 있습니다. URI는 휘발성이고, 우리가 들고 있는 건 그 시점의 데이터 사본일 뿐이라는 점만 기억하면 헷갈리지 않더라고요.

 

안드로이드 16 이하는 공식 백포트가 없는 상황입니다(2026-06 기준). Build.VERSION.SDK_INT로 듀얼 경로를 굴려야 하는데, 호출부가 두 갈래로 갈라지지 않게 공통 인터페이스로 추상화해두는 쪽이 나았어요.

 

사용자 취소(RESULT_CANCELED)도 새로 정의해야 합니다. 권한 거부 다이얼로그가 없는 대신, 사용자가 Picker에서 그냥 뒤로 가기로 나오는 케이스가 정상 흐름으로 들어옵니다. "0건 선택했어요" 정도의 부드러운 안내 카피로 바꿔두시는 게 좋아요.

 

10/28 데드라인 전에 끝내둘 마이그레이션 체크리스트

오늘 기준 D-130입니다. 정책 적용까지 4개월 조금 넘게 남았어요. 일정을 역산해보면 3구간으로 끊어 가는 쪽이 안전합니다.

 

1. D-130 (지금): 영향 범위 분석

 

READ_CONTACTS 사용 지점, ContentResolver로 연락처를 긁어오는 모든 호출, ACTION_PICK 잔재를 전부 색출합니다. 예전에 누가 짜놓고 잊힌 백오피스 흐름이 의외로 많이 걸리거든요. 공수 산정은 이 단계가 끝난 다음에 잡으셔야 정확합니다.

 

2. D-90 (7월 말): 듀얼 코드 경로 + 회귀 테스트

 

안드로이드 16 이하 경로와 17 경로를 동시에 굴리는 단계입니다. 특히 제조사별 커스텀 연락처 앱의 다중 선택 동작이 표준과 다른 경우가 있어요. 삼성, 샤오미, 픽셀 최소 3종은 실기기에서 잡아보시는 걸 추천합니다.

 

3. D-30 (9월 말): Play 개발자 선언서 또는 정책 사전 점검

 

핵심 기능에 해당돼서 권한을 유지하실 거라면 선언서를 9월 말까지 제출해두세요. 마감 직전엔 정책팀 응답이 늦어지는 경우가 많은 편이라, 일정 여유를 미리 확보해두는 쪽이 무난합니다. 일반 기능이라면 같은 시점에 Picker로 완전히 갈아탄 빌드를 내부 트랙에 올려두시면 됩니다.

 


이렇게 정리해두고 10월 말 어느 평일에 정책이 발효되는 장면을 그려보면 두 화면이 갈립니다. 한쪽 화면엔 플레이 콘솔 알림창에 "정책 위반으로 업데이트 거부" 문구가 떠 있을 거고, 다른 한쪽은 그날도 별일 없이 새 빌드를 그대로 올리고 있을 거예요. 그 차이를 만드는 건 결국 6월부터 10월 사이 어느 주말에 READ_CONTACTS 문자열을 IDE에서 한 번 검색해봤느냐 안 해봤느냐, 그 한 번의 차이로 갈리는 셈입니다.

 

 

반응형
LIST