연습 프로젝트에서 모달을 구현하던 중 리액트 포털이라는 개념을 알게되었다.
요소, 컴포넌트에 대한 실제 HTML 코드를 JSX 코드가 작성된 위치를 기준으로 렌더링하지 않고, DOM의 다른 위치로 렌더링되게 한다.
프로젝트의 컴포넌트 구조는 다음과 같다.
// App.tsx
return (
<>
<AddUser/>
<UserList/>
</>
)
// AddUser.tsx
return (
<>
{error && <ErrorModal/>}
<div>
...
</div>
</>
)
루트 컴포넌트에서 UserList
, AddUser
컴포넌트를 사용하고, AddUser
컴포넌
트에서 에러 상태에 따라 ErrorModal
컴포넌트를 렌더링한다.
개발자 도구에서 살펴보면, 에러가 발생하는 상황에서 모달이 DOM에 렌더링 되고 정상적으로 동작한다.
하지만, 의미적인 관점이나 간결한 HTML 구조를 갖췄는지의 관점에서 살펴본다면 이 코드, 구조는 완벽하지는 않다.
왜냐하면, 모달
은 기본적으로 전체 페이지 위에 표시되는 오버레이이기 때문이다.
따라서 다른 모든 요소 위에 존재해야 한다.
그런데, 현재의 모달은 다른 HTML 요소 안에 중첩되어 있다. 기술적으로 CSS 스타일 덕분
에 동작은 하지만 좋은 코드, 구조를 가지고 있지는 않다.
즉 모달
이 다른 요소 안에 중첩되어 있기 때문에 모달
이 다른 모든 페이지에 대한 오버레이인지 명확하지 않다.
(모달 뿐만 아니라 사이드 드로어, Dialog 같은 모든 종류의 오버레이도 마찬가지이다)
(현재에는 단순한 프로젝트이기 때문에ErrorModal
컴포넌트가 root
컴포넌트 바로 아래(상단)에서 AddUser
컴포넌트와 함께 존재하고 있지만, 복잡한 프로젝트의 DOM 구조에서는ErrorModal
이 더 중첩된 곳에 존재할 수도 있다. 이럴 경우에도 당연히 의미적으로나 구조적으로 옳지 않다.)
리액트 포털을 사용하면 기존의 JSX 코드를 거의 유지하면서 렌더링된 모달의 HTML 코드를
다른 위치로 옮길 수 있다. (전체 페이지의 최상단 : body의 자식으로)
즉 모달과 같은 전체 페이지에 대한 오버레이가 다른 요소들 안에 중첩되어 있는 구조적인 문제를 해결할 수 있다.
리액트 포털을 사용하기 위해 두 단계가 필요하다.
- 컴포넌트가 이동할 장소
- createPortal을 사용해 컴포넌트를 이동시키기
컴포넌트가 이동할 장소를 표시하기 위해 index.html
에 다음과 같이 작성해 준다.
backdrop-root
과 modal-root
은 각각 모달 바깥쪽에 대한 HTML 코드와 모달 자체에 대한 HTML 코드가 이동할 위치이다.
이렇게 작성해주면 나중에 body
의 자식 위치에(페이지 최상단에) 모달의 HTML 코드가 렌더링 될 것이다.
위치를 정했으면 리액트에게 이 위치로 이동되어야 한다고 알려줘야 한다.
먼저, react-dom
라이브러리의 createPortal
메소드를 사용하기 위한 import를 해준다.
그리고 기존에 모달 코드가 작성되었던 JSX 코드에서 createPortal
메소드를 호출한다.
createPortal
메소드는 두 개의 인수를 취한다.
참고 : 2번 째 인수를 작성할 때 DOM API
를 이용해 실제 렌더링 되어야 하는 요소를 선택해야 한다.
document.getElementById()
는 일반적으로 리액트 프로젝트에서는 직접 사용하진 않지만, 리액트 포털을 사용하기 위해선 실제 DOM 요소에 접근해야 하므로 명시적으로 코드를 작성한다.
createPortal
을 사용하지 않은 기존 코드 return (
<>
<div className={styles["backdrop"]} onClick={props.onCloseModal}></div>
<Card className={styles["modal"]}>
<header className={styles["header"]}>
<h2>{props.title}</h2>
</header>
<div className={styles["content"]}>
<p>{props.message}</p>
</div>
<footer className={styles["actions"]}>
<Button onClick={props.onCloseModal}>닫기</Button>
</footer>
</Card>
</>
);
createPortal
메소드를 사용하는 코드 return (
<>
{ReactDOM.createPortal(
<div className={styles["backdrop"]} onClick={props.onCloseModal}></div>,
document.getElementById("backdrop-root")!
)}
{ReactDOM.createPortal(
<Card className={styles["modal"]}>
<header className={styles["header"]}>
<h2>{props.title}</h2>
</header>
<div className={styles["content"]}>
<p>{props.message}</p>
</div>
<footer className={styles["actions"]}>
<Button onClick={props.onCloseModal}>닫기</Button>
</footer>
</Card>,
document.getElementById("overlay-root")!
)}
</>
);