Modal은 현재 화면 위에 떠오르는 오버레이 UI이고, Alert는 시스템 수준의 간단한 확인 다이얼로그입니다.

모바일 앱에서 사용자에게 확인을 요청하거나, 추가 옵션을 보여줄 때 Modal과 Alert를 사용합니다. 각각의 용도가 다르니 상황에 맞게 선택해야 합니다.


Alert — 시스템 다이얼로그

TSX
import { Alert, Pressable, Text } from 'react-native';

function AlertExamples() {
  // 1. 단순 알림
  const showSimple = () => {
    Alert.alert('알림', '저장이 완료되었습니다.');
  };

  // 2. 확인/취소
  const showConfirm = () => {
    Alert.alert(
      '삭제 확인',                    // 제목
      '정말 삭제하시겠습니까?',         // 메시지
      [
        { text: '취소', style: 'cancel' },
        {
          text: '삭제',
          style: 'destructive',       // iOS에서 빨간색으로 표시
          onPress: () => deleteItem(),
        },
      ]
    );
  };

  // 3. 세 개 버튼
  const showThreeButtons = () => {
    Alert.alert(
      '변경사항',
      '저장하지 않은 변경사항이 있습니다.',
      [
        { text: '취소', style: 'cancel' },
        { text: '저장 안 함', style: 'destructive', onPress: () => discard() },
        { text: '저장', onPress: () => save() },
      ]
    );
  };

  // 4. 입력 프롬프트 (iOS만)
  const showPrompt = () => {
    Alert.prompt(
      '이름 변경',
      '새 이름을 입력하세요',
      (text) => console.log('입력값:', text),
      'plain-text',          // 'plain-text' | 'secure-text'
      '기본값',              // 기본 입력값
    );
  };

  return (
    <>
      <Pressable onPress={showSimple}><Text>단순 알림</Text></Pressable>
      <Pressable onPress={showConfirm}><Text>확인 다이얼로그</Text></Pressable>
    </>
  );
}

Alert.prompt는 iOS에서만 동작합니다. Android에서 입력을 받으려면 커스텀 Modal을 만들어야 합니다.


Modal — 커스텀 오버레이

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

function BasicModal() {
  const [visible, setVisible] = useState(false);

  return (
    <>
      <Pressable onPress={() => setVisible(true)}>
        <Text>모달 열기</Text>
      </Pressable>

      <Modal
        visible={visible}
        animationType="slide"        // 'none' | 'slide' | 'fade'
        presentationStyle="pageSheet" // iOS: 'fullScreen' | 'pageSheet' | 'formSheet'
        transparent={false}
        onRequestClose={() => setVisible(false)} // Android 뒤로가기 대응
      >
        <View style={styles.modalContainer}>
          <View style={styles.modalHeader}>
            <Pressable onPress={() => setVisible(false)}>
              <Text style={styles.closeButton}>닫기</Text>
            </Pressable>
            <Text style={styles.modalTitle}>모달 제목</Text>
            <View style={{ width: 40 }} />
          </View>
          <View style={styles.modalBody}>
            <Text>모달 내용</Text>
          </View>
        </View>
      </Modal>
    </>
  );
}

const styles = StyleSheet.create({
  modalContainer: {
    flex: 1,
    backgroundColor: 'white',
  },
  modalHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#eee',
  },
  closeButton: {
    color: '#007AFF',
    fontSize: 16,
  },
  modalTitle: {
    fontSize: 18,
    fontWeight: 'bold',
  },
  modalBody: {
    flex: 1,
    padding: 20,
  },
});

투명 배경 모달 (오버레이)

TSX
function OverlayModal() {
  const [visible, setVisible] = useState(false);

  return (
    <Modal
      visible={visible}
      transparent={true}              // 배경 투명
      animationType="fade"
      onRequestClose={() => setVisible(false)}
    >
      {/* 반투명 배경 — 탭하면 닫기 */}
      <Pressable
        style={styles.overlay}
        onPress={() => setVisible(false)}
      >
        {/* 모달 콘텐츠 — 이벤트 전파 방지 */}
        <Pressable style={styles.modalContent} onPress={() => {}}>
          <Text style={styles.title}>확인</Text>
          <Text style={styles.message}>정말 진행하시겠습니까?</Text>
          <View style={styles.buttonRow}>
            <Pressable
              style={[styles.btn, styles.cancelBtn]}
              onPress={() => setVisible(false)}
            >
              <Text>취소</Text>
            </Pressable>
            <Pressable
              style={[styles.btn, styles.confirmBtn]}
              onPress={() => {
                handleConfirm();
                setVisible(false);
              }}
            >
              <Text style={{ color: 'white' }}>확인</Text>
            </Pressable>
          </View>
        </Pressable>
      </Pressable>
    </Modal>
  );
}

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  modalContent: {
    backgroundColor: 'white',
    borderRadius: 16,
    padding: 24,
    width: '80%',
    maxWidth: 340,
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 8,
  },
  message: {
    fontSize: 16,
    color: '#666',
    marginBottom: 24,
  },
  buttonRow: {
    flexDirection: 'row',
    gap: 12,
  },
  btn: {
    flex: 1,
    padding: 14,
    borderRadius: 8,
    alignItems: 'center',
  },
  cancelBtn: {
    backgroundColor: '#f0f0f0',
  },
  confirmBtn: {
    backgroundColor: '#007AFF',
  },
});

