
react-native-modal 라이브러리에 내장된 기능으로 모달의 배경을 클릭했을 때 호출되는 이벤트 핸들러이다.
onBackdropPress={() => setModalVisible(false)}를 사용해서 모달의 배경을 클릭하며 모달을 닫을 수 있다.
✅ 애니메이션
useNativeDriver={true}
: 애니메이션을 네이티브 드라이버를 사용하여 실행할지 여부를 결정한다. true로 설정되면 애니메이션이 더 원활하고 성능이 좋게 실행된다.
주로 애니메이션의 효율성과 성능에 영향을 준다.
hideModalContentWhileAnimating={true}
: 모달이 애니메이션 중일 때 모달의 내용을 숨길지 여부를 결정한다. true로 설정하면 애니메이션이 진행되는 동안 모달의 내용이 보이지 않는다.
이는 콘텐츠가 애니메이션 중에 깜빡이는 것을 방지하고, 보다 부드러운 애니메이션을 제공한다.
적용된 코드
{currentImage && (
<Modal
isVisible={modalVisible}
onBackdropPress={() => setModalVisible(false)}
useNativeDriver={true}
hideModalContentWhileAnimating={true}>
<View style={styles.modalContainer}>
<TouchableOpacity
style={{alignItems: 'flex-end', marginRight: 20}}
onPress={() => setModalVisible(false)}>
<Image
style={{width: 15, height: 15, marginTop: 9}}
source={require('../../assets/icon/cancel2.png')}
/>
</TouchableOpacity>
<Image
source={{uri: currentImage}}
style={styles.fullImage}
resizeMode="contain"
/>
</View>
</Modal>
)}
import { Modal } from 'react-native';
const handleGroupPress = groupId => {
setCurrentGroupId(groupId); // 선택한 그룹 ID 저장
setCurrentGroupImages(groupedImages[groupId]); // 현재 그룹의 모든 이미지 저장
setModalVisible(true);
Animated.timing(animatedValue, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}).start();
};
// 모달 닫기 함수
const closeModal = () => {
Animated.timing(animatedValue, {
toValue: 0,
duration: 120,
useNativeDriver: true,
}).start(() => setModalVisible(false));
};
모달을 열고 닫는 함수에서 각각 애니메이션을 커스텀 해준다.
여기서는 모달이 켜지고 닫히는 지속성(시간)을 지정했다.
const modalTranslateY = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [300, 0], // 모달이 열릴 때 애니메이션
});
추가적으로 modalTranslateY 를 통해 모달의 위치 변화를 정의했다. animatedValue의 변화에 따라 모달이 아래에서 위로 부드럽게 이동하게 된다.
animatedValue는 애니메이션에 의해 변하는 값이고
interpolate메서드는 animatedValue가 변화하는 동안, 그 값에 따라 다른 값을 계산하는 데 사용된다.
즉, 애니메이션이 진행됨에 따라 inputRange에 대응하는 outputRange 값을 생성한다.
inputRange: [0, 1]: animatedValue가 0에서 1 사이의 값을 가질 때의 범위를 지정합니다. 예를 들어, 애니메이션이 시작될 때는 animatedValue가 0일 것이고, 애니메이션이 끝날 때는 1이 됩니다.
outputRange: [300, 0]: inputRange 값에 따라 출력될 값을 지정합니다. animatedValue가 0일 때는 출력값이 300이 되고, animatedValue가 1일 때는 출력값이 0이 됩니다.
✅ 정리하면,
handleGroupPress에서 애니메이션을 실행시키고, modalTranslateY를 사용해 그 애니메이션이 모달의 위치에 반영되도록 한 것이다!
전체 코드
import React, {useState, useEffect, useRef} from 'react';
import {
View,
Text,
StyleSheet,
Image,
TouchableOpacity,
ScrollView,
Modal,
TouchableWithoutFeedback,
Alert,
Animated,
} from 'react-native';
import {useNavigation, useRoute} from '@react-navigation/native';
import {useSelector, useDispatch} from 'react-redux';
import {assignHashTag} from '../../api/HashTagAssign';
import {HashTagListCheck} from '../../api/HashTagListCheck';
const Filter4 = () => {
const navigation = useNavigation();
const route = useRoute(); // useRoute 훅을 사용하여 경로 정보 가져오기
const {groupedImages} = route.params; // Filter3에서 전달된 데이터(여러 그룹 이미지들)
const [modalVisible, setModalVisible] = useState(false);
const [currentGroupId, setCurrentGroupId] = useState(null);
const [currentGroupImages, setCurrentGroupImages] = useState([]); // 현재 그룹의 모든 이미지 저장
const [selectedTags, setSelectedTags] = useState({}); // 각 그룹의 선택된 해시태그 저장
const hashtagList = useSelector(state => state.HashTagReducer.hashtagList); // 해시태그 목록을 가져옴
const dispatch = useDispatch();
const animatedValue = useRef(new Animated.Value(0)).current; // 애니메이션 값을 초기화
const handleNavigation = () => {
navigation.navigate('Filter5', {
groupedImages,
});
};
// 해시태그 목록 요청
useEffect(() => {
dispatch(HashTagListCheck());
}, [dispatch]);
// 이미지 그룹 클릭할 때 호출 -> 현재 그룹 id, 이미지 저장
const handleGroupPress = groupId => {
setCurrentGroupId(groupId); // 선택한 그룹 ID 저장
setCurrentGroupImages(groupedImages[groupId]); // 현재 그룹의 모든 이미지 저장
setModalVisible(true);
Animated.timing(animatedValue, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}).start();
};
// 모달 닫기 함수
const closeModal = () => {
Animated.timing(animatedValue, {
toValue: 0,
duration: 120,
useNativeDriver: true,
}).start(() => setModalVisible(false));
};
// 해시태그를 선택할 때 호출 -> 선택한 해시태그 저장 및 API 호출
const handleSelectTag = async tag => {
const imageUrls = currentGroupImages.map(url => url.split('?')[0]);
try {
const requestBody = {
imageUrls,
hashtagId: tag.id,
};
const data = await assignHashTag(requestBody);
console.log('해시태그 저장 성공:', data);
// 해시태그 선택 상태 업데이트
setSelectedTags(prevState => ({
...prevState,
[currentGroupId]: tag.text,
}));
} catch (error) {
console.error('해시태그 저장 중 오류 발생:', error);
Alert.alert(
'Error',
`해시태그 저장 중 오류가 발생했습니다: ${error.message}`,
);
}
};
const modalTranslateY = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [300, 0], // 모달이 열릴 때 애니메이션
});
return (
<View style={styles.outerContainer}>
<ScrollView contentContainerStyle={styles.container}>
<View style={styles.header}>
<Image
style={styles.step_3}
source={require('../../assets/icon/step_3.png')}
resizeMode="contain"
/>
<Text style={styles.text}>
그룹을 클릭하고 해시태그를 지정하세요!
</Text>
</View>
<View style={styles.gridContainer}>
{groupedImages.map((group, index) => (
<React.Fragment key={index}>
{index % 2 === 0 && index !== 0 && (
<View style={styles.separator} />
)}
<View style={styles.imageContainer}>
<TouchableOpacity
style={styles.imageWrapper}
onPress={() => handleGroupPress(index)}>
<Image
source={{uri: group[0]}} // 첫 번째 이미지를 썸네일로 사용
style={styles.image}
/>
<Text style={styles.imageCount}>
{`${group.length}장`} {/* 이미지 개수*/}
</Text>
</TouchableOpacity>
</View>
</React.Fragment>
))}
</View>
<Modal
visible={modalVisible}
transparent={true}
animationType="none"
onRequestClose={closeModal}>
<TouchableWithoutFeedback onPress={closeModal}>
<View style={styles.modalBackground}>
<TouchableWithoutFeedback>
<Animated.View
style={[
styles.modalContainer,
{transform: [{translateY: modalTranslateY}]},
]}>
<ScrollView contentContainerStyle={styles.hashList}>
{hashtagList.map(item => (
<TouchableOpacity
key={item.id.toString()}
style={[
styles.hashItem,
selectedTags[currentGroupId] === item.text &&
styles.selectedHashItem,
]}
onPress={() => handleSelectTag(item)}>
<Text
style={[
styles.hashText,
selectedTags[currentGroupId] === item.text,
]}>
{`#${item.text}`}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</Animated.View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
</ScrollView>
<View style={styles.fixedFooter}>
<TouchableOpacity onPress={handleNavigation}>
<Image
style={styles.next}
source={require('../../assets/icon/next2.png')}
resizeMode="contain"
/>
</TouchableOpacity>
</View>
</View>
);
};
const styles = StyleSheet.create({
outerContainer: {
flex: 1,
backgroundColor: 'white',
},
container: {
alignItems: 'center',
padding: 20,
},
header: {
width: '100%',
alignItems: 'center',
marginBottom: 20,
},
step_3: {
width: 300,
margin: 10,
},
text: {
fontSize: 18,
textAlign: 'center',
color: 'black',
},
gridContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
imageContainer: {
width: '48%', // 2개씩 배치
marginBottom: 20,
},
imageWrapper: {
position: 'relative',
},
image: {
width: '95%',
height: 150,
borderRadius: 10,
},
separator: {
width: '100%',
height: 1,
backgroundColor: '#ccc',
marginVertical: 10,
},
modalBackground: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
modalContainer: {
width: '55%',
backgroundColor: 'white',
borderRadius: 10,
borderColor: '#F7F8CB',
borderWidth: 5,
shadowColor: '#000',
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.8,
shadowRadius: 2,
elevation: 5,
paddingVertical: 5,
justifyContent: 'center',
alignItems: 'center',
},
hashList: {
flexDirection: 'column',
paddingVertical: 15,
justifyContent: 'flex-start',
alignItems: 'center',
},
hashItem: {
marginVertical: 10,
alignItems: 'center',
paddingVertical: 10,
paddingHorizontal: 20,
},
selectedHashItem: {
backgroundColor: '#F7F8CB',
borderRadius: 8,
},
hashText: {
fontSize: 18,
},
selectedHashText: {
color: 'white',
},
next: {
width: 120,
top: 15,
},
fixedFooter: {
position: 'absolute',
bottom: 20,
width: '100%',
alignItems: 'center',
},
imageCount: {
position: 'absolute',
bottom: 5,
right: 13,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
color: 'white',
fontSize: 12,
padding: 5,
borderRadius: 5,
},
});
export default Filter4;