React Native에서 <InputAccessoryView> 구축하기
배경
3년 전, React Native에서 입력 액세서리 뷰를 지원해 달라는 GitHub 이슈가 올라왔다.

그 후 몇 년 동안 수많은 '+1' 투표와 다양한 해결 방법이 제안되었지만, 이 문제와 관련해 React Native에는 구체적인 변화가 없었다. 오늘부터 iOS를 시작으로 네이티브 입력 액세서리 뷰에 접근할 수 있는 API를 공개한다. 이 API를 어떻게 만들었는지 여러분과 공유하게 되어 기쁘다.
배경
입력 액세서리 뷰란 무엇일까? Apple 개발자 문서를 살펴보면, 리스폰더가 퍼스트 리스폰더가 될 때 시스템 키보드 상단에 고정할 수 있는 커스텀 뷰라는 것을 알 수 있다. UIResponder
를 상속받는 모든 객체는 .inputAccessoryView
프로퍼티를 읽기-쓰기로 재선언하고, 여기에 커스텀 뷰를 관리할 수 있다. 리스폰더 인프라는 이 뷰를 마운트하고, 시스템 키보드와 동기화를 유지한다. 드래그나 탭과 같이 키보드를 닫는 제스처는 프레임워크 레벨에서 입력 액세서리 뷰에 적용된다. 이를 통해 키보드 닫기와 상호작용이 가능한 콘텐츠를 만들 수 있으며, iMessage나 WhatsApp 같은 최상위 메시징 앱에서 필수적인 기능이다.
키보드 상단에 뷰를 고정하는 두 가지 일반적인 사용 사례가 있다. 첫 번째는 Facebook 컴포저의 배경 선택기와 같은 키보드 툴바를 만드는 것이다.

이 시나리오에서는 키보드가 텍스트 입력 필드에 포커스되고, 입력 액세서리 뷰는 추가 키보드 기능을 제공하는 데 사용된다. 이 기능은 입력 필드의 타입에 따라 달라진다. 지도 애플리케이션에서는 주소 제안이 될 수 있고, 텍스트 편집기에서는 서식 지정 도구가 될 수 있다.
이 시나리오에서 <InputAccessoryView>
를 소유하는 Objective-C UIResponder
는 명확해야 한다. <TextInput>
이 퍼스트 리스폰더가 되면, 내부적으로 UITextView
또는 UITextField
의 인스턴스가 된다.
두 번째 일반적인 시나리오는 스틱키 고정 텍스트 입력이다:

여기서 텍스트 입력은 실제로 입력 액세서리 뷰 자체의 일부이다. 이는 메시징 애플리케이션에서 일반적으로 사용되며, 이전 메시지 스레드를 스크롤하면서 메시지를 작성할 수 있다.
이 예제에서 <InputAccessoryView>
를 소유하는 것은 누구일까? 다시 UITextView
나 UITextField
가 될 수 있을까? 텍스트 입력이 입력 액세서리 뷰 안에 있으므로, 이는 순환 의존성처럼 들린다. 이 문제를 해결하는 것만으로도 별도의 블로그 포스트가 있다. 스포일러: 소유자는 일반적인 UIView
서브클래스이며, 수동으로 becomeFirstResponder를 호출한다.
API 설계
이제 <InputAccessoryView>
가 무엇인지, 그리고 어떻게 사용할지 이해했다. 다음 단계는 두 가지 사용 사례에 적합하고, <TextInput>
과 같은 기존 React Native 컴포넌트와 잘 작동하는 API를 설계하는 것이다.
키보드 툴바를 설계할 때 고려해야 할 사항은 다음과 같다:
- 일반적인 React Native 뷰 계층 구조를
<InputAccessoryView>
로 끌어올릴 수 있어야 한다. - 이 일반적이고 분리된 뷰 계층 구조가 터치를 받아들이고 애플리케이션 상태를 조작할 수 있어야 한다.
- 특정
<TextInput>
에<InputAccessoryView>
를 연결할 수 있어야 한다. - 코드를 중복하지 않고 여러 텍스트 입력 필드 간에
<InputAccessoryView>
를 공유할 수 있어야 한다.
첫 번째 요구사항은 React 포털과 유사한 개념을 사용해 달성할 수 있다. 이 설계에서 React Native 뷰를 응답자 인프라가 관리하는 UIView
계층 구조로 포털한다. React Native 뷰는 UIView로 렌더링되기 때문에 이 작업은 매우 간단하다. 다음과 같은 메서드를 재정의하면 된다:
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
그리고 모든 하위 뷰를 새로운 UIView 계층 구조로 연결한다. 두 번째 요구사항을 위해 <InputAccessoryView>
에 새로운 RCTTouchHandler를 설정한다. 상태 업데이트는 일반적인 이벤트 콜백을 사용해 수행한다. 세 번째와 네 번째 요구사항을 위해 <TextInput>
컴포넌트 생성 시 네이티브 코드에서 액세서리 뷰 UIView 계층 구조를 찾기 위해 nativeID 필드를 사용한다. 이 함수는 기본 네이티브 텍스트 입력의 .inputAccessoryView
속성을 사용한다. 이를 통해 <InputAccessoryView>
와 <TextInput>
을 ObjC 구현에서 효과적으로 연결한다.
스크롤 가능한 텍스트 입력(시나리오 2)을 지원하려면 몇 가지 제약 조건이 추가된다. 이 설계에서 입력 액세서리 뷰는 텍스트 입력을 하위 요소로 가지므로 nativeID를 통해 연결할 수 없다. 대신, 일반적인 오프스크린 UIView
의 .inputAccessoryView
를 네이티브 <InputAccessoryView>
계층 구조로 설정한다. 이 일반적인 UIView
를 수동으로 첫 번째 응답자로 지정하면, 응답자 인프라에 의해 계층 구조가 마운트된다. 이 개념은 앞서 언급한 블로그 포스트에서 자세히 설명한다.
문제점과 해결 과정
이 API를 구축하는 과정에서 모든 것이 순조롭게 진행되지는 않았다. 여기서는 우리가 마주친 몇 가지 문제점과 그 해결 방법을 설명한다.
이 API를 구축하기 위한 초기 아이디어는 NSNotificationCenter
를 사용해 UIKeyboardWill(Show/Hide/ChangeFrame) 이벤트를 감지하는 것이었다. 이 패턴은 일부 오픈소스 라이브러리와 Facebook 앱의 일부에서 사용되고 있다. 그러나 UIKeyboardDidChangeFrame
이벤트는 스와이프 동작 시 <InputAccessoryView>
의 프레임을 업데이트하기에 충분히 빠르게 호출되지 않았다. 또한, 키보드 높이 변화도 이러한 이벤트로 캡처할 수 없었다. 이로 인해 다음과 같은 버그가 발생했다:

