Android 개발/Jetpack Compose

Compose 1.11 대화면 대응 일지: 트랙패드가 따로 놀던 화면 손보기

stackD 2026. 8. 9. 18:00
반응형

 

얼마 전에 삼성 덱스(DeX)에 앱을 띄워놓고 트랙패드로 글자를 드래그했는데, 선택이 안 되고 화면이 주르륵 밀리더라고요. 마우스를 버튼에 올려도 아무 반응이 없었습니다.

 

분명 폰에서는 멀쩡하게 잘 되던 화면이었어요. 근데 노트북 크기로 띄워놓고 마우스·트랙패드·키보드를 섞어 쓰기 시작하니까, 그동안 안 보이던 구멍이 우르르 드러났습니다. 안드로이드 폴더블이랑 덱스, 크롬북까지 챙기다 보니 결국 입력 처리를 통째로 다시 봐야 했어요.

 

Compose 대화면에서 트랙패드·마우스가 따로 놀던 자리들

처음 정리해보니 문제가 딱 세 갈래였어요.

 

  1. 트랙패드로 텍스트를 클릭&드래그하면 선택이 아니라 스크롤이 됐습니다.
  2. 마우스를 버튼 위에 올려도 호버 효과나 커서 모양이 전혀 안 바뀌었어요.
  3. 키보드 Tab으로 이동하면 지금 포커스가 어디 가 있는지 화면에 표시가 안 됐습니다.

 

폰 화면만 생각하고 짠 UI라 터치 외의 입력은 사실상 고려가 안 돼 있었던 거예요. 큰 화면에서는 이 세 개가 한꺼번에 티가 나더라고요.

 

Compose 1.11 트랙패드 PointerType 변경이 핵심

가장 먼저 잡힌 게 트랙패드 드래그였습니다. 원인을 파고들어 보니 입력 종류를 어떻게 분류하느냐 문제였어요.

 

젯팩 컴포즈(Jetpack Compose) 1.11 미만 버전에서는 트랙패드 입력을 일종의 가짜 터치, 그러니까 PointerType.Touch로 처리했습니다. 그래서 손가락으로 드래그한 것처럼 인식돼 스크롤이 돼버린 거예요.

 

1.11부터 (2026년 4월 릴리스) 이게 바뀌었습니다. 트랙패드 입력을 마우스, 즉 PointerType.Mouse로 분류하게 됐어요. 그러니까 클릭&드래그가 의도대로 텍스트 선택으로 동작하게 됐습니다.

 

문제는 여기서 조용한 회귀가 생길 수 있다는 점이에요. 기존에 이런 식으로 분기하던 코드가 있다면 점검이 필요합니다.

 

Modifier.pointerInput(Unit) {
    awaitPointerEventScope {
        while (true) {
            val event = awaitPointerEvent()
            val type = event.changes.first().type
            // 1.11 미만: 트랙패드가 여기 걸렸음
            // 1.11 이상: 트랙패드는 Mouse로 빠져서 이 분기를 안 탐
            if (type == PointerType.Touch) {
                handleTouchGesture(event)
            }
        }
    }
}

 

이전엔 트랙패드가 Touch 분기를 통과했는데, 1.11부터는 Mouse로 빠지면서 이 로직을 더 이상 안 타게 됩니다. 빌드도 안 깨지고 에러도 안 나니까 진짜 모르고 넘어가기 쉬워요. 저도 처음엔 왜 특정 제스처만 먹통인지 한참 헤맸습니다.

 

다행히 1.11에는 performTrackpadInput 같은 트랙패드 동작 테스트 API가 새로 들어왔어요. CI에서 트랙패드 드래그·스크롤을 자동으로 검증할 수 있으니, 마이그레이션할 때 이걸로 회귀 테스트를 걸어두는 게 안전하다고 봅니다. 정확한 시그니처는 1.11 공식 문서를 보시는 게 확실해요.

 

 

