다짐) React Native Modal Refactoring (Part.3 Promise로 뷰와 로직 분리하기)

2ast·2023년 8월 20일
2
post-custom-banner

Promise로 Custom Modal 강화하기

이제 길고 길었던 Modal Refactoring 마지막 파트다. 이번에는 Promise를 활용해서 modal action을 처리하는 방식으로 뷰와 로직을 분리하는 작업을 해볼 예정이다. 저번 시간에 작성한 코드를 기반으로 확장할 예정이므로, 이전 글을 읽고 오는 것을 추천한다. 개인적으로 이 작업을 하면서 시야가 굉장히 넓어지는 경험을 했기 때문에 이렇게 글로 전달할 수 있게 되어 정말 기쁘다.

일단 이 작업은 deferred라는 유틸함수를 이용해 구현할 것이다. deferred는 지난번 React Native에서 로그인 가딩으로 사용성 극대화하기 (with Promise) 편에서도 사용한 적 있는데, Promise를 사용하기 편리한 형태로 가공해서 제공해주는 역할을 한다.

export interface Deferred<T> {
  get state(): 'pending' | 'fulfilled' | 'rejected';
  promise: Promise<T>;
  resolve: (value?: T | PromiseLike<T>) => void;
  reject: (reason?: unknown) => void;
}

export const deferred = <T>(): Deferred<T> => {
  let state = 'pending';
  let resolve!: (value: T | PromiseLike<T>) => void;
  let reject!: (reason?: unknown) => void;

  const promise = new Promise<T>((res, rej) => {
    resolve = (value: T | PromiseLike<T>) => {
      state = 'fulfilled';
      res(value);
    };
    reject = (reason?: unknown) => {
      state = 'rejected';
      rej(reason);
    };
  });

  return {
    get state() {
      return state;
    },
    promise,
    resolve,
    reject,
  } as Deferred<T>;
};

준비는 끝났다. 각설하고, 바로 강화에 돌입해보겠다.

다짐 디자인 시스템상 모달에서 사용자가 취할 수 있는 기본 액션은 크게 4가지로 분류할 수 있다.

  • press main button
  • press sub button
  • press back button (android 물리 버튼)
  • press backdrop (모달 여백)

이렇게 4개의 액션을 resolve type으로 선언해주었다.

export type DialogHandlerResolveType =
  | 'pressedBackButton'
  | 'pressedBackDrop'
  | 'pressedMainButton'
  | 'pressedSubButton'

dialog handler store에 deferred 추가하기


export const useDialogHandler = create<DialogHandlerState>(set => ({
  modalProps: undefined,
  openModal: (
    _modalProps: DgModalProps,
    d?: Deferred<DialogHandlerResolveType>,
  ) =>
    set(() => {
      const modalProps = {..._modalProps};
      if (d) {
        modalProps.onMainButtonPress = () => {
          if (_modalProps.onMainButtonPress) {
            _modalProps.onMainButtonPress();
            d.resolve('pressedMainButton');
          }
        };
        modalProps.onSubButtonPress = () => {
          if (_modalProps.onSubButtonPress) {
            _modalProps.onSubButtonPress();
            d.resolve('pressedSubButton');
          }
        };
        modalProps.onBackdropPress = () => {
          if (_modalProps.onBackdropPress) {
            _modalProps.onBackdropPress();
            d.resolve('pressedBackDrop');
          }
        };
        modalProps.onBackButtonPress = () => {
          if (_modalProps.onBackButtonPress) {
            _modalProps.onBackButtonPress();
            d.resolve('pressedBackButton');
          }
        };
      }

      return {modalProps};
    }),
  closeModal: () => set({modalProps: undefined}),
}));

지난번에 정의했던 useDialogHandler의 openModal이 optional하게 deferred를 받도록 추가해주었다. 이때 d의 resolve타입은 직전에 정의해주었던 DialogHandlerResolveType이다. openModal의 내부에서는 resolveType에 해당하는 modalProps의 함수와 resolve를 바인딩해주었다. 이렇게하면 만약 사용자가 모달에서 mainButton을 누를 경우 deferred가 'pressedMainButton'으로 resovle될 것이다.

openModal에 deferred 넘겨주기

이제 실제로 openModal을 deferred와 함께 사용해볼 것이다.

const openSignOutConfirmModal = async () => {
    const d = deferred<DialogHandlerResolveType>();
    openModal(
      {
        type: 'twoButton',
        title: '로그아웃 하시겠어요?',
        content: '언제든지 다시 로그인하실 수 있어요.',
        mainButtonLabel: '로그아웃',
        onMainButtonPress: closeModal,
        subButtonLabel: '다음에',
        onSubButtonPress: closeModal,
        onBackdropPress: closeModal,
        onBackButtonPress: closeModal,
      },
      d,
    );
    return d.promise;
};

