지인들과 있는 채팅방에서 modal을 구현할 때 어떤방식으로 하느냐에 대해 얘기가 나왔는데 createPortal를 사용하여 렌더링을 하게된다면 React 컴포넌트 트리 외부에 있는 DOM 노드로 컴포넌트를 렌더링할 수 있다.
처음보는 방식이라 관련된 내용들을 찾아보며 portal을 적용해 보았다.
적용하면서 배운 방식에 대해 내용을 남겨보려고 한다.
리액트 공식문서에 따르면,
Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법을 제공한다고 한다.
ReactDOM.createPortal(child, container)
첫번째 인자로 들어가는 렌더링 할 수 있는 React 자식이 들어가야하고
두번째 인자로는 렌더링 될 DOM 엘리먼트이다.
즉, 우리가 렌더링 할 모달 컴포넌트라고 볼 수 있다.
기존의 전형적인 모달 생성방식을 통해 모달을 띄워줄 부모컴포넌트에서
<Modal/>
을 렌더링하면 부모 노드에서 가장 가까운 자식으로 DOM에 나타나진다.
아래와 같이 부모컴포넌트 HomeBgColor
내부에서 사용한 Modal은
자식 DOM으로 렌더링 된다.
React에서 제공하는 createPortal
을 사용해서 Modal을 띄운다면
부모컴포넌트와 관계 없이 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링 해준다.
이렇게 되었을 때 이점은,
- 현재 컴포넌트와 다른 DOM트리에서 생성하게 됨으로 서로의 영향을 최대한 받지 않도록 할 수 있다.
✔️ style을 전역적으로 관리한다면 style 관심 분리가 가능하다.
- 실제 DOM 위치에서는 분리되지만 리액트 어플리케이션 컴포넌트 트리 구조를 따르기때문에 관심사 분리는 가능하지만 이벤트 전달에는 무리가 없다.
✔️ portal에서 발생한 이벤트는 이벤트버블링 때문에 부모 컴포넌트에서 이벤트를 확인할 수 있다.
✔️ Modal 컴포넌트가 렌더링 된다면 부모는 portal에서 생성되었는지와 관계 없이 Modal에서 발생한 이벤트를 확인 할 수 있다.
Modal에 넘겨줘야하는 상태와 state 값들을 전역스토어에 의존하지 않고 쉽게 전달해 줄 수 있다.
confirm 이후의 동작을 모달에 넘겨주거나 컨펌 상태를 받아서 모달을 띄운 컴포넌트가 처리를 해줘야하는데 그렇게 된다면,
modal, componentA, store 세 곳에서 자식으로 관리를 하게 되는데 상단에서만 관리하게 된다면 실제 DOM은 다른곳에 위치할 수 있어 관리하기 좋다고 하는 것 같다.
부모의 CSS 종속성 관계에서 벗어나서 modal을 생성 할 수 있는 장점도 있다.const ComponentA = () => { const [showModal] = useState const handleConfirm = useCallback return ( <> <div>...</div> {showModal && <ModalA onConfirm={handleConfirm}/>} </> ) }
modal-root
(Portal) 생성import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head></Head>
<body>
<Main />
<div id='modal-root' />
<NextScript />
</body>
</Html>
);
}
위와 같이 document를 수정하고 modal을 띄운다면
지정해놓은 div 자식으로 modal이 생성 되는걸 확인 할 수 있다.
처음에는 다른 블로그 글들을 참고해서 Portal이란 컴포넌트를 하나 생성하여서
props로 전달받은 selector
에
화면에 나타 낼 Modal들을 렌더링 하는 방식으로 코드를 작성하였었다.
import { ReactNode, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
interface PortalProps {
children: ReactNode;
selector: string;
}
const Portal = ({ children, selector }: PortalProps) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
return () => setMounted(false);
}, []);
const element =
typeof window !== 'undefined' && document.querySelector(selector);
return mounted
? element && children && createPortal(children, element)
: null;
};
export default Portal;
위 코드는
컴포넌트가 mount 되었을 때 모달을 띄울 element를 먼저 확인하고
1. 마운트가 되었다면
2. element가 있고
3. props로 전달받은 children이 있고
4. 포탈을 생성해라
5. 아니라면 null을 리턴한다.
이렇게 코드를 작성하게 된다면 원하는대로 동작은 하지만
return
문에서 삼항연상자 조건을 mounted
로 전달해 놓고
그 뒤에 조건문을 더 붙이기 때문에 이 방법은 잘못되었다.
mounted && element && children
?
:
삼영연산자의 조건에는 모든 조건이 들어가야 된다.
그리고 나는 Modal, Popup 등 여러방식을 사용하지 않을 예정이여서
만들었던 Portal
컴포넌트를 삭제하고 단독으로 사용 할
Modal
컴포넌트에 Portal을 적용하는 방식으로 수정 하였다.
const Modal = () => {
return (
<>
{isOpen &&
createPortal(
<>
<ModalOverlay />
<ModalWrapper >
{children}
</ModalWrapper>
</>,
document.querySelector('#modal-root') as HTMLDivElement
)}
</>
);
};
모달을 띄워 줄 portal은 한개만 존재하기 때문에
selector를 전달받을 필요 없이 document
에 생성해 놓은 div를
고정 값으러 전달 해 주었고, isOpen
이라는 값을 전달 받아
모달이 열렸다면 Portal에 모달이 생성 되도록 전달 해 주었다.
type 에러로 인해 as HTMLDivElement
로 타입을 정의해 주었는데
리턴하는 곳에서 타입캐스팅을 쓰는 방법 보다는
리턴 문 이전에 조건문을 걸어 주는 방법이 더 낫다는 피드백을 받았다.
const portalDiv = document.querySelector('#modal-root')
if(!portalDiv) return null;
위에서 Div가 없을 경우에 null로 리턴해 주기 때문에
return문에 들어가는 코드가 조금 더 간결해지고 명확하도록 작성 할 수 있다!
관련된 내용을 공부하면서 여러가지의 글들을 찾아보았는데,
기존에 사용하는 DOM 과 다른 DOM에서 렌더링을 할 수 있기 때문에 관리하기가 좋다 라는 문구를 제일 많이 본거 같다.
생각보다 내용이 와닿지 않아서 좀 더 찾아보게 되니
css style
에 대한 관심사 분리에 이점도 찾아볼 수 있게 되었고
결론적으로는
실제 DOM 위치에서는 분리되지만 리액트 어플리케이션 컴포넌트 트리 구조를 따르기때문에 관심사 분리는 가능하지만 이벤트 전달에는 무리가 없다.
이 점이 제일 포인트인 거 같다.
createPortal을 사용하게 되면서,
코드가 어떠한 방식으로 동작하는것은 이해할 수 있지만
그것을 내 상황에 맞추어 적용시키는것은 아직 조금 어려운 것 같다.
이 또한 열심히 공부하고 내가 어떠한 동작을 구현해야하는지와
각 컴포넌트에 대한 관심사 구분을 명확히 해야겠다는 생각을 했다
리액트 공식문서 : https://ko.reactjs.org/docs/portals.html
참고한 블로그
추가로 읽어보면 좋은 아티클