일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 개발자
- React
- 차이
- css
- 비동기
- 취업
- 공부
- http
- 코딩테스트
- TypeScript
- 알고리즘
- JavaScript
- Sass
- csr
- 에러
- 백준
- 코딩
- dynamic import
- Next.js
- 서버
- React Query
- SSR
- Browser
- 취업준비
- 프론트엔드
- html
- DOM
- error
- Vite
- Git
- Today
- Total
minTech
[React] react portal로 모달창 만들기 (feat. 이벤트 버블링) 본문
프로젝트를 진행하면서 본격적인 페이지 구현에 들어가기에 앞서 모달창 구현을 맡았다.
전체적인 모달창은 react portal을 이용해 만들었다.
처음에 프로젝트를 진행하기 전까지 react portal을 몰랐고, 사용해본 적도 없었는데 이를 사용해서 모달창을 만들면 어떻냐는 팀원분의 의견에 공부해볼겸 정면돌파 해보았다.
그럼 내가 지금까지 이해한 react portal에 대해서 정리해보겠다.
🕵️ React Portal
"포탈"이라는 단어를 생각해보면 말 그대로 순간이동의 느낌이 든다. 원하는 목적지를 선택하면, 그 목적지로 바로 이동할 수 있다.
react potal를 사용하면 이러한 간단한 원리를 통해 모달창을 표시할 수 있다.
먼저 내가 필요한 모달 컴포넌트를 생성하고, 모달이 화면에 표시되어야 할 목적지만 설정해주면 부모 컴포넌트가 무엇이든 상관없이 설정된 목적지로 해당 컴포넌트가 바로 이동하여 렌더링 된다.
👀 왜 사용할까?
모달창을 하나의 컴포넌트로 만들어 해당 페이지 내에서 필요할 때 호출하면 되는데 왜 굳이 portal을 사용하여 모달창을 구현하는지 궁금했다.
다양한 인터넷 속의 글을 보았고, 정말 여러가지 다양한 이유들이 있었다. 많은 이유들을 간단하게 작성해보겠다.
(해댱 출처는 아래에 작성하였습니다.)
1. CSS 스타일링의 단순화
기존의 react portal을 사용하지 않고 모달창을 구현했을 때는 모달 창이 모달창이 아닌 모든 요소들보다 위로 띄워야하기 때문에 이렇게 다른 요소들간의 계층 구조를 z-index 속성 값을 넣어주어 직접 다루어야 했다. 만약 화면 내의 어떤 컴포넌트가 z-index 값이 모달보다 큰 값을 갖고 있엇더라면 모달 밖으로 컴포넌트가 밝게 보이게 되는 문제가 있어 만약 그런문제가 생긴다면 그에 맞게 z-index 값을 다시 조정하는 것이 까다로울 수도 있다.
하지만 react portal을 사용하게 되면 모달창의 부모 컴포넌트가 무엇이든 독립적인 위치로 렌더링되기 때문에 다른 요소들 간의 계층 구조를 신경 쓸 필요가 없게 된다. 따라서 더욱 스타일링이 더욱 간편하고, 다른 요소들과의 CSS 충돌을 예방할 수 있다.
2. 모달의 존재감 유무
만약 css 충돌없이 화면에 모달창이 정상적으로 렌더링 되었다고 해도, HTML 코드 구조 상 해당 컴포넌트 이름이 모달이 아닌 test 라고 해놓는다고 임의로 정해보면 코드만으로는 test 가 모달인지 아닌지 인식하기가 어렵다.
3. 불필요한 렌더링의 최소화
만약 모달창을 react portal 을 사용하지 않고 구현했다고 가정해보면, 모달창의 표시 유무를 위해 state 값을 생성할 것이다. 리액트의 경우 컴포넌트 내에서 state 값이 변하면 해당 컴포넌트와 자식 컴포넌트 모두 다시 렌더링된다.
react portal를 사용하여 모달창을 구현할 경우 변경된 부분만 렌더링되기 때문에 불필요한 렌더링을 최소화할 수 있다.
✏️ 그렇다면 사용법을 파헤져보자
1. 모달창의 목적지 선택하기
모달 창이 열리면 화면의 어떠한 컴포넌트보다 모달창이 우선순위이다. 따라서 다른 컴포넌트에 밀리지 않기 위해서
목적지는 제일 우선순위가 높은 최상단 root 로 설정해주어야한다.
해당 코드에서는 최상단에 위치한 layout.tsx 파일 속 root의 바디에 "modal-root" 라는 id 명을 부여했다.
// layout.tsx
import type { Metadata } from 'next';
import React from 'react';
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<body>
<div id="modal-root" />
</body>
</html>
);
}
2. modal portal을 생성하여 모달창의 목적지로 설정
포털을 생성하기 위해서는 createPortal을 호출하여 아래와 같이 인수를 전달해준다.
createPortal(children, domNode, key?);
- children: 해당 위치로 이동하여 렌더링 시킬 jsx
- domNode: 목적지로 설정할 DOM 노드
나는 위에서 layout의 modal-root 라는 id 명을 설정한 body를 목적지로 설정하고 싶기 때문에 아래 처럼 getElemenyByid로 해당 DOM 노드를 불러와 넣어주었다.
그리고 해당 위치로 렌더링 시킬 jsx 는 modal 의 타입에 따라 다르게 렌더링하기 위해 아래와 같이 setOpenModalType에 들어온 모달의 타입에 따라 다르게 렌더링되도록 설정하였다.
// modalPortal.tsx
import React from 'react';
import { createPortal } from 'react-dom';
import styles from './ModalPortal.module.css';
import ModalContents from '../ModalContents';
const ModalPortal = ({
openModalType,
setOpenModalType,
}: ModalPortalPropsType) => {
if (openModalType === '') return null;
const modalContent = ModalContents({
openModalType,
setOpenModalType,
});
return createPortal(
<div className={styles.modalBackground}>{modalContent}</div>,
document.getElementById('modal-root') as HTMLElement,
);
};
export default ModalPortal;
3. modal 사용
이제 세팅은 끝났다!!
모달 타입에 따른 자식 컴포넌트를 만들어 정의하고, 모달을 사용하기 위해서는
사용할 페이지 내에서 modalPortal 컴포넌트를 import 하고 원하는 타임에 모달의 타입만 넣어 prop으로 넘겨주면 된다.
// 모달창을 열 페이지 내
const ModalPage = () => {
const [openModalType, setOpenModalType] = useState(null);
setOpenModalType('deleteModal');
<ModalPortal
openModalType={openModalType}
setOpenModalType={setOpenModalType}
/>
😭 주의할 점
react 공식 문서를 보면 " 포털은 DOM 노드의 물리적 배치만 변경할 뿐 이벤트는 react 트리에 따라 자식에서 부모로 버블린된다. " 라는 글이 나온다.
이 부분을 자세하게 읽지 않고 모달창을 막무가내로 구현하다가 문제가 발생했다 ㅜ
😂 문제 발생
필요한 모달을 띄우고, 팝업창을 통해 그 자리에 다른 모달을 띄우려는 상황이었다.
하지만 두 번째로 띄워지는 모달을 클릭만 해도 해당 모달이 닫히는 것이었다!
정말 한 2,3 시간...? 정도 오류와 사투를 벌인 후 문제의 원인을 알아냈다...!!
🤷♂️ 문제 원인
문제의 원인은 바로 이벤트 버블링이었다.
이벤트 버블링 ( Event Bubbling)
자식 컴포넌트에서 발생한 이벤트가 부모 요소로 거품처럼 전파되는 현상
첫 번째로 띄워지는 모달창과 두 번째로 띄워지는 모달창은 독립적인 컴포넌트이기 때문에 각각의 이벤트를 넣어준다고 해도 영향이 없을 줄 알았다.
하지만 알고보니 첫 번째 모달창의 외부를 클릭할 때 모달창이 닫히는 이벤트를 발생하도록 구현하였는데 이 이벤트가 부모 컴포넌트까지 전파된 것이다. 따라서 이 상태에서 modal portal을 통해 다음 모달창이 바로 열리면서 두 번째 컴포넌트를 내부를 클릭해도 첫 번째 모달창의 외부를 클릭 한 것과 동일한 효과가 발생하는 것이다.
이러한 원인으로 인해 관계없는 모달창의 내부를 클릭해도 해당 모달창이 닫히는 것이었다.
😄 문제 해결!
이 원인을 깨닫고 두 번째 모달창 자체에 ref를 통해 해당 모달창 클릭 시 e.stopPropagation() 함수를 실행하게 만들어서 이벤트 버블링을 중지시키도록 하였다. 그랬더니 문제가 해결되었다.
// 두 번째 모달창의 코드 중 일부분
const handleClickInsideModal = (e) => {
e.stopPropagation();
};
useOutsideClick(modalRef, handleOutsideClick);
return (
<div onClick={handleClickInsideModal}>
정말 이벤트 버블링과 캡처링에 대해서 이론적으로만 알았지 직접 이 문제를 맞닥뜨리게 될 줄 몰랐다,,,
역시 이래서 사람은 부딪혀봐야 된다..!
처음에는 portal에 대해서 이론적으로 이해하고, 이를 모달창 구현에 어떻게 사용되는지 굉장히 이해가 안되고 어려웠는데 공식문서에 나와있는 간단한 예시를 혼자 직접 따라해보니까 이게 무엇인지 확 다가오는 것 같았다.
이론을 처음 마주했을 때, 막연하게 무슨 말인지 모르겠고 이 기능을 빨리 사용해야한다면 무작정 간단한 예시를 따라해보면서 차근차근 읽어나는 것도 좋은 것 같다.
만약 모달이 별로 사용되지 않는 프로젝트라면 portal을 통해 굳이 만들지 않아도 되지만, 이번에 수행했던 프로젝트 같이 모달창이 많고, 일관된 디자인이 여러 개 사용되는 모달 창이 다수인 경우에는 portal을 통해 구현하는 것이 훨씬 좋은 것 같다.
물론 모달창 때문에 우여곡절이 많았지만 굉장히 많은 것을 알아내고 깨달은 것 같아 뿌듯하다! ✌️
출처
'React' 카테고리의 다른 글
[React] Prettier 적용이 안된다면? (1) | 2024.10.04 |
---|---|
[React] 컬러 피커(color-picker) 사용하기 (1) | 2024.05.25 |
[React. Error] 'error:03000086:digital envelope routines::initialization error' (0) | 2024.04.09 |
[React, Project] "react-slick"을 이용해 커스텀 화살표를 추가한 캐러셀 구현하기 (feat. Unknown props currentSlide, slideCount Error) (0) | 2024.03.21 |
[React] intersection observer API를 이용해 무한 스크롤 구현하기 (0) | 2024.03.07 |