react-native-global-components
앱을 개발하다보면 Snackbar(Toast), Popup, BottomSheet 등 Modal 형태의 UI 로 사용자와의 인터렉션을 필요로 하는 컴포넌트들이 있다.
이런 컴포넌트들을 개발하다보면 여러가지 상황에 마주치는데 예를들어
라는 코드를 작성했을때, 특정 유저가 두가지 경우를 모두 만족하는 경우
이렇게 컴포넌트가 겹쳐져서 보여지게 되는 경우를 많이 봤었고 이를 해결하고 싶었다.
기존에 react-native 에서 라이브러리를 찾아보면
스낵바 : react-native-snackbar
Snackbar.show({
text: 'Hello world',
duration: Snackbar.LENGTH_INDEFINITE,
action: {
text: 'UNDO',
textColor: 'green',
onPress: () => { /* Do something. */ },
},
});
각 라이브러리에서 제공하는 API 가 있는데 이때 UI 상 스타일을 변경하고 싶다던지, 팝업이 나타나고 사라지는 애니메이션을 수정하고 싶다던지에 대한 확장성이 좋지 않다.
이를 라이브러리 사용하는 개발자가 직접 결정하고 커스터마이징 할 수 있도록 할 순 없을까? 🤔 라는 생각을 하게 되었다.
그래서 Snackbar API 를 lazy 하게 개발자가 직접 정의해서 사용하도록 하는 팩토리 함수만 제공 해주자 생각하게 되었다.
// ./src/global-components/InputPopup.ts
export default createPopup(InputPopup); // 사용자가 직접 정의한 컴포넌트
// ./src/App.ts
InputPopup.show(props);
흔히 전공때 배운 Race Condition
과 비슷한 상황이라 생각했는데 하나의 화면을 두고 오버레이를 포함한 2~3개의 팝업이 동시에 떳을때 컨텐츠가 겹쳐져 보이기 때문에 예뻐보이지 않게 되기 때문에 이를 lock 을 가지고 스크린의 사용 여부 상태를 관리하는 Manager 를 정의했다.
class GlobalComponentManager {
/**
* observable for render command
*/
protected render$ = new Subject<RenderCommand>();
/**
* observable for remove command
*/
protected remove$ = new Subject<RemoveCommand>();
/**
* flag screen in use
*/
private locked = false;
/**
* component props list queue
*/
private queue: RenderCommand[] = [];
/**
* render component
* if screen in use, push props and render later when screen is free.
*
* @param {RenderCommand} command
* @returns {void} of component props in queue
*/
public render(command: RenderCommand): void {
if (this.locked) {
this.queue.push(command);
return;
}
this.locked = true;
this.render$.next(command);
}
실제 소스코드 일부인데, render$
와 remove$
라는 observable 을 만들어서 각각의 실제 그려지는 컴포넌트에서 구독해서 show 요청을 받게하고 Manager 에서는 queue 를 활용해서 들어온 요청들을 보관했다가 처리하도록 설계했다.
이제 Portal
이란 개념을 사용했는데 원하는 위치에 렌더링 할 수 있도록 제공 했다. (실제로 ReactDOM API 에서 제공하는 portal 과는 동작 방식이 다르지만 개념적으로 비슷한거 같아서 네이밍을 채택했다)
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
<SimpleSnackbar.Portal />
<InputPopup.Portal />
워하는 위치에 쓰윽~
interface InputPopupProps {
userSendTo: UserInfo;
onSend: (text: string) => void;
sendText: string;
}
const InputPopup: React.FC<InputPopupProps> = (props) => {
const [message, setMessage] = useState('');
const { hide } = useUpdateGlobalComponentState();
const { style: fade } = useFadeAnimationStyle();
const { style: slide } = useSlideAnimationStyle({ translateY: -30 });
return <Animated.View style={[fade, slide]}><MyCustomUI {...props} /></Animated.View>
}
export default createPopup(InputPopup);
라이브러리에서 제공하는 useFadeAnimationStyle
, useSlideAnimationStyle
등을 통해서 사용자가 직접 애니메이션을 선택해서 사용할 수 있고 원하는 경우 직접 hook 을 만들어 얼마든지 애니메이션을 변경 할 수 있다. (앞으로 필요할 때 마다 여러가지 hook 만들어 둬야겠다..!)
MyCustomUI 처럼 개발자가 자신의 팝업에 대한 UI 를 직접 작성할 수 있고 필요한 props 를 마음대로 추가할 수 있다.
props 를 추가하면 createPopup
팩토리에서 show 메서드에 필요한 props 를 제공하도록 타입을 infer 하기 때문에 타입스크립트의 도움을 100% 받으면서 사용할 수 있다.
첫 오픈소스 릴리즈!
사이드 프로젝트할 때 계속 사용해 가면서 유지보수할 예정이다.
제리제리~ 메인에 걸리셨네요 ~! 대박 🤩