UX 엔지니어링 가이드은(는) 무엇을 하나요?
Jetpack Compose, Material 3, 오프라인 우선 복원력, 인간 우선 모바일 인체공학을 사용하여 네이티브 Android 애플리케이션을 설계하고 구축하기 위한 실용적이고 프로덕션에서 검증된 운영 프레임워크입니다.
1. 핵심 모바일 인체공학 및 엄지 영역
모바일 기기는 70% 이상의 시간 동안 한 손으로 사용됩니다. 엄지손가락 도달성, 촉각 피드백, 최소한의 물리적 마찰을 고려하여 인터페이스를 설계하세요.
1.1 인체공학적 도달성 계층 구조
+---------------------------+ <- 화면 상단 (표시 전용)
| 상태 및 헤더 | 도달하기 가장 어려움. 수동적
| 제목, 할당량 측정기 | 상태, 검색창, 실시간 측정기용으로 예약.
+---------------------------+
| | <- 중간 도달 영역
| 스크롤 가능한 콘텐츠 | 목록 항목, 세부 정보 및
| 받은편지함 카드, 스니펫 | 탐색 가능한 콘텐츠 카드에 이상적.
| |
+---------------------------+
| 기본 작업 대상 | <- 프라임 엄지 영역 (하단 40%)
| 보내기 버튼, 탭 | 탭하기 가장 쉬움. 탐색, CTA,
| 바텀 시트, 필터 | 및 대화형 컨트롤을 여기에 유지.
+---------------------------+
1.2 필수 터치 대상 표준
- 최소 대화형 대상: 모든 클릭 가능한 요소는 $48 \times 48\text{dp}$ (
Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)) 이상이어야 합니다. 시각적 아이콘이 $24\text{dp}$인 경우에도 마찬가지입니다. - 가장자리 여백 및 안전 영역: 카메라 컷아웃, 제스처 및 탐색 표시줄을 고려하여 항상 루트 Compose 레이아웃을
Scaffold로 감싸거나Modifier.windowInsetsPadding(WindowInsets.safeDrawing)을 적용하세요. - 하단의 양식 컨트롤: 텍스트 입력, 제출 버튼 및 확인 대화상자를
ModalBottomSheet또는 하단 고정 카드에 배치하여 소프트웨어 키보드(imePadding())가 활성 필드를 가리지 않도록 하세요.
2. 속도 및 지각된 성능
모바일 사용자는 지연을 즉시 인지합니다. 이전에 가져온 데이터를 사용할 수 있을 때 탐색을 차단하거나 빈 로딩 상태를 표시하지 마세요.
2.1 제로-스켈레톤 규칙 (Stale-While-Revalidate)
- 문제점: 사용자가 받은편지함 탭을 탭할 때마다 전체 페이지 스켈레톤 로더 또는 원형 스피너를 표시하면 느리고, 거슬리며, 미완성된 느낌을 줍니다.
- 해결책: Stale-While-Revalidate (SWR) 캐시 패턴을 구현합니다.
- 가져온 항목을 로컬 암호화 저장소(
EncryptedSharedPreferences또는 SQLite/Room)에 저장합니다. - 화면에 진입할 때 캐시된 항목을 즉시 렌더링합니다 (0ms 지연).
- 캐시가 5~10분보다 오래된 경우 백그라운드에서 재검증합니다.
- 새로 고쳐진 데이터가 도착하면 목록을 지우거나 빈 자리 표시자를 표시하지 않고 항목을 부드럽게 추가/업데이트합니다.
- 백그라운드 새로 고침 중에는 미묘한 표시기(예: 당겨서 새로고침 스피너 또는 작은 상단 진행률 표시줄)만 표시합니다.
- 가져온 항목을 로컬 암호화 저장소(
2.2 오프라인 우선 아웃박스 패턴
모바일 기기의 셀룰러 연결이 불안정하거나 일시적으로 비행기 모드인 경우에도 사용자 작업이 실패하지 않도록 하세요.
flowchart TD
UserAction["사용자가 파일 공유 / 메모 입력"] --> LocalDraft["1. 로컬 아웃박스 초안에 저장"]
LocalDraft --> RenderOutbox["2. 아웃박스 대기열 카드에 즉시 렌더링"]
RenderOutbox --> CheckNet{"네트워크에 연결됨?"}
CheckNet -- 예 --> DirectUpload["3. 직접 업로드"]
CheckNet -- 아니오 --> EnqueueWork["3. WorkManager 작업 대기열 (NetworkType.CONNECTED)"]
EnqueueWork --> WaitConn["4. Wi-Fi / 셀룰러 대기"]
WaitConn --> AutoDispatch["5. 지수 백오프로 백그라운드 업로드"]
DirectUpload --> Success["6. UI 상태 업데이트 및 알림 토스트 표시"]
AutoDispatch --> Success
아웃박스 UI 상태:
- 보류 중: "네트워크를 기다리는 중…" 및 일시 중지된 클라우드 아이콘.
- 업로드 중: "전송 중 42%…" 및 선형 진행률 표시기.
- 실패 / 재시도 중: 명확한 상태 표시기와 함께 명시적인 재시도 및 취소 버튼.
3. 모션, 전환 및 촉각 햅틱
최신 Android 디자인(Android 14/15+)은 네이티브 및 반응형으로 느껴지도록 동적이고 물리학 기반의 모션과 촉각 감각 피드백이 필요합니다.
3.1 가장자리 대 가장자리 및 시스템 바 처리
항상 MainActivity.kt에서 시스템 바 뒤에 그립니다.
enableEdgeToEdge()
setContent {
VaultTheme {
Scaffold(
contentWindowInsets = WindowInsets.safeDrawing
) { innerPadding ->
AppNavHos(Modifier.padding(innerPadding))
}
}
}
3.2 예측 뒤로 가기 제스처
AndroidManifest.xml에서 예측 뒤로 가기를 활성화하고(android:enableOnBackInvokedCallback="true") 바텀 시트 및 세부 정보 서랍에 PredictiveBackHandler를 사용하세요.
PredictiveBackHandler(enabled = isDetailOpen) { progressFlow ->
try {
progressFlow.collect { backEvent ->
// backEvent.progress(0f ~ 1f)에 비례하여 세부 정보 시트 크기 조정 또는 이동
detailScale = 1f - (backEvent.progress * 0.1f)
}
// 사용자가 뒤로 가기 제스처를 완료함
closeDetail()
} catch (e: CancellationException) {
// 사용자가 뒤로 가기 제스처를 중단하고 1f로 다시 튕김
detailScale = 1f
}
}
3.3 공유 요소 전환
받은편지함 목록 행과 항목 세부 정보 보기 간에 전환할 때 화면을 SharedTransitionLayout으로 감쌉니다.
SharedTransitionLayout {
NavHos(...) {
composable("inbox") {
InboxScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
composable("detail/{id}") {
DetailScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
}
}
썸네일 카드, 아바타 아이콘 및 제목에 .sharedElement() 또는 .sharedBounds()를 사용하여 매끄러운 전환을 만듭니다.
3.4 촉각 햅틱
물리적 이정표가 발생할 때 사용자 작업을 절대 무음으로 남기지 마세요.
- 임계값 스와이프 (스와이프하여 닫기): 스와이프가 작업 임계값을 넘으면
HapticFeedbackConstantsCompat.GESTURE_START를 트리거합니다. - 클립보드에 복사 / 보내기 완료:
HapticFeedbackConstantsCompat.CONFIRM을 트리거합니다. - 유효성 검사 실패 / 네트워크 오류:
HapticFeedbackConstantsCompat.REJECT를 트리거합니다.
val view = LocalView.current
fun triggerHaptic(feedbackConstant: Int) {
view.performHapticFeedback(feedbackConstant)
}
3.5 선형 이징 대신 스프링 물리학
경직된 선형 애니메이션을 피하세요. 제스처, 시트 및 해제에는 항상 스프링 물리학을 사용하세요.
val offsetAnim by animateFloatAsState(
targetValue = targetOffset,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
4. 스마트 원탭 작업 및 시스템 통합
모바일 인터페이스는 페이로드 유형을 인식하고 가장 관련성 높은 네이티브 시스템 작업을 제공하여 인지 부하를 최소화해야 합니다.
4.1 컨텍스트 인식 작업 매트릭스
| 페이로드 유형 | 기본 작업 | 보조 작업 | 시스템 통합 |
|---|---|---|---|
| URL / 웹 링크 | 브라우저에서 열기 (Intent.ACTION_VIEW) | 링크 복사 | Chrome 사용자 지정 탭 또는 시스템 기본 브라우저 |
| 텍스트 / 스니펫 | 텍스트 복사 | 공유 (Intent.ACTION_SEND) | Android ClipboardManager 및 즉시 토스트 / 스낵바 |
| 파일 / 미디어 | 폴더에 다운로드 | 파일 열기 | Storage Access Framework (SAF) + FileProvider |
4.2 사용자 지정 기본 디렉토리를 사용한 직접 파일 다운로드
파일을 다운로드할 때마다 시스템 파일 선택기를 표시하면 불필요한 마찰이 발생합니다.
- 사용자가 설정/허브에서
ActivityResultContracts.OpenDocumentTree()를 사용하여 기본 다운로드 폴더를 한 번 선택하도록 허용합니다. - 권한을 영구적으로 유지합니다.
context.contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) - 다운로드를 탭할 때:
- 기본 디렉토리가 설정된 경우:
DocumentFile.createFile()을 사용하여 바이트를 직접 스트리밍하고, 실시간 바이트 진행률을 표시하며, 네이티브 완료 토스트를 실행합니다 ("document.pdf를 Vault에 다운로드했습니다"). - 기본 디렉토리가 설정되지 않은 경우:
ActivityResultContracts.CreateDocument()로 대체합니다.
- 기본 디렉토리가 설정된 경우:
4.3 FileProvider를 사용한 안전한 파일 열기
안전하지 않은 원시 file:// URI를 절대 사용하지 마세요. 항상 FileProvider를 사용하여 파일 보기를 네이티브 Android 애플리케이션에 위임하세요.
<!-- res/xml/file_paths.xml -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cached_files" path="." />
</paths>
val contentUri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
tempFile
)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(contentUri, mimeType)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(intent, "다음으로 열기..."))
5. 마찰 없는 모바일 인증
모바일 터치스크린에 서버 URL과 긴 비밀번호를 입력하면 오타가 발생하기 쉽습니다.
5.1 로그인 우선 순위 계층 구조
- 우선 순위 1: QR 코드 카메라 스캔 (1초 페어링)
- 데스크톱 웹 앱에 임시 서명된 페어링 QR 코드를 표시합니다.
- 모바일 사용자가 앱을 열고 QR 코드 스캔을 탭한 다음 카메라를 화면에 댑니다.
- 앱이 단일 패스에서 출처, 토큰 및 장치 이름을 구문 분석하여 즉시 인증합니다.
- 우선 순위 2: 접을 수 있는 서버 URL이 있는 사용자 이름 및 비밀번호
- 99%의 로그인이 기본 구성된 호스트를 사용합니다.
- 기본적으로 사용자 이름 및 비밀번호 필드만 표시합니다.
- 서버 URL을 미묘한 접을 수 있는 토글 뒤에 숨깁니다:
"서버 구성 (고급)".
5.2 14일 슬라이딩 세션 지속성
- 액세스 토큰(
va1_) 및 리프레시 토큰(vr1_)을 AndroidEncryptedSharedPreferences에 저장합니다. - 활성 사용 시 리프레시 토큰을 자동으로 슬라이드/로테이션합니다.
- 사용자가 14일 이내에 앱을 열면 재인증 없이 로그인 상태가 유지됩니다.
6. 적응형 레이아웃 (폴더블 및 태블릿)
WindowSizeClass를 사용하여 창 크기 클래스에 따라 화면이 부드럽게 확장되도록 빌드하세요.
- 컴팩트 (
<600dp): 단일 열, 하단 탐색 모음, 모달 바텀 시트. - 중간 (
600dp - 840dp): 탐색 레일, 2열 그리드. - 확장됨 (
>840dp): 영구적인 마스터-디테일 2개 창 레이아웃을 나란히 표시 (왼쪽에 목록, 오른쪽에 검사기).
7. Material 3 색상 대비 및 상태 접근성 (WCAG 2.2 AA / AAA)
모바일 장치는 가변적인 주변 조명(직사광선, 야외 눈부심, OLED 배터리 절약 어두움)으로 인해 대비 실패에 특히 취약합니다. WCAG 2.2 AAA 텍스트 대비($\ge 7:1$) 및 WCAG 1.4.11(비텍스트 테두리 대비 $\ge 3:1$) 및 WCAG 1.4.1(색상 사용)을 엄격히 준수하도록 노력하세요.
7.1 Material 3 다크 모드 FilterChip 함정
- 결함: 기본적으로 Material 3의
FilterChip은 다크 모드에서 어두운surface(#14181E)에 대해 선택된 배경색으로secondaryContainer(#37474F)를 사용합니다. 이는 허용할 수 없는 1.83:1의 대비 비율을 생성합니다. 낮은 밝기나 눈부심이 있는 경우 사용자는 어떤 칩이 활성화되어 있는지 알 수 없습니다. - 수정:
selectedBorderColor = MaterialTheme.colorScheme.primary로 높은 대비 테두리 정의를 강제합니다. (다크 모드에서 9.88:1 대비 비율을 생성).selectedBorderWidth = 1.5.dp로 설정합니다.- 선행 체크 표시 아이콘을 제공하여 WCAG 1.4.1(색상에만 의존하지 않는 상태)을 준수합니다.
FilterChip( selected = isSelected, onClick = { onSelect() }, label = { Text(label) }, leadingIcon = if (isSelected) { { Icon(Icons.Filled.Done, contentDescription = null, Modifier.size(16.dp)) } } else null, border = FilterChipDefaults.filterChipBorder( enabled = true, selected = isSelected, selectedBorderColor = MaterialTheme.colorScheme.primary, selectedBorderWidth = 1.5.dp ) )
7.2 햇빛 및 눈부심 가독성 기준
- 기본 텍스트 대 표면: 다크 모드에서 고대비 텍스트(
onSurface#E1E2E8또는#F8FAFCon#14181E)를 표준화하여 $\ge 13:1$ 대비를 달성합니다 (WCAG AAA 초과). - 양식 컨트롤 윤곽선:
OutlinedTextField및 라디오/체크박스 테두리는 컨테이너 카드(Color(0xFF8D9199)또는 명시적rgba(255, 255, 255, 0.20))에 대해 최소 $3:1$ 대비를 유지하는outline토큰을 사용해야 합니다. - 컴팩트 프리셋 행: 컴팩트 칩 행(예: 보존 프리셋
1h,4h,12h,1d)을 배치할 때 대상 크기를 유지하고maxLines = 1을selectedBorderColor와 함께 적용하여 칩이 줄바꿈되거나 모호해 보이지 않도록 합니다.
8. 참조 오픈소스 리포지토리
디자인 및 구현 영감을 위해 다음 업계 선도적인 오픈소스 Android 프로젝트를 검토하세요.
android/nowinandroid: 동적 테마, 적응형 레이아웃 및 클린 아키텍처를 보여주는 Google의 대표적인 Material 3 Compose 샘플.skydoves/Pokedex-Compose: 프로덕션 등급의 공유 요소 전환 (.sharedBounds/.sharedElement) 및 부드러운 세부 정보 시트 제스처.plainhub/plain-app: 로컬 전화 관리, Material You 테마 적용 및 반응형 클라이언트-서버 동기화의 뛰어난 구현.Gurupreet/ComposeCookBook: 프로덕션 Compose 애니메이션, 하단 바 및 사용자 정의 캔버스 마이크로 상호작용의 포괄적인 카탈로그.signalapp/Signal-Android: 로컬 암호화 저장소, 백그라운드 아웃박스 워커 및 네트워크 복원력의 표준.
적용 방법: 이 가이드를 프로젝트의 설계 및 구현 단계에 통합하세요. 각 섹션은 독립적으로 적용 가능하지만, 최상의 사용자 경험을 위해 모든 섹션을 함께 고려하는 것이 좋습니다. 특히 접근성, 성능 및 오프라인 지원은 모바일 앱의 품질을 결정하는 핵심 요소입니다.