키보드 포커스 링으로 대화면 내비게이션 살리기

트랙패드를 잡고 나니 다음은 키보드였습니다. 큰 화면에서는 마우스 없이 Tab으로 쭉 훑는 분들이 생각보다 많은데, 지금 어디에 포커스가 가 있는지 안 보이면 길을 잃어버리거든요.

 

그래서 포커스 링(focus ring)을 붙였어요. Tab으로 이동했을 때 현재 위치에 테두리가 또렷하게 보이게 한 겁니다. 핵심은 키보드로 이동할 때만 링을 노출하고, 마우스 클릭으로 포커스가 잡힐 때는 숨기는 거예요. 마우스 클릭마다 테두리가 번쩍이면 오히려 화면이 산만해지니까요.

 

여기서 한 가지 더 챙겨야 했던 게 포커스 순서였습니다. 화면에 보이는 시각적 흐름이랑 Tab 이동 순서가 어긋나 있으면 사용자가 엉뚱한 곳으로 튀어버립니다. Modifier.focusProperties로 다음·이전 포커스를 명시해서, 눈으로 보는 순서랑 키보드 이동 순서를 맞춰뒀어요.

 

 

마우스 호버 효과와 커서 모양 처리

세 번째는 호버였어요. 마우스를 쓰는 사람은 버튼 위에 커서를 올렸을 때 "여기 누를 수 있구나" 하는 신호를 기대합니다. 근데 아무 변화가 없으니 죽은 화면처럼 보이더라고요.

 

호버 상태를 받아서 배경색이 살짝 바뀌게 처리했어요.

 

val interactionSource = remember { MutableInteractionSource() }
val isHovered by interactionSource.collectIsHoveredAsState()

Box(
    Modifier
        .hoverable(interactionSource)
        .background(if (isHovered) HoverColor else Color.Transparent)
        .pointerHoverIcon(PointerIcon.Hand) // 클릭 가능한 요소
)

 

커서 모양도 손봤습니다. 클릭 가능한 요소엔 PointerIcon.Hand로 손가락 커서를, 텍스트 위에는 PointerIcon.Text로 I자 모양 커서를 띄우게 했어요.

 

다만 호버 효과를 아무 데나 다 넣으면 화면이 정신없어집니다. 그래서 실제로 상호작용이 필요한 핵심 요소에만 골라서 적용했어요. 장식용 카드까지 다 반응하게 만들면 오히려 산만해지는 경우가 많거든요.

 

폴더블·DeX 대화면 회귀 방지 점검 항목

마지막으로 안드로이드 폴더블이랑 덱스를 같이 놓고 최종 점검을 돌렸습니다. 제가 실제로 체크한 항목은 이랬어요.

 

  1. 폴더블을 펼치고 접을 때 입력 상태가 유지되는지, 힌지 부분에서 UI가 깨지지 않는지 확인합니다.
  2. 창 크기를 조절하면서 터치·마우스·키보드를 섞어 써도 동작이 일관되게 나오는지 봅니다.
  3. 드래그 선택, Tab 순회, 호버, 커서 변경, 힌지 관련 동작을 전부 자동화 테스트로 묶어둡니다.

 

특히 3번이 중요하다고 봅니다. 입력 처리는 한 번 고쳐놔도 라이브러리 버전을 올리다 보면 조용히 회귀하기 딱 좋은 영역이에요. 이번 1.11 PointerType 변경처럼요.

 

큰 화면 대응은 결국 "우리 앱은 터치만 들어온다"는 가정을 버리는 일이었습니다. 다음 라이브러리 업데이트 때 입력이 또 따로 놀지 않도록, 트랙패드 드래그와 Tab 순회만큼은 테스트 코드로 먼저 묶어두고 버전을 올릴 생각이에요. 다음 덱스 점검 때는 글자 드래그가 한 번에 잡히고 Tab 테두리가 또렷하게 따라오는 화면을 보게 되겠지요.

 

 

반응형
LIST