[React Native] 확장성 좋은 Snackbar, Popup 개발하기

Huckleberry·2023년 1월 7일
14

react-native

목록 보기
1/2
post-thumbnail

결과물

react-native-global-components

개발 배경

앱을 개발하다보면 Snackbar(Toast), Popup, BottomSheet 등 Modal 형태의 UI 로 사용자와의 인터렉션을 필요로 하는 컴포넌트들이 있다.

이런 컴포넌트들을 개발하다보면 여러가지 상황에 마주치는데 예를들어

  • A 라는 상황에 팝업을 보여준다.
  • B 라는 상황에 바텀시트를 보여준다.

라는 코드를 작성했을때, 특정 유저가 두가지 경우를 모두 만족하는 경우

이렇게 컴포넌트가 겹쳐져서 보여지게 되는 경우를 많이 봤었고 이를 해결하고 싶었다.

해결방안

  1. UI 확장성

기존에 react-native 에서 라이브러리를 찾아보면

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);
  1. 스크린에 겹쳐지는 문제

흔히 전공때 배운 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 만들어 둬야겠다..!)

  • UI 확장성

MyCustomUI 처럼 개발자가 자신의 팝업에 대한 UI 를 직접 작성할 수 있고 필요한 props 를 마음대로 추가할 수 있다.

props 를 추가하면 createPopup 팩토리에서 show 메서드에 필요한 props 를 제공하도록 타입을 infer 하기 때문에 타입스크립트의 도움을 100% 받으면서 사용할 수 있다.

끝 ~ 🥳

첫 오픈소스 릴리즈!

사이드 프로젝트할 때 계속 사용해 가면서 유지보수할 예정이다.

4개의 댓글

comment-user-thumbnail
2023년 1월 16일

제리제리~ 메인에 걸리셨네요 ~! 대박 🤩

답글 달기
comment-user-thumbnail
2023년 1월 17일

ㅋㅋㅋㅋㅋ 머야머야~!! 😶

답글 달기
comment-user-thumbnail
2023년 1월 19일

대박대박 !! 🥰

답글 달기
comment-user-thumbnail
2023년 1월 19일

좋아요랑 구독했습니다

답글 달기