React Native의 Modal (1. 개념)

eeennsu·1일 전

React Native

목록 보기
96/96

개요

Modal 은 React Native 가 기본으로 제공하는 코어 컴포넌트다. 현재 화면 위에 콘텐츠를 띄워 사용자의 흐름을 잠시 끊고 확인이나 입력을 받을 때 쓴다. 알림, 확인 다이얼로그, 이미지 뷰어, 짧은 폼 정도가 흔한 용도다.

이름만 보면 zIndex 를 크게 준 View 와 비슷해 보이지만 실제 동작은 다르다. Modal 은 네이티브 계층에서 별도의 표시 영역으로 올라간다. iOS 는 UIViewController 의 모달 프리젠테이션을 쓰고, Android 는 별도의 Dialog 윈도우로 렌더링된다. 여기서 말하는 윈도우는 운영체제가 화면에 무언가를 그리려고 할당하는 독립적인 표시 영역이다. 보통 "앱 화면 하나"가 윈도우 하나에 해당한다.

그래서 Modal 은 React Navigation 스택이든 앱 안의 어떤 zIndex 든 상관없이 항상 위에 떠 있다. 편리한 특성이지만 동시에 이 글 뒷부분에 나오는 제약들의 원인이기도 하다.



기본 사용법

import React, {useState} from 'react';
import {Modal, View, Text, Pressable, StyleSheet} from 'react-native';

export default function ModalExample() {
  const [visible, setVisible] = useState(false);

  return (
    <View style={styles.container}>
      <Pressable onPress={() => setVisible(true)}>
        <Text>모달 열기</Text>
      </Pressable>

      <Modal
        animationType="slide"
        transparent
        visible={visible}
        onRequestClose={() => setVisible(false)}>
        <View style={styles.backdrop}>
          <View style={styles.sheet}>
            <Text>모달 내용</Text>
            <Pressable onPress={() => setVisible(false)}>
              <Text>닫기</Text>
            </Pressable>
          </View>
        </View>
      </Modal>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {flex: 1, justifyContent: 'center', alignItems: 'center'},
  backdrop: {
    flex: 1,
    backgroundColor: 'rgba(0,0,0,0.5)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  sheet: {
    backgroundColor: 'white',
    padding: 24,
    borderRadius: 12,
  },
});

패턴 자체는 단순하다. visible 을 부모 컴포넌트의 state 로 들고 있고, 닫히는 모든 경로에서 그 state 를 false 로 내려주면 된다.

주의할 점은 visiblefalse 인 동안 Modal 의 children 은 렌더되지 않는다는 것이다. 상태가 유지된 채 숨어 있는 게 아니라, 열 때마다 자식 트리 전체가 새로 마운트된다. 모달 안에 무거운 리스트나 차트가 있으면 여는 순간 그 마운트 비용을 한꺼번에 치른다.

커스텀 배경을 만들 거라면 transparent 는 사실상 필수다. 이걸 켜지 않으면 모달 컨테이너 자체가 backdropColor(기본 흰색)로 채워져서 뒤 화면이 보이지 않는다.



주요 Props

Modal 은 View Props 를 상속한다. 그 위에 Modal 고유의 props 가 얹힌다.

공통

Prop타입기본값설명
visiblebooltrue모달 표시 여부
animationType'none' \| 'slide' \| 'fade''none'등장 애니메이션. slide 는 아래에서 위로, fade 는 페이드 인
transparentboolfalse모달 컨테이너 배경을 투명하게 렌더링. 커스텀 backdrop 을 만들 때 필요
backdropColorcolorwhite모달 컨테이너의 배경색. transparenttrue 면 무시된다
onRequestClosefunction-닫기 요청 콜백. Android 와 TV 에서는 필수
onShowfunction-모달이 화면에 표시된 직후 호출

iOS 전용

Prop타입설명
presentationStyle'fullScreen' \| 'pageSheet' \| 'formSheet' \| 'overFullScreen'표시 스타일. 기본값은 transparentfalsefullScreen, trueoverFullScreen
allowSwipeDismissalbool아래로 스와이프해서 닫기를 허용한다. 켜면 onRequestClose 를 반드시 구현해야 한다
onDismissfunction모달이 완전히 사라진 직후 호출
onOrientationChangefunction모달 표시 중 방향이 바뀌면 'portrait' 또는 'landscape' 를 전달
supportedOrientationsarray회전 허용 방향 목록. pageSheet, formSheet 에서는 무시된다

onDismiss 는 실무에서 생각보다 자주 쓰인다. 모달을 닫고 곧바로 다른 모달을 띄우거나 화면을 전환해야 할 때, 이 콜백을 기다리지 않으면 애니메이션이 꼬인다. Android 에는 대응되는 prop 이 없어서 플랫폼 분기가 필요하다.

Android 전용

Prop타입기본값설명
hardwareAcceleratedboolfalse모달 윈도우의 하드웨어 가속을 강제한다
statusBarTranslucentboolfalse모달을 상태바 아래까지 확장한다
navigationBarTranslucentboolfalse모달을 네비게이션 바 아래까지 확장한다. statusBarTranslucent 도 함께 true 여야 한다

