-
[ React Native ] 직접 Popup 만들기Application/React Native 2025. 10. 8. 19:18반응형
항상 프로젝트를 하면 라이브러리를 많이 사용하는데
popup이나 modal같은 경우는 항상 직접만들게 된다.
라이브러리를 사용하면 커스텀이 복잡하여 원하는 디자인으로 만들기가 어려워서 직접 만들게 된다.
react native에서도 마찬가지로 직접 만들어 보았다.
react native에서는 Modal컴포넌트가 있어서 좀 쉽게 구현이 되는데 이것저것하다가 Modal이 재대로 적용이 안되어서
그냥 다른 방식으로 만들기로했다.
Initialized
portal은 portal로 감싼 자식들을 최상위로 보내준다.
web으로 따지만 자식 컴포넌트이지만 <body>최상위로 보내주는 것과 같다고 생각하면 된다.
Modal 컴포넌트도 portal처럼 작동하지만 뭔가 꼬여버린지 잘 안되어서 portal을 그대로 사용했다.
npm install react-native-portalize
직접 popup을 만들때 portal이나 Modal을 사용하는 이유는 아래에 하면서 설명을 진행.
Useage
우선 직접 팝업을 만들어보자
더보기BoxView는 그냥 박스를 만들어주는 컴포넌트를 만들었다.
AppText는 테마적용에따라서 Text의 color가 바뀌어야하는데 react Native에서는 View의 style에서 color가 안들어가서
테마별로 글자 색이 변경되도록하는 컴포넌트를 만들었다.
여기서 portal이 사용된다. 팝업부분은 portal로 최상위로 위치하게 해준다.
이러면 portal은 부모 view의 영향을 받지 않게된다.
그 과정에서 position: absolute를 주면 전체 화면의 기준이 되므로 ref 참조로 현재 버튼의 위치를 가져와서 popup의 위치를 적용시켜줘야한다.
그래서 팝업이 열릴때 buttonRef의 현재 화면을 기준으로 x,y좌표와 버튼의 크기를 가져와서 위치를 잡아준다.
setTimeout을 걸어주는 이유는 팝업이 열릴때에는 팝업이
레이아웃 계산이 끝나기 전에 measureInWindow를 호출시 좌표가 다르게 나오게 되어 팝업 위치가 재대로 안잡히기에
setTimeout으로 호출 시간의 타이밍을 늦췄다.
const SelectPopup = <T extends Record<string, string>>(props: Props<T>) => { const [isOpen, setIsOpen] = useState(props.open) const [buttonLayout, setButtonLayout] = useState<Layout | null>(null); const buttonRef = useRef<View>(null); useEffect(() => { setIsOpen(props.open) }, [props.open]); useEffect(() => { if (!isOpen) return; // 팝업이 열릴 때만 setTimeout(() => { buttonRef.current?.measureInWindow((x, y, width, height) => { console.log('screen coords:', x, y, width, height); setButtonLayout({ x, y, width, height }); }); }, 0); }, [isOpen]); return ( <View> <View ref={buttonRef} style={styles.select}> <AppText>{props.list[props.value]}</AppText> <SvgIcon name="ArrowRight"/> </View> { isOpen && buttonLayout != null && <Portal> <BoxView style={[styles.boxView, { position: 'absolute', top: buttonLayout.y + buttonLayout.height + 10, // 버튼 아래 left: buttonLayout.x, // 버튼 왼쪽 정렬 boxShadow: theme.app_theme_box_shadow}]}> {Object.entries(props.list).map(([key, label]) => ( <Pressable key={key} onPress={() => props.onSelect(key)} style={({ pressed }) => [ styles.selectItem, pressed && { backgroundColor: theme.app_theme_itemHover } ]} > <AppText>{label}</AppText> </Pressable> ))} </BoxView> </Portal> } </View> ) }
우선 스타일은 이렇게 주었다.
const styles = StyleSheet.create({ select: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 5, }, boxView: { width: 'auto', borderRadius: 16, padding: 0, zIndex: 2, overflow: 'hidden', alignSelf: 'flex-start', }, selectItem: { padding: 10, alignSelf: 'flex-start' } })
Overlay
팝업을 만들고 난 후에는 이제 팝업 외부를 터치하면 팝업이 닫히도록 구현을 해야한다.
web에서는 클릭 이벤트로 현재 클릭 위치를 가져와서 쉽게 구현이 가능했는데
react native에서는 overlay를 만들어서 직접 overlay위에 popup을 띄우고 overlay클릭시에 팝업이 닫히게 해야한다.
이것이 popup을 portal을 써서 최상위로 보내는 이유이다
reactNative 에서는 zIndex를 써도 감싸진 View를 기준으로만 적용이 된다.
또한 width와 height도 감싸진 View의 기준으로 되며 이 크기가 넘어가지지 않았다.
그러다보니 Overlay는 최상위 부모 View에 위치해야하며, popup은 자식 View안에서 만들어졌기에
이 두개는 서로 zIndex가 따로적용되기때문에 portal을 활용하여 popup의 위치를 overlay보다 더 높은 위치에 만들어줘야한다.
Overlay컴포넌트를 만들고 width랑 height는 화면 크기를 가져와서 설정해주었다.
화면을 채우기 위해서는 부모 View의 크기가 화면 전체 크기가 적용된 상태 flex: 1인 상태인 곳에 위치해야한다.
테스트를 위해 background에 색상을 넣어주었다.
또한 Overlay는 팝업이 열리면 항상 나와야하기때문에 popup이 열릴때의 상태관리를 했고,
overlay를 클릭했을때 상태값이 false로 바뀌면 팝업의 isOpen 값도 false로 바뀌게 처리
import {create} from "zustand/react"; interface DialogState { overlay: boolean, setOverlay: (val: boolean) => void } export const useDialogStore = create<DialogState>((set,get) => ({ overlay: false, setOverlay: (val) => set({overlay: val}) }))
import {Dimensions, Pressable, StyleSheet,} from "react-native"; import {useDialogStore} from "@/store/dialogStore.ts"; const AppOverlay = () => { const dialogStore = useDialogStore() if(!dialogStore.overlay) { return null } const onPressOverly = () => { dialogStore.setOverlay(false) } return ( <Pressable style={styles.container} onPress={onPressOverly} /> ) } const {width, height} = Dimensions.get('window') const styles = StyleSheet.create({ container: { position: 'absolute', width: width, height: height, backgroundColor: 'red', } }) export default AppOverlay
현재 모든 자식의 최상위는 AppView 컴포넌트를 만들었고 여기서 header와 content가 만들어진다.
header는 overlay가 적용안되게 처리하고싶어서 overlay컴포넌트 위치를 이렇게 잡았다.
const AppView = (props: AppViewProps) => { return ( <SafeAreaView style={[styles.safeArea, { backgroundColor: theme.app_theme_background }]}> { props.header && (props.headerOption?.type ?? true) ? <AppHeader/> : <AppHeader type={false}/> } <View style={[styles.container, { backgroundColor: theme.app_theme_background }]}> {props.children} <AppOverlay/> </View> </SafeAreaView> ) } const styles = StyleSheet.create({ safeArea: { flex: 1, }, container: { flex: 1, padding: 20, }, })
그러면 팝업이 열리면 overlay가 화면이 감싸지고 팝업이 닫히면 overlay가 사라진다.
728x90반응형'Application > React Native' 카테고리의 다른 글
[ React Native ] i18n 적용 언어변경하기 (0) 2025.10.12 [ React Native ] Stack Navigation (0) 2025.10.08 [ React Native ] AsyncStorage key 값 저장 (0) 2025.10.06 [ React Native ] 다크모드 테마설정 간단하게 처리하기 (0) 2025.10.06 [ React Native ] react-navigation bottom-tabs (0) 2025.10.05