
얼마 전에 로그인 화면을 컴포즈로 옮기던 밤이었어요. 아이디 칸에 포커스를 줬는데 제안 바가 텅 빈 채로 아무것도 안 뜨더라고요. 로그를 까보니 요청 자체가 나가지도 않고 있었습니다.
XML로 짜여 있던 로그인 폼을 젯팩 컴포즈(Jetpack Compose)로 갈아끼운 순간 자동완성 칩이 사라진 거였어요. 처음엔 컴포즈를 의심했는데, 범인은 따로 있었습니다.
한 줄 요지: "컴포즈 한계"가 아니라 옮기면서 빠진 배선이 문제였고, 자동완성이 컴포즈엔 두 갈래로 존재한다는 걸 몰랐던 게 핵심이었습니다.
아래에서 왜 침묵했는지, ui 1.12·androidx.credentials 1.5.0 기준으로 어떻게 연결하는지, API 34 폴백까지 순서대로 풀어볼게요.
컴포즈 로그인 화면에서 자동완성이 침묵한 자리
XML 시절엔 EditText에 android:autofillHints="username" 한 줄만 있으면 포커스 순간 저장된 아이디 칩이 떴습니다. 근데 같은 필드를 컴포즈 TextField로 바꾸니 포커스를 줘도 그냥 빈 채였어요.
더 골 때렸던 건, 비밀번호 필드에서 제안이 한 번 안 뜨기 시작하면 앱을 재시작하기 전까지 다른 필드까지 같이 먹통이 되던 현상이었습니다. 이건 제 환경에서 실제로 겪은 현상인데, 한 번 걸리면 재현이 꽤 끈질기더라고요.
여기서 제일 중요한 관찰이 하나 있었어요. 로그를 보니 "요청은 나갔는데 UI가 안 뜬" 게 아니라, 요청 트리거 자체가 없었습니다. 원인 후보를 좁히면 세 가지였어요.
- 필드 종류를 알려주는
contentType을 아예 안 넣어서, 시스템이 뭘 채워야 할지 모르는 경우. - 인라인 제안 자체가 안 뜨는, 제 환경에서 재현되던 그 먹통 현상에 걸린 경우.
- Credential Manager 경로를 쓰려면
GetCredentialRequest를 만들어 필드에 연결해야 하는데 그 배선이 없는 경우.

컴포즈 자동완성이 두 갈래로 나뉘어 있다는 사실
이게 대부분의 블로그가 안 짚는 진짜 정답인데요. 컴포즈에서 자동완성은 성격이 다른 두 개가 따로 놀고 있습니다.
1. contentType 시맨틱스 (패시브 방식)
필드에 "이건 아이디 칸이야"라고 종류만 표시해두는 방식이에요. 그러면 시스템의 자동완성 서비스(1Password나 구글 비밀번호 관리자 같은)가 알아서 붙습니다. 컴포즈 1.8.0(2025년 4월)에서 정식화됐고, 코드는 이렇게 짧습니다.
TextField(
value = username,
onValueChange = { username = it },
// 필드 종류만 시스템에 알려주는 패시브 방식
modifier = Modifier.semantics { contentType = ContentType.Username }
)
옛날에 쓰던 Modifier.autofill()이나 AmbientAutofill은 1.8.0에서 deprecated 됐습니다. "예전 API 계속 써도 되겠지" 하고 남겨두면 경고 뜨고 동작도 어긋날 수 있어요. 지금은 시맨틱스가 정답입니다.
2. credentialRequest / Credential Manager (액티브 방식)
이건 앱이 직접 "이런 자격 증명을 달라"는 요청을 조립해서, 포커스 순간 Credential Manager로 쏘는 방식이에요. 결과가 키보드 위 인라인 제안으로 뜹니다. 패스키까지 여기서 다뤄집니다.
여기서 흔한 오해가 하나 있어요. "contentType만 넣으면 패스키 자동완성까지 붙는다"는 건 아닙니다. 두 트리거는 완전히 별개예요. 첫 번째는 시스템 서비스에 의존하는 패시브, 두 번째는 앱이 직접 요청을 쏘는 액티브라서, 원하는 흐름에 맞춰 골라야 합니다.

