ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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

Designed by Tistory.