주소 자동완성과 키보드의 전쟁
React Native에서 키보드가 올라간 상태로 드롭다운을 탭하는 것이 왜 이렇게 어려운지에 대한 기록.
주소 자동완성과 키보드의 전쟁
단순한 요구사항이었습니다. 주소를 입력하면 제안 목록이 뜨고, 탭하면 선택됩니다. 어디서나 볼 수 있는 자동완성 UI.
문제는 키보드였습니다.
문제의 발견
시뮬레이터에서는 잘 됐습니다. 실기기에서 테스트하니 안 됐습니다. 정확히는, 소프트 키보드가 올라가 있는 상태에서 제안 항목을 탭하면 선택이 안 됐습니다.
원인을 추적해 보니: 키보드가 올라간 상태에서 TextInput 밖을 터치하면, 시스템이 키보드를 먼저 내립니다. 이 과정에서 onEndEditing이 발생하고, 드롭다운이 사라지고, 탭 이벤트가 유실됩니다.
시뮬레이터에서는 하드웨어 키보드를 쓰니까 소프트 키보드가 안 올라옵니다. 그래서 문제가 안 나타났던 겁니다.
첫 번째 시도: 타이밍 조절
onEndEditing에서 드롭다운을 숨기는 deactivate()를 300ms 지연시켰습니다. 키보드가 내려가도 드롭다운이 잠깐 남아 있으니, 그 사이에 탭이 처리될 거라 생각했습니다.
안 됐습니다. 키보드가 내려가면서 터치 이벤트 자체가 유실되거나, Pressable의 onPress가 호출되지 않았습니다.
onPressIn(터치 다운 시점)으로 바꿔 봤습니다. 역시 안 됐습니다.
두 번째 시도: keyboardShouldPersistTaps
React Native의 ScrollView에는 keyboardShouldPersistTaps라는 prop이 있습니다. "handled"로 설정하면 Pressable이 처리하는 탭은 키보드를 내리지 않습니다.
문제: 이건 해당 ScrollView 안의 모든 Pressable에 적용됩니다. 드롭다운뿐만 아니라 페이지의 다른 버튼들도 키보드를 내리지 않게 됩니다. 상태 변경 버튼을 눌러도 키보드가 안 내려갑니다.
드롭다운이 보일 때만 "handled"를 적용하고 아닐 때는 기본값으로 돌리는 것도 시도했습니다. 동적으로 값을 바꾸면 탭 처리 중에 re-render가 발생해서 터치가 취소됐습니다.
세 번째 시도: 네이티브 모듈
JS 레벨에서 해결이 안 되니 네이티브로 내려갔습니다. Expo 모듈로 KeyboardPersistView를 만들었습니다.
iOS — hitTest override
iOS에서는 hitTest(_:with:)를 override해서 터치 타겟을 자기 자신으로 설정했습니다. 이렇게 하면 터치가 React Native의 일반 터치 핸들러 대신 우리 뷰에서 처리됩니다.
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hit = super.hitTest(point, with: event)
if hit != nil && hit != self {
return self // 터치를 직접 처리
}
return hit
}
touchesEnded에서 탭된 자식 뷰의 인덱스를 찾아 JS에 이벤트로 전달합니다. 키보드는 여전히 내려가지만, 선택은 확실히 동작합니다.
키보드를 아예 안 내려가게 하는 것도 시도했습니다.
touchesBegan에서super를 호출하지 않기 — 안 됐습니다. 키보드 dismiss는 gesture recognizer 레벨에서 발생합니다.UITapGestureRecognizer에delaysTouchesBegan = true— 안 됐습니다.- 부모 뷰 체인의 모든 gesture recognizer에
require(toFail:)— 안 됐습니다. UIView.setAnimationsEnabled(false)+ 즉시becomeFirstResponder— 키보드 애니메이션이 UIView 애니메이션이 아니라 안 됐습니다.- 별도
UIWindow에 드롭다운 렌더링 — 키보드는 안 내려갔지만 나머지 모든 게 부자연스러웠습니다.
결국 “키보드는 내려가되, 선택은 확실히 동작”하는 hitTest 방식이 최선이었습니다.
Android — 벽
Android에서는 상황이 달랐습니다. 네이티브 뷰의 dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent 어느 것도 호출되지 않았습니다.
원인은 두 가지였습니다.
첫째, Android에서 position: "absolute"로 부모 영역 밖에 위치한 뷰는 보이기는 하지만 터치를 받지 못합니다. iOS와 다릅니다.
둘째, 설사 일반 flow로 렌더해도 Android OS가 TextInput 밖 터치 시 키보드를 먼저 내리고 터치 이벤트를 소비합니다. 뷰에 도달하기 전에 사라집니다.
FOCUS_BLOCK_DESCENDANTS, isFocusable = false — 다 시도했습니다. OS 레벨에서 터치를 가져가니 뷰 레벨에서 할 수 있는 게 없었습니다.
Portal로 FlatList 바깥에 렌더하는 것도 시도했지만, 위치 추적, 키보드 높이 대응, 스크롤 동기화 등 새로운 문제가 끝없이 나왔습니다.
결론
iOS에서는 네이티브 모듈로 해결했습니다. 키보드는 내려가지만 선택은 동작합니다. 완벽하진 않지만 실용적입니다.
Android에서는 비활성화했습니다. OS가 터치를 가로채는 근본적인 문제를 뷰 레벨에서 해결할 수 없었습니다.
단순한 자동완성 UI에 이틀을 썼습니다. 시도한 방법만 열 가지가 넘습니다. “키보드가 올라간 상태에서 드롭다운을 탭한다”는 것이 이렇게 어려운 문제인 줄 몰랐습니다.
모바일 개발에서 키보드는 언제나 마지막 보스입니다.