본문 바로가기

React.js

(React) Next.js 에서 리액트 포털로 모달 만들기

* 문제점

모달 컴포넌트를 만들 때 우리는 흔히 useState을 잔뜩 사용해서

모달에 입력 받아야 하는 상태값들을 나열해준다.

 

그리고 모달을 띄울 boolean 값과 모달 엘리먼트의 position을

fixed로 설정하고 표시를 한다. 만일 가입이나 확인 버튼이 있다면

모달의 boolean 값을 true로 하고 background을 클릭할 경우

false로 하는 방법을 흔히 이용한다.

 

위의 방법은 간단한 방법으로 모달 컴포넌트를 구현하고 작동시켜 준다.

하지만 재사용성의 관점에서는 재활용할 께 딱히 없어

코드 복붙 외에는 좋은 코드라고 할 수 가 없다.

 

* 해결방법 - 리액트 포털

ReactDOM.createPortal(child, container)

포털은 '부모 컴포넌트 DOM 계층 외부에 있는 DOM 노드로 자식을 렌더링' 하는 방법이다.

Portal 컴포넌트를 만들고 Portal 컴포넌트는 모달 컴포넌트를 만드는 로직을 구현한다.

 

body에 Portal 컴포넌트가 새롭게 생성할 모달 컴포넌트의 자리를 id로 잡아놓고

Portal 컴포넌트에서 document.querySelector 함수로 그 자리를 불러와 거기다가

모달 컴포넌트를 렌더링한다.

 

우선 _app.tsx 에 root-modal을 만든다.

 

pages/_app.tsx
function MyApp({ Component, pageProps }: AppProps) {
  return (
    <>
      <GlobalStyle />
      <BaseLayout>
        <Component {...pageProps} />
        <div id="root-modal"></div>
      </BaseLayout>
    </>
  )
}

export default MyApp

 

Portal 컴포넌트에서는 #root-modal 에 모달 컴포넌트를 렌더링 시킨다.

components/ModalPortal.tsx
  const ModalPortal: React.FC<IProps> = ({ children }) => {
    const ref = useRef<Element | null>()
    const [mounted, setMounted] = useState(false)

    useEffect(() => {
      setMounted(true)
      if (document) {
        const dom = document.querySelector('#root-modal')
        ref.current = dom
      }
    }, [])

    if (ref.current && mounted && modalOpened) {
      return createPortal(
        <Container>
          <div
            className="modal-background"
            role="presentation"
            onClick={closeModal}
          ></div>
          {children}
        </Container>,
        ref.current,
      )
    }
    return null
  }

  return {
    openModal,
    closeModal,
    ModalPortal,
  }
}

export default ModalPortal

 

이제 ModalPotal의 자식 컴포넌트로 사용할

<Modal /> 컴포넌트를 ModalPortal의 아래에 위치시킨다.

 

components/Header.tsx
<ModalPortal>
  <Modal />
<ModalPortal />