Android 15 부터 edge-to-edge 가 강제되면서 앱 화면은 기본적으로 시스템 바 아래까지 그려진다. 그런데 Modal 은 별도의 Dialog 윈도우라서 앱 화면의 설정을 그대로 물려받지 않는 경우가 많다. 전체 화면을 덮는 모달을 만들었는데 상태바 영역만 색이 다르게 남는다면 이 두 prop 을 먼저 확인하면 된다.



자주 부딪히는 제약

한 번에 하나

Modal 은 동시에 하나만 안정적으로 표시된다. 모달을 닫으면서 곧바로 다른 모달을 띄우면 두 번째가 열리지 않거나 애니메이션이 어긋난다. 닫힘이 끝난 뒤에 다음 것을 여는 식으로 순서를 만들어야 한다.

// iOS: onDismiss 로 닫힘 완료를 기다린다
<Modal visible={firstVisible} onDismiss={() => setSecondVisible(true)} ... />

Android 에는 onDismiss 가 없으므로 애니메이션 길이만큼 setTimeout 을 두거나, 애초에 모달 하나 안에서 내용만 바꾸는 방식으로 설계하는 편이 낫다. 모달을 겹쳐 띄우는 UX 가 반복해서 필요하다면 빌트인 Modal 로는 한계가 분명하다.

onRequestClose 와 백 버튼

Android 에서는 onRequestClose 가 필수다. 빼먹으면 경고가 뜨고, 하드웨어 백 버튼으로 모달을 닫을 수 없게 된다.

더 중요한 건 모달이 열려 있는 동안 BackHandler 이벤트가 아예 발생하지 않는다는 점이다. 백 버튼은 오직 모달의 onRequestClose 로만 간다. 화면 단에서 BackHandler 로 뒤로가기를 가로채는 로직을 짜뒀다면, 모달이 열린 동안에는 그 로직이 통째로 무시된다고 보면 된다. 반대로 이 동작이 필요해서 Modal 을 쓰는 경우도 있다.

키보드

모달 안에서 TextInput 을 쓰면 키보드가 입력창을 가리는 문제가 iOS 에서 특히 자주 나온다. 모달 내부에도 KeyboardAvoidingView 를 따로 감싸야 한다.

Android 는 Dialog 윈도우가 액티비티의 windowSoftInputMode 를 그대로 따르지 않기 때문에, 앱 화면에서는 잘 동작하던 키보드 회피가 모달 안에서만 어색해지는 일이 생긴다. 이 경우 원인을 코드에서 찾기 어려워서 시간을 많이 쓰게 된다.

세이프 에어리어

모달 컨테이너는 앱 화면의 세이프 에어리어 컨텍스트 밖에 있다. 노치나 홈 인디케이터를 피하려면 모달 안에서 별도로 처리해야 한다. react-native-safe-area-context 를 쓴다면 모달 내부를 SafeAreaProvider 로 다시 감싸거나, initialWindowMetrics 를 넘겨줘야 값이 제대로 잡힌다.

presentationStyle 과 transparent 조합

pageSheetformSheet 는 iOS 가 그리는 시트 형태의 프리젠테이션이다. 여기에 transparent={true} 를 같이 주면 의도한 대로 동작하지 않는다. 시트 스타일은 투명 배경을 전제로 만들어진 게 아니기 때문이다. 커스텀 backdrop 이 필요하면 overFullScreen 을 쓰고 배경을 직접 그리는 쪽이 맞다.



대안 라이브러리

빌트인 Modal 로 부족하다고 느낄 때 흔히 쓰는 선택지들이다.

라이브러리어떤 경우에
react-native-modal빌트인 Modal 을 감싸서 애니메이션, 스와이프 닫기, backdrop 제어를 추가한다. 별도 윈도우라는 구조는 그대로다
@gorhom/bottom-sheet제스처로 조작하는 본격적인 바텀시트가 필요할 때
@gorhom/portal 같은 Portal 라이브러리같은 윈도우 안에서 오버레이를 띄우고 싶을 때. 성능 문제를 우회하는 용도로도 쓴다
React Navigation 의 modal presentation모달을 화면 전환의 일부로 다루고 뒤로가기 스택에 태우고 싶을 때

react-native-modal 은 빌트인 Modal 을 래핑한 것이라, 뒤에서 다룰 Android 의 별도 윈도우 문제는 그대로 안고 간다. 성능 때문에 대안을 찾는 상황이라면 이쪽이 아니라 Portal 계열을 봐야 한다.



정리

Modal 은 화면 위에 무언가를 잠깐 띄우는 가장 간단한 방법이다. 확인 다이얼로그나 단순한 알림 정도라면 빌트인으로 충분하고, 굳이 라이브러리를 붙일 이유가 없다.

다만 Modal 이 일반 View 가 아니라 네이티브의 별도 표시 영역이라는 사실은 기억해둘 필요가 있다. 한 번에 하나만 열린다는 점, 백 버튼이 가로채진다는 점, 세이프 에어리어와 키보드 처리가 앱 화면과 따로 논다는 점은 전부 이 구조에서 나온다. 제약을 만났을 때 코드가 아니라 구조를 의심해야 답이 빨리 나온다.

모달 내부에 상태 변경이 잦거나 무거운 자식 트리가 있다면 Android 에서 체감 성능이 눈에 띄게 나빠지는데, 이건 사용법으로 해결되는 문제가 아니다. 그 원인과 대안은 이어지는 두 글에서 다룬다.

profile
이력서 https://resume.eunsu.pro

0개의 댓글