credentialRequest 시맨틱스로 TextField에 신원 요청 배선하기
제가 침묵의 원인으로 지목한 "빠진 배선"이 바로 이 두 번째 갈래였습니다.
androidx.credentials 1.5.0에는 View용으로 PendingGetCredentialRequest라는 게 있어요. GetCredentialRequest(무엇을 요청할지)와 응답 콜백을 묶은 구조입니다. XML 뷰에선 setPendingGetCredentialRequest로 이걸 필드에 걸어줬는데, 컴포즈로 옮기면서 여기에 대응하는 시맨틱스 연결을 제가 안 해준 거였어요.
흐름 자체는 이렇게 그려집니다.
- 미리
GetCredentialRequest를 만듭니다 (비밀번호 옵션 + 패스키 옵션 조합). - 그 요청을 특정
TextField의 시맨틱스에 결합합니다. - 포커스가 들어오면 요청이 Credential Manager로 전송됩니다.
- 결과가 키보드 인라인 제안으로 노출됩니다.
다만 여기서 솔직하게 짚어야 할 게 있어요. 이 credentialRequest 시맨틱스 연결 API는 ui 1.12 / credentials 1.5.0 근처에서 정식으로 넘어오는 중이라, 정확한 시맨틱스 속성 이름과 패키지 경로가 시점에 따라 다를 수 있습니다. 그래서 저는 이 부분 코드를 단정해서 박기보다, 공식 Credential Manager 자동완성 문서를 먼저 확인하고 시그니처를 맞추시라고 말씀드리고 싶어요. 발행 직전에 한 번 더 공식 문서로 정확한 속성명을 최종 대조하는 걸 권합니다. alpha에서 정식으로 넘어오는 API는 이름이 바뀐 채 발행되면 그대로 깨지거든요.

API 34 미만 기기에서 Credential Manager 폴백 나누기
이 배선을 다 해놔도 낮은 기기에선 안 뜹니다. credentialRequest 계열은 API 34(안드로이드 14) 이상이 기준으로 알려져 있어요. 인라인 제안이 뜨는 secondary UI 흐름은 그보다 더 위, 안드로이드 15와 credentials 1.5.0-alpha01 근처부터라고 합니다.
API 34가 어느 정도냐면, 지금 시장에 깔린 기기 중 아직 이보다 낮은 버전 비중이 만만치 않다는 얘기입니다. 그 아래에선 자동으로 첫 번째 갈래인 contentType 자동완성으로 폴백하게 분기해줘야 해요.
그래서 현실적인 판단이 필요합니다. 유저 대부분이 최소 SDK 버전이 34 미만이라면, 공들여 Credential Manager를 붙여도 실제로 인라인 제안을 보는 사람은 소수고 나머지는 폴백만 받아요. 투자 대비 효과가 낮아지는 셈이죠.
제 생각에는, 단순히 아이디·비밀번호 로그인만 필요한 앱이라면 contentType 시맨틱스 하나로 끝내는 걸 먼저 권합니다. 패스키까지 풀로 통합하는 건 제공자·기기·OS 조합마다 UX가 갈려서 QA 매트릭스가 확 늘어나거든요. 재현이 안 되는 조합도 나옵니다.

안드로이드 패스키 로그인 UX에서 다시 점검할 지점
이걸 겪고 나니까 자동완성이 그냥 편의 기능이 아니라 패스키 전환의 입구라는 게 보이더군요. 안드로이드 패스키 로그인을 제대로 붙이려면 이 배선을 피해갈 수 없습니다.
디버깅할 때 저는 이 세 층위부터 갈라봅니다.
- 요청이 나가고는 있는가 — 로그에 요청 전송 흔적이 있는지부터 봅니다.
- 요청은 나가는데 UI만 안 뜨는가 — 여기부터는 기기·키보드 조합을 의심합니다.
- 내가 지금 두 갈래 중 어느 쪽을 켜려던 거였나 — 패시브면 contentType, 액티브면 GetCredentialRequest 연결입니다.
그날 밤샘의 결론은 단순했어요. 로그에 요청 전송 자체가 없었으니 UI를 아무리 뜯어봐야 답이 안 나왔던 겁니다. 침묵한 자동완성 앞에서 컴포즈를 탓하기 전에, 필드에 실제 요청이 배선돼 있는지부터 열어보면 범인은 대개 그 자리에 앉아 있었습니다.
'Android 개발 > Jetpack Compose' 카테고리의 다른 글
| 1.9.24 vs 1.9.25, 숫자 하나로 빌드 터지던 Compose 컴파일러 버전 청소법 (0) | 2026.10.06 |
|---|---|
| 4px 패딩 회귀를 눈으로 못 잡던 나, Compose 스크린샷 테스트로 넘긴 이야기 (0) | 2026.09.26 |
| 버튼 소리 직접 심었다면 Compose 1.12에서 두 번 울립니다 (1) | 2026.09.19 |
| 안드로이드 화면을 iOS에 그대로? Compose Multiplatform 1.11 공유 일지 (0) | 2026.09.16 |
| 값을 화면 깊은 곳까지 파라미터로 줄줄이 내리던 자리 — CompositionLocal 제대로 쓴 일지 (0) | 2026.08.29 |