-
Recoil 사용해서 Modal 여러개 띄우기Nextjs 2024. 1. 30. 18:57
Modal 여러개 띄우기
AS IS
기존 모달 사용 방식은 Context안에 children을 보내서 createPortal로 띄우는 방식이었다. 그런데 이 방식은 모달이 하나 뜨면 다른 모달이 꺼져버린다는 것이 문제가 되었다. 모달 위에 확인 모달을 띄워야 할 때가 있는데 이 경우에는 dom 구조 바깥에 뜨는 modal의 z-index를 조정한다고 해결이 되지 않아서 변경하기로 마음을 먹었다.
//ModalProvider.js export default function ModalProvider({ children, selector }: Props) { const ref = useRef<any>(null) const [mounted, setMounted] = useState(false) const [modal, setModal] = useState<ReactNode>(null) useEffect(() => { ref.current = document.getElementById(selector) setMounted(true) }, [selector]) const handleClickOverlay = (event: React.MouseEvent<HTMLDivElement>) => { if (event.currentTarget !== event.target) return setModal(null) } const renderModal = () => { return modal ? ( <Overlay onClick={e => { if (clickable) handleClickOverlay(e) return }} role='presentation' > {modal} </Overlay> ) : null } return ( <ModalContext.Provider value={{ setModal, setClickable, setScrollable }}> {mounted ? createPortal(renderModal(), ref.current) : null} {children} </ModalContext.Provider> ) }
TO BE
1. context에서 recoil 전환
- 이미 상태관리 라이브러리로 recoil을 사용하기로 결정했고 따로 context를 사용해서 modal만의 state를 관리하는건 불필요하다고 생각했다.
- 또한 여러 모달을 띄울 수 있게 할 예정이기 때문에 추적과 관리가 용이할 수 있으면 좋을 것 같았다.
- context는 상태 변경 감지를 위해 다시 렌더링되기 때문에 맘에 안들었다.
export type ModalType = { type: string children: ReactNode isCloseable?: boolean } export const modalState = atom<Array<ModalType>>({ key: 'modalState', default: [], })
모달이 쌓이면 이런식으로 나오게 된다.2. hook으로 만들기
그리고 이걸 hook으로 만들어서 어디서나 편하게 가져다 쓸 수 있게 만들었다.
export function useModal() { const [modal, setModal] = useRecoilState(modalState) const openModal = (newValue: ModalType) => { setModal(oldModalState => { const isCloseable = newValue.isCloseable ?? false return oldModalState.concat({ ...newValue, isCloseable: isCloseable, }) }) } const closeModal = (type: string) => { setModal(oldModalState => { const newModalState = [...oldModalState] newModalState.pop() return newModalState }) } return { modal, openModal, closeModal } } export default useModal
3. Provider 만들기
import React, { useEffect } from 'react' import { createPortal } from 'react-dom' import { styled } from '@mui/system' import useModal, { modalState, ModalType } from '@src/hooks/useModal' import { useRecoilValueLoadable } from 'recoil' function ModalContainer() { const modalList = useRecoilValueLoadable(modalState) const { closeModal } = useModal() const handleClickOverlay = ( name: string, event: React.MouseEvent<HTMLDivElement>, ) => { if (event.currentTarget !== event.target) return event.preventDefault() //⬇️ 이 코드가 없으면 이벤트 버블링에 의해 자동으로 closeModal이 실행되면서 중첩 모달을 띄울 수 없게 됨 event.stopPropagation() closeModal(name) } useEffect(() => { if (modalList.getValue().length > 0) { document.body.style.cssText = ` position: fixed; top: -${window.scrollY}px; overflow-y: scroll; width: 100%;` return () => { const scrollY = document.body.style.top document.body.style.cssText = '' window.scrollTo(0, parseInt(scrollY || '0', 10) * -1) } } }, [modalList]) function isClientSideRendering() { return typeof window !== 'undefined' } const element = typeof window !== 'undefined' ? document.getElementById('modal') : null const renderModal = modalList .getValue() .map(({ type, children, isCloseable = true }: ModalType) => { return ( <Overlay key={type} onClick={e => { isCloseable && handleClickOverlay(type, e) }} > {children} </Overlay> ) }) return isClientSideRendering() ? element && renderModal && createPortal(renderModal, element) : null } export default ModalContainer
_app.tsx에 import 해서 호출하는 layout보다 위에 선언해주면 된다. _doucment.tsx에 body 안쪽에도 <div id='modal' /> 넣어주면 된다.
4. 간편하게 사용하기
openModal({ type: '' , children : <Modal /> }) //type에는 unique한 이름, children에는 띄우고 싶은 컴포넌트 closeModal('') //openMdoal에서 type에 적는 unique한 이름
이렇게 모달 위에 모달이 잘 뜨게 된다!참고 : https://velog.io/@rkio/React-ReactDOM.createPortal
https://ko.reactjs.org/docs/portals.html
https://jeonghwan-kim.github.io/2022/06/02/react-portal'Nextjs' 카테고리의 다른 글
Nextjs(typescript) 에서 Web worker 사용하기 (0) 2024.02.01 Input type이 number일 때 maxLength 지정안될 때 (0) 2024.01.30 사용자의 페이지 이탈 감지하기 (1) 2024.01.30