openUseOneDayMembershipConfirmModal 함수는 openModal의 두번째 parameter로 d를 넘기고 있고, 비동기적으로 d.promise를 반환하고 있다. 그리고 onPress action으로 모두 closeModal만을 넘기고 있는 부분이 눈에 띈다. 이 함수는 modal 안쪽으로 어떠한 로직도 넘겨주지 않는다. 모달은 오로지 '뷰'를 그려줄 뿐이고, 사용자가 어떤 액션을 취했는지를 resolve할 뿐이다.

실제 컴포넌트에서 사용해보기

const MyDagym = () =>{
	...
    
    const onPressSignOut = async () => {
        const action = await openSignOutConfirmModal();
        if (action === 'pressedMainButton') {
			dgSignOut()
    	}
	}
    
    return <Container>
      ...
       <SignOutButton onPress={onPressSignOut}/>
    </Container>
}

위 코드는 openSignOutConfirmModal을 실제 컴포넌트에서는 어떻게 사용하는지 간략하게 나타낸 것이다. SignOutButton을 누르면 openSignOutConfirmModal이 실행되어 모달이 노출되고, 이 함수는 비동기적으로 사용자 액션을 반환한다. 이때 반환된 action을 보고 적절한 함수를 실행해주면 된다. 만약 action이 'pressedMainButton'으로 반환됐다면 사용자가 '로그아웃'을 눌렀다는 뜻이고, 그에 따라 dgSignOut 함수를 실행해주는 방식이다.

기존 코드와 비교해보기

이렇게 다짐의 모달 리팩토링이 모두 끝이 났다. 마지막으로 새로운 다짐의 모달이 얼마나 아름다운지 체감해보기 위해 '작성한 리뷰를 삭제하는 모달'을 기존 코드와 비교해보려고 한다. 아래는 단순한 예시 코드이며 간랸한 코드를 위해 모달의 구현부분은 생략하도록 하겠다.

기존 방식

const ReviewManagement = () =>{
	const [deletingReviewId,setDeletingReviewId] = useState('')
    const isDeleteConfirmModalVisible = Boolean(deletingReviewId)
    const closeDeleteConfirmModal = () =>setDeletingReviewId('')
    
    const deleteReview =(reviewId:string)=>{
    	...
    }
        
    const onPressDeleteReview = (reviewId:string) =>{
    	setDeletingReviewId(reviewId)
    }
        
    return <Container>
      {reviews.map((review)=>{
        return <Reivew review={review} onPressDeleteReview={onPressDeleteReview}/>
      })}
      <DeleteReviewConfirmModal 
        isVisible={isDeleteConfirmModalVisible} 
        closeModal={closeDeleteConfirmModal}
        deleteReview={deleteReview}
        onBackdropPress={closeDeleteConfirmModal}
        onCancelPress={closeDeleteConfirmModal}
      />
    </Container>
}

리팩토링 이후

const ReviewManagement = () =>{

  const {openDeleteReviewConfirmModal} = useReviewManagementDialogHandler()
  
    const deleteReview =(reviewId:string)=>{
    	...
    }
        
    const onPressDeleteReview = async (reviewId:string) =>{
		const action = await openDeleteReviewConfirmModal()
        if(action === 'pressedMainButton'){
        	deleteReview(reviewId)
        }
    }
        
    return <Container>
      {reviews.map((review)=>{
        return <Reivew review={review} onPressDeleteReview={onPressDeleteReview}/>
      })}
    </Container>
}

기존 방식은 isVisible state 선언을 피하기 위해 deletingReviewId로 isVisible을 대체했음에도 모달의 액션을 구현하기 위해 많은 값들을 Modal component의 props로 넘겨주어야했다. 이는 코드 가독성은 물론 관리 관점에서도 꽤나 신경쓰이는 측면이 있다.
반면 새롭게 리팩토링 된 코드는 모달로 어떠한 로직도 넘겨주지 않은채 오직 뷰를 노출하는 역할만 부여했고, 반대로 모달로부터 사용자가 어떤 액션을 취했는지를 컴포넌트로 반환해주기 때문에 이를 분석해 적절한 함수를 실행해줄 수 있다. 코드가 훨씬 직관적이고 간결해졌음을 느낄 수 있다.

profile
React-Native 개발블로그
post-custom-banner

0개의 댓글