Modal과 Alert — 사용자 인터랙션 다이얼로그
Modal은 현재 화면 위에 떠오르는 오버레이 UI이고, Alert는 시스템 수준의 간단한 확인 다이얼로그입니다.
모바일 앱에서 사용자에게 확인을 요청하거나, 추가 옵션을 보여줄 때 Modal과 Alert를 사용합니다. 각각의 용도가 다르니 상황에 맞게 선택해야 합니다.
Alert — 시스템 다이얼로그
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 — 커스텀 오버레이
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,
},
});
투명 배경 모달 (오버레이)
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',
},
});
바텀시트 모달
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,
},
});
재사용 가능한 모달 컴포넌트
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 네이티브 스타일)
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가 필요한 오버레이에 적합, 완전 커스터마이징 가능
onRequestClose는 Android 뒤로가기 를 처리하기 위해 반드시 설정하세요- 투명 모달에서 배경 탭으로 닫기 + 콘텐츠 영역 이벤트 차단 패턴을 기억하세요
Alert.prompt는 iOS 전용이므로, 크로스플랫폼이 필요하면 커스텀 모달을 사용하세요
댓글 로딩 중...