리액트 네이티브에서 중첩 모달로 발생한 충돌

FeRo 페로·2025년 1월 16일
0

프로젝트를 작업하던 중 정말 난감한 오류를 마주쳤다. iOS 시뮬레이터에서 Expo 앱이 충돌하는 문제였는데, React Navigation 이슈 에서 언급된 사례와 비슷한 상황이었다. 문제는 내가 다른 팀원이 작성한 코드를 리팩토링하던 중 발생했다. 해당 코드는 여러 개의 모달을 동시에 렌더링하고 있었고, 모달 안의 버튼을 클릭하면 다른 경로(route)의 화면으로 이동해야 했다. 그런데 버튼을 클릭하자마자 Expo 앱이 바로 충돌해버렸다.

특히나 충돌 로그가 전혀 남지 않았기 때문에 몇 번이고 같은 에러를 발생시키면서 원인을 찾으려 해도 찾을 수 없었다. 에러 로그 대신에 다음 사진처럼 생긴 macOS의 충돌 보고서만 표시되었다.

error report

문제를 파악하는 데 꽤 오랜 시간이 걸렸다. 다행히도 GitHub가 항상 그렇듯 날 살려주었다.. 처음에는 react-native-modal 레포지토리로 가서 비슷한 문제를 겪은 사람들이 있는지 확인했다. 그리고 이슈 탭과 FAQ 섹션에서 힌트를 얻을 수 있었다.

결론적으로, React Native는 여러 모달을 동시에 렌더링하는 것을 잘 처리하지 못한다는 것이 문제였다. React Native 자체의 한계로 인해 이런 상황에서 충돌이 발생할 수 있었다. 우리 코드에서는 이미 이를 우회하기 위한 트릭을 사용하고 있었지만, 이 방식에서 다른 경로로 네비게이션을 하는데 에러가 발생했다. 모달과 모달이 위치하고 있는 스크린이 완전히 unmount되기 전에 네비게이션을 시도하면서 충돌이 발생한 것으로 보였다.
GitHub에서 추천된 몇 가지 해결책을 시도해봤지만 나에게는 효과가 없었다. 예를 들어 setTimeout이 있었다. 몇몇 분들은 상태 업데이트가 완료될 때까지 비동기적으로 기다리는 걸로 해결을 했다고 했지만 나에게는 효과가 없었다.

결국, 나는 겹쳐진 모달을 분리하고 onModalHide 이벤트를 활용하는 방식으로 문제를 해결했다. 아래는 초기 코드이다.

// ChlidrenModal.tsx
const ChildrenModal = ({isVisible, ...}:PropsInterface) => {
  ...
  return (
  	<Modal isVisible={isModalVisible} 
            onClickBtn=
            {()=>navigation.navigate('OtherRouteScreen')} ... />
  )
}

// ParentsModal.tsx
const ParentsModal = ({isVisible, ...}:PropsInterface) => {
  const [isModalVisible, setIsModalVisible] = useState(false);
  ...
  return (
      <>
      	  <Modal isVisible={isVisible} />
          <ChildrenModal isVisible={isModalVisible} 
            onClickBtn=
            {()=>navigation.navigate('OtherRouteScreen')} ... />
      </>
  )
}

// RandomScreen.tsx
const RandomScreen = () => {
  const [isParentsModalVisible, setIsParentsModalVisible] = useState(false);
  ...
  return (
  	<>
      ...
      <ParentsModal isVisible={isParentsModalVisible} ... />
    </>
  )
}

위 코드에서는 ParentsModal과 ChildrenModal이 중첩되어 있었다. 일단 먼저 분리를 해서 중첩된 모달을 분리했다.

// ChlidrenModal.tsx didn't change

// ParentsModal.tsx
const ParentsModal = ({isVisible, ...}:PropsInterface) => {
  ...
  return (
    <Modal isVisible={isVisible} />
  )
}

// RandomScreen.tsx
const RandomScreen = () => {
  const [isParentsModalVisible, setIsParentsModalVisible] = useState(false);
  const [isChildrenModalVisible, setIsChildrenModalVisible] = useState(false);
  ...
  return (
  	<>
      ...
      <ParentsModal ... />
      <ChildrenModal ... />
    </>
  )
}

이후 onModalHide과 useRef를 활용해서 모달이 닫힐 때 시나리오에 따라 분기처리를 했다.

// ParentsModal.tsx and ChlidrenModal.tsx  didn't change

// RandomScreen.tsx
const RandomScreen = () => {
  const [isParentsModalVisible, setIsParentsModalVisible] = useState(false);
  const [isChildrenModalVisible, setIsChildrenModalVisible] = useState(false);
  const conditionInModalHideEvent = useRef({
            ...
            isClickedNavigationBtn: false,
          }
        )

  const onClickBtn = () => {
    conditionInModalHideEvent.curretn.isClickedNavigationBtn = true;
    ...
  }
    
  const onModalHide = () => {
    // condition with ref
    if(conditionInModalHideEvent.curretn.isClickedNavigationBtn) {
      conditionInModalHideEvent.curretn.isClickedNavigationBtn = false;
      navigation.navigate('other route');
    }
  }
 
  ...
  return (
  	<>
      ...
      <ParentsModal ... />
      <ChildrenModal onClickBtn={onClickBtn} onModalHide={onModalHide} ... />
    </>
  )
}

여기서 useState 대신 useRef를 사용했다. 그 이유는 불필요한 리렌더링을 피하고 싶었기 때문이다. onModalHide에서 조건을 확인할 때 state를 사용하면 조건문에서 렌더링이 발생한다. 즉, onModalHide에서는 navigation을 하려고 하는데, 스크린에서는 다시 리렌더링을 하려는 상황이 발생한다는 것이다. 이는 원래 문제가 일어나는 이유로 추측하는 그 조건을 또 다시 충족하게 되는 것이다.
또 다른 이유는 UI와 직접적으로 관련된 것이 아니고 단순히 논리적인 흐름을 제어하기 위한 것이기 때문이다. 그렇기 때문에 useRef가 더 적합하다고 판단했다.

수정 후에는 완벽하게 작동했다! 더 나은 방법이 있겠지만, expo crash report 창이 눈 앞에 나타났을 때를 생각해 보면 아주아주 다행이다.


참고 자료
react native modal repo
react navigation issue
react native modal issue

profile
주먹펴고 일어서서 코딩해

0개의 댓글