바텀시트 모달

TSX
function BottomSheet() {
  const [visible, setVisible] = useState(false);

  return (
    <Modal
      visible={visible}
      transparent
      animationType="slide"
      onRequestClose={() => setVisible(false)}
    >
      <Pressable
        style={styles.bottomOverlay}
        onPress={() => setVisible(false)}
      >
        <Pressable style={styles.bottomSheet} onPress={() => {}}>
          {/* 드래그 핸들 */}
          <View style={styles.handle} />

          <Text style={styles.sheetTitle}>옵션 선택</Text>

          <Pressable style={styles.option} onPress={() => handleOption('edit')}>
            <Text>수정하기</Text>
          </Pressable>
          <Pressable style={styles.option} onPress={() => handleOption('share')}>
            <Text>공유하기</Text>
          </Pressable>
          <Pressable
            style={[styles.option, styles.destructiveOption]}
            onPress={() => handleOption('delete')}
          >
            <Text style={{ color: 'red' }}>삭제하기</Text>
          </Pressable>
        </Pressable>
      </Pressable>
    </Modal>
  );
}

const styles = StyleSheet.create({
  bottomOverlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'flex-end',
  },
  bottomSheet: {
    backgroundColor: 'white',
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    paddingHorizontal: 20,
    paddingBottom: 40,
  },
  handle: {
    width: 40,
    height: 4,
    backgroundColor: '#ddd',
    borderRadius: 2,
    alignSelf: 'center',
    marginVertical: 12,
  },
  sheetTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  option: {
    paddingVertical: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  destructiveOption: {
    borderBottomWidth: 0,
  },
});

재사용 가능한 모달 컴포넌트

TSX
interface CustomModalProps {
  visible: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
}

function CustomModal({ visible, onClose, title, children }: CustomModalProps) {
  return (
    <Modal
      visible={visible}
      transparent
      animationType="fade"
      onRequestClose={onClose}
    >
      <Pressable style={overlayStyle} onPress={onClose}>
        <Pressable style={contentStyle} onPress={() => {}}>
          <View style={headerStyle}>
            <Text style={titleStyle}>{title}</Text>
            <Pressable onPress={onClose} hitSlop={12}>
              <Text style={closeStyle}>X</Text>
            </Pressable>
          </View>
          {children}
        </Pressable>
      </Pressable>
    </Modal>
  );
}

// 사용
function Screen() {
  const [showFilter, setShowFilter] = useState(false);

  return (
    <>
      <CustomModal
        visible={showFilter}
        onClose={() => setShowFilter(false)}
        title="필터"
      >
        <FilterContent onApply={() => setShowFilter(false)} />
      </CustomModal>
    </>
  );
}

ActionSheet (iOS 네이티브 스타일)

TSX
import { ActionSheetIOS, Platform } from 'react-native';

function showActionSheet() {
  if (Platform.OS === 'ios') {
    ActionSheetIOS.showActionSheetWithOptions(
      {
        options: ['취소', '카메라', '갤러리', '삭제'],
        cancelButtonIndex: 0,
        destructiveButtonIndex: 3,
      },
      (buttonIndex) => {
        switch (buttonIndex) {
          case 1: openCamera(); break;
          case 2: openGallery(); break;
          case 3: deletePhoto(); break;
        }
      }
    );
  } else {
    // Android에서는 커스텀 바텀시트 사용
    setBottomSheetVisible(true);
  }
}

정리

  • Alert: 간단한 확인/취소에 적합, 시스템 UI 사용
  • Modal: 복잡한 UI가 필요한 오버레이에 적합, 완전 커스터마이징 가능
  • onRequestCloseAndroid 뒤로가기 를 처리하기 위해 반드시 설정하세요
  • 투명 모달에서 배경 탭으로 닫기 + 콘텐츠 영역 이벤트 차단 패턴을 기억하세요
  • Alert.prompt는 iOS 전용이므로, 크로스플랫폼이 필요하면 커스텀 모달을 사용하세요
댓글 로딩 중...