- Published on
React Portal로 Modal 만들기. useModal 훅 만들기
- Authors

- Name
- dwook
Table of Contents
React Portal
- Portal은 부모 컴퍼넌트의 DOM 계층 외부에 있는 DOM 노드로 자식을 렌더링하는 방법
- 첫 번째 인자로 리액트 컴퍼넌트를 받고, 두 번째 인자로 리액트 컴퍼넌트를 넣은 DOM을 받음
ReactDOM.createPortal(child, container)
일반적인 모달 만들기
pages/_app.tsx
const app = ({ Component, pageProps }: AppProps) => {
return (
<>
<GlobalStyle />
<Header />
<Component {...pageProps} />
<div id="root-modal" />
</>
);
};
components/Mo
import React, { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import styled from "styled-components";
const Container = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
left: 0;
z-index: 11;
.modal-background {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.75);
}
`;
interface ModalProps {
children: React.ReactNode;
closePortal: () => void;
}
const ModalPortal: React.FC<ModalProps> = ({ children, closePortal }) => {
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) {
return createPortal(
<Container>
<div
className="modal-background"
role="presentation"
onClick={closePortal}
/>
{children}
</Container>,
ref.current
);
}
return null;
};
export default ModalPortal;
components/Header.tsx
import ModalPortal from "./ModalPortal";
import SignUpModal from "./auths/SignUpModal";
{
modalOpened && (
<ModalPortal>
<SignUpModal />
</ModalPortal>
)
}
재사용성을 높인 useModal
- 모달을 사용하기 위해 부모에 상태를 하나 만들어야하고, ModalPortal에 props로 모달을 닫는 함수를 전달하는 일은 번거롭기 때문에 커스텀 훅으로 만들기.
- ModalPortal 컴퍼넌트를 리턴하기 때문에 .tsx 파일
hooks/useModal.tsx
import React, { useRef, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import styled from "styled-components";
const Container = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
left: 0;
z-index: 11;
.modal-background {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.75);
}
`;
const useModal = () => {
const [modalOpened, setModalOpened] = useState(false);
const openModal = () => {
setModalOpened(true);
};
const closeModal = () => {
setModalOpened(false);
};
interface ModalProps {
children: React.ReactNode;
}
const ModalPortal: React.FC<ModalProps> = ({ 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}
/>
{children}
</Container>,
ref.current
);
}
return null;
};
return {
openModal,
closeModal,
ModalPortal,
};
};
export default useModal;
components/HeaderAuths.tsx
import useModal from "../hooks/useModal";
const HeaderAuths: React.FC = () => {
const { openModal, ModalPortal, closeModal } = useModal();
return (
<>
<div className="header-auth-buttons">
<button
className="header-sign-up-button"
onClick={() => {
dispatch(authActions.setAuthMode("signup"));
openModal();
}}
type="button"
>
회원가입
</button>
<button
className="header-login-button"
type="button"
onClick={() => {
dispatch(authActions.setAuthMode("login"));
openModal();
}}
>
로그인
</button>
</div>
<ModalPortal>
<AuthModal closeModal={closeModal} />
</ModalPortal>
</>
);
};
export default HeaderAuths;
Modal
import React, { FC, PropsWithChildren, useCallback } from 'react';
interface Props {
show: boolean;
onCloseModal: () => void;
}
const Modal: FC<PropsWithChildren<Props>> = ({ show, children, onCloseModal }) => {
const stopPropagation = useCallback((e) => {
e.stopPropagation();
}, []);
if (!show) {
return null;
}
return (
<div onClick={onCloseModal}>
<div onClick={stopPropagation}>
<button onClick={onCloseModal}>×</button>
{children}
</div>
</div>
);
};
export default Modal;