안녕하세요. 짭짭이입니다.
이번에는 EXPO의 Bottom Sheet 라이브러리와
expo Router에서 Modal 방식으로 View를 나오게 할 때 주의사항에 대해 말씀드릴려고 합니다.
https://github.com/gorhom/react-native-bottom-sheet
GitHub - gorhom/react-native-bottom-sheet: A performant interactive bottom sheet with fully configurable options 🚀
A performant interactive bottom sheet with fully configurable options 🚀 - gorhom/react-native-bottom-sheet
github.com
gorhom의 Bottom Sheet를 사용했습니다. (요즘 reat native의 매력에 빠졌습니다 ㅋㅋㅋㅋ.)
이놈이 정말 사용하기 쉽습니다.
저는 EXPO를 사용하니 아래와 같이 명령어를 통해서 깔아주고
npx expo install react-native-reanimated react-native-gesture-handler
저 같은 경우 Expo Router를 사용하니 _layout.tsx에 아래와 같이 코드를 작성해 줍니다.
import { Tabs } from "expo-router";
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function TabsLayout() {
return (
<GestureHandlerRootView style={{flex:1}}>
<Tabs>
<Tabs.Screen name="index" />
<Tabs.Screen name="tabSecond" />
</Tabs>
</GestureHandlerRootView>
)
}
여기서 중요한건 Bottom Sheet를 Drag 할 수 있도록 하는 Gesture 인식 컴포넌트를 가장 상위에 넣어줘야합니다.
<GestureHandlerRootView style={{flex:1}}>
</GestureHandlerRootView>
이제 Index.tsx에 아래와 같이 작성해줘야합니다.
import {
BottomSheetModal,
BottomSheetModalProvider,
} from '@gorhom/bottom-sheet';
export default function HomeScreen() {
const modalRef = useRef<BottomSheetModal>(null)
function openFiltersModal() {
modalRef?.current?.present()
}
function closeFiltersModal() {
modalRef.current?.close()
}
return (<BottomSheetModalProvider>
<View style={styles.container}>
<FiltersModal bottomModalRef={modalRef}
filters={filters}
setFilters={setFilters}
onClose={closeFiltersModal}
onApply={applyFilters}
onReset={resetFilters}/>
</View>
</BottomSheetModalProvider>)
}
BottomSheetModalProvider 하위에 FilterModal를 감싸줍니다.
이때 가장 상위 뷰인 HomeScreen Component에서 ModalView를 다룰 것이기 때문에
BottomSheetModal의 Reference를 useReference로 전달해줍니다.
export type Props = {
bottomModalRef: RefObject<BottomSheetModal>
filters: Record<string, any>
setFilters: Dispatch<SetStateAction<Record<string, any>>>
onClose: () => void
onApply: () => void
onReset: () => void
};
export function FiltersModal(props : Props) {
const snapPoints = useMemo(() => ['85%'], []);
const handleSheetChanges = useCallback((index: number, position: number) => {
console.log('handleSheetChanges', index, position);
}, []);
console.log(snapPoints)
return (
<BottomSheetModal
ref={props.bottomModalRef}
snapPoints={snapPoints}
index={0} // 이 줄을 유지합니다.
onChange={handleSheetChanges}
enablePanDownToClose={true}
enableDynamicSizing={false}
backdropComponent={CustomBackrop}
>
<BottomSheetView style={styles.contentContainer}>
<View style={styles.content}>
<Text style={styles.filterText}>Filters</Text>
{
(Object.keys(sections) as (keyof typeof sections)[]).map((sectionName, index) => {
const sectionView = sections[sectionName];
let title = capitalize(sectionName)
let sectionData = {
data: data.filters[sectionName],
modalProps: props,
filterName: sectionName
}
return (
<Animated.View key={index} entering={FadeInDown.delay(index * 100).springify().damping(10)}>
<SectionView
title={title}
content={sectionView(sectionData)}
/>
</Animated.View>
);
})
}
</View>
<Animated.View entering={FadeInDown.delay(550).springify().damping(20)} style={styles.buttons}>
<Pressable style={[styles.button, styles.resetButton]} onPress={props.onReset}>
<Text style={[styles.buttonText , {color:theme.colors.neutral(0.9)}]}>Reset</Text>
</Pressable>
<Pressable style={[styles.button, styles.applyButton]} onPress={props.onApply}>
<Text style={[styles.buttonText , {color:theme.colors.white}]}>Apply</Text>
</Pressable>
</Animated.View>
</BottomSheetView>
</BottomSheetModal>
)
}
FilterModal은 위와 같이 작성되어 있습니다.
<BottomSheetModal
ref={props.bottomModalRef}
snapPoints={snapPoints}
index={0} // 이 줄을 유지합니다.
onChange={handleSheetChanges}
enablePanDownToClose={true}
enableDynamicSizing={false}
backdropComponent={CustomBackrop}> // Modal View
<BottomSheetView style={styles.contentContainer}>
//원하는 뷰를 여기서 그려주면 됩니다.
</BottomSheetView>
</BottomSheetModal>
기본적인 구조는 이렇게 되어 있습니다.
ref={props.bottomModalRef} // 내가 Bottom Sheet를 다른 곳에서 제어할 때 참조자를 넣어줍니다.
const snapPoints = useMemo(() => ['85%'], []);
snapPoints={snapPoints} //snapPoint는 제공하는 index에 따라 보여주는 정도를 다르게 합니다.
index={0} // 이 줄을 유지합니다. snapPoint가 0일 때 85% 정도 보여주도록 합니다.
const handleSheetChanges = useCallback((index: number, position: number) => {
console.log('handleSheetChanges', index, position);
}, []);
onChange={handleSheetChanges} // 변화를 감지할 수 있습니다.
enablePanDownToClose={true} // 아래로 끌면 사라지도록 하는 겁니다.
//animatedIndex는 BottomSheet의 몇퍼센트 레인지에 도달했는지에 대한 Index이고
//Style은 BottomSheet의 현재 Style을 의미한다.
function CustomBackrop({animatedIndex, style}: BottomSheetBackdropProps) {
console.log(animatedIndex.value, style)
const containerAnimatedStyle = useAnimatedStyle( () : ViewStyle => {
let opacity = interpolate(
animatedIndex.value,
[-1,0], //index가 -1 일 때 Opacity 0 / index가 0 일 때 opacity 1
[0,1],
Extrapolation.CLAMP
)
console.log("Opcaity",opacity)
return {
opacity : opacity
}
})
const blurView = <BlurView intensity={25} tint={"dark"} style={StyleSheet.absoluteFill} />
const containerStyle = [
StyleSheet.absoluteFill,
style,
styles.overlay,
containerAnimatedStyle
]
return (
<Animated.View style={containerStyle}>
{blurView}
</Animated.View>
)
}
backdropComponent={CustomBackrop} // Blure와 같은 뒷 화면에 View를 넣을 수 있습니다.
함수에 대해서는 아래 Reference를 보면 자세히 알 수 있습니다.
https://gorhom.dev/react-native-bottom-sheet/modal
Modal | React Native Bottom Sheet
A performant interactive bottom sheet modal with fully configurable options 🚀
gorhom.dev
추가적으로 BottomSheetView에 또다른 SectionView를 추가하였는데,
const sections = {
"order": (props: OrderProps) => <OrderView {...props}/>,
"orientation":(props: OrderProps) => <OrderView {...props}/>,
"type": (props: OrderProps) => <OrderView {...props}/>,
"colors": (props: OrderProps) => <ColorFilterView {...props}/>,
}
// FilterModal.tsx
let sectionData = {
data: data.filters[sectionName],
modalProps: props,
filterName: sectionName
}
return (
<Animated.View key={index} entering={FadeInDown.delay(index * 100).springify().damping(10)}>
<SectionView
title={title}
content={sectionView(sectionData)}
/>
</Animated.View>
);
// FilterView.tsx
type SectionProps = {
title: string;
content: React.ReactNode;
};
export function SectionView(props: SectionProps) {
return (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{props.title}</Text>
{props.content}
</View>
)
}
// OrderView.tsx
export function OrderView(props: OrderProps) {
function onSelect(item: string) {
props.modalProps.setFilters((prevFilters) => ({
...prevFilters,
[props.filterName]: item,
}));
}
console.log("OrderView Rendering", props.modalProps.filters)
return (
<View style={styles.orderViewContainer}>
{
props.data && <FlashList
data={props.data}
horizontal={true} // 수평으로 설정합니다.
keyExtractor={(data) => data}
extraData={props.modalProps.filters} //FlashList는 data가 변경될 때 Rendering이 되도록 하는데 extraData로 추가적으로 렌더링 할 수 있게 한다.
estimatedItemSize={50}
renderItem={({ item }) => {
console.log("OrderView FlashList Render")
let isActive = props.modalProps.filters[props.filterName] === item;
let backgroundColor = isActive ? theme.colors.neutral(0.7): "white";
let color = isActive ? "white" : theme.colors.neutral(0.7);
return (
<Pressable key={item}
onPress={()=> {
onSelect(item)
}}
style={[styles.outlinedButton,{backgroundColor: backgroundColor}]}>
<Text style={[styles.outlinedButtonText,{color:color}]}>{capitalize(item)}</Text>
</Pressable>
)
}}
contentContainerStyle={styles.flashListContent} // 이 줄을 추가합니다.
/>
}
</View>
)
}
그런데 정말 중요한 것은
ModalView를 사용할 때, 높이와 크기에 대해서 정확하게 지정을 할 수 있어야합니다.
그래야 안정적으로 View가 나올 수 있게 됩니다.
또다른 예시의 코드 입니다.
import React, { useCallback, useMemo, useRef } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Button } from 'react-native';
import { router } from 'expo-router';
import BottomSheet, { BottomSheetFlashList, BottomSheetView, SNAP_POINT_TYPE, useBottomSheetSpringConfigs } from '@gorhom/bottom-sheet';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function TabSecondPage() {
const bottomSheetRef = useRef<BottomSheet>(null);
const handleSheetChanges = useCallback((index: number, position: number, type: SNAP_POINT_TYPE) => {
console.log('handleSheetChanges', index, position, type);
}, []);
const animationConfigs = useBottomSheetSpringConfigs({
damping: 100,
overshootClamping: true,
restDisplacementThreshold: 0.1,
restSpeedThreshold: 0.1,
stiffness: 500,
});
const data = useMemo<string[]>(
() =>
Array(50)
.fill(0)
.map((_, index) => `index-${index}`),
[]
);
const snapPoints = useMemo(() => ["25%", "50%"], []);
const handleSnapPress = useCallback((index: number) => {
bottomSheetRef.current?.snapToIndex(index);
}, []);
const handleClosePress = useCallback(() => {
bottomSheetRef.current?.close();
}, []);
const renderItem = useCallback(({ item }: { item: string }) => {
return (
<View key={item} style={styles.itemContainer}>
<Text>{item}</Text>
</View>
);
}, []);
return (
<View style={styles.container}>
<Button title="Snap To 50%" onPress={() => handleSnapPress(1)} />
<Button title="Snap To 25%" onPress={() => handleSnapPress(0)} />
<Button title="Close" onPress={() => handleClosePress()} />
<BottomSheet
ref={bottomSheetRef}
index={0} // 이 줄을 유지합니다.
onChange={handleSheetChanges}
snapPoints={snapPoints}
animationConfigs={animationConfigs}
enableDynamicSizing={false}
>
<BottomSheetFlashList
data={data}
keyExtractor={(item) => item}
renderItem={renderItem}
estimatedItemSize={43.3}
/>
<BottomSheetView style={styles.contentContainer}>
<Text>Awesome 🎉</Text>
</BottomSheetView>
</BottomSheet>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 24,
marginBottom: 20,
},
button: {
backgroundColor: '#007BFF',
padding: 10,
borderRadius: 5,
},
linkText: {
color: '#FFFFFF',
textAlign: 'center',
},
contentContainer: {
flex: 1,
padding: 36,
alignItems: 'center',
},
itemContainer: {
padding: 6,
margin: 6,
backgroundColor: "#eee",
},
});
'React > Native' 카테고리의 다른 글
EXPO) FlashList로 다양한 크기의 이미지 보여주기 (0) | 2025.02.23 |
---|---|
EXPO) Expo Router로 Navigation 구현하기 (0) | 2025.01.20 |
EXPO) React Native에 TypeScript+Redux ToolKit 적용#1 (0) | 2024.12.08 |
EXPO) EXPO에 앱 업로드 하기 (Simulator & Manual Build) (0) | 2024.12.06 |
ReactNative) ScrollView와 FlatList의 비교 (feat: React의 &&와 ...) (1) | 2023.03.08 |
댓글