iPhone X에서는 텍스트 키보드와 이모티콘 키보드의 높이가 다르다. 키보드 이벤트를 사용해 텍스트 입력 프레임을 조작하는 대부분의 애플리케이션은 위 버그를 해결해야 했다. 우리의 해결책은 .inputAccessoryView
프로퍼티를 사용하는 것이었다. 이렇게 하면 응답자 인프라가 프레임 업데이트를 처리할 수 있다.
우리가 마주친 또 다른 까다로운 버그는 iPhone X의 홈 바를 피하는 것이었다. "Apple이 safeAreaLayoutGuide를 개발한 이유가 바로 이것인데, 이건 간단한 문제야!"라고 생각할 수도 있다. 우리도 그렇게 생각했다. 첫 번째 문제는 네이티브 <InputAccessoryView>
구현이 나타나기 직전까지 앵커링할 윈도우가 없다는 것이다. 이 문제는 -(BOOL)becomeFirstResponder
를 재정의하고 레이아웃 제약을 강제하는 것으로 해결할 수 있다. 이러한 제약을 준수하면 액세서리 뷰가 위로 올라가지만, 또 다른 버그가 발생한다:
입력 액세서리 뷰는 홈 바를 성공적으로 피하지만, 이제 안전하지 않은 영역 뒤의 콘텐츠가 보인다. 이 문제는 radar에서 해결책을 찾을 수 있었다. 나는 네이티브 <InputAccessoryView>
계층을 safeAreaLayoutGuide
제약을 따르지 않는 컨테이너로 감쌌다. 네이티브 컨테이너는 안전하지 않은 영역의 콘텐츠를 덮고, <InputAccessoryView>
는 안전 영역 경계 내에 머무르게 된다.
예제 사용법
다음은 <TextInput>
의 상태를 초기화하는 키보드 툴바 버튼을 만드는 예제이다.
class TextInputAccessoryViewExample extends React.Component<
{},
*,
> {
constructor(props) {
super(props);
this.state = {text: 'Placeholder Text'};
}
render() {
const inputAccessoryViewID = 'inputAccessoryView1';
return (
<View>
<TextInput
style={styles.default}
inputAccessoryViewID={inputAccessoryViewID}
onChangeText={text => this.setState({text})}
value={this.state.text}
/>
<InputAccessoryView nativeID={inputAccessoryViewID}>
<View style={{backgroundColor: 'white'}}>
<Button
onPress={() =>
this.setState({text: 'Placeholder Text'})
}
title="Reset Text"
/>
</View>
</InputAccessoryView>
</View>
);
}
}
스틱키 고정 텍스트 입력의 다른 예제는 저장소에서 확인할 수 있다.
이 기능을 언제 사용할 수 있나요?
이 기능 구현에 대한 전체 커밋은 여기에서 확인할 수 있습니다. <InputAccessoryView>
는 다음 v0.55.0 릴리스에서 사용 가능합니다.
즐거운 코딩 되세요 :)