
같은글 노션링크 | 코드 변경 전후를 명확하게 보실수있습니다.
1.1 도입이유
1.2 도입 목표
2.1 기존 모달 로직
2.2 context 생성
2.3 모달 사용하기
2.4 ReactDom.CreatePortal
현재 AelrtModalContext 로 모달을 사용하고있다. 기능적으로 문제는 없지만, 해당 코드를 볼수록 디자인과 기능이 강결합돼 있어서 Headless 처럼 두 로직을 분리해야 하지 않을까 라는 생각이 들었다.
또, 다른 UI의 모달을 새로 만든다면, 기존의 것(AlertModalContext)을 재사용할 수 없으므로, XXXModalContext를 여러개 새로 만들어야만 하는 상황이다. 그래서 모달을 잘 만들고 효율적으로 관리할 수 있는 방법을 찾아보게 되었다.
문제점
// context/AlertModalProvider.tsx
export const useAlertModalContext = () => {
return useContext(AlertModalContext);
};
export const AlertModalContext = createContext({
showAlert(message: string) {},
});
export const AlertModalProvider = ({ children }: PropsWithChildren) => {
const [message, setMessage] = useState('');
const [isOpen, setIsOpen] = useState(false);
const showAlert = (message: string) => {
setIsOpen(true);
setMessage(message);
};
const closeAlert = () => {
setIsOpen(false);
setMessage('');
};
return (
<AlertModalContext.Provider value={{ showAlert }}>
{children}
{isOpen && (
<Backdrop whiteBoard>
<div
css={css`
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
width: 85%;
height: 200px;
font-size: 22px;
font-family: 'Noto Sans KR';
`}
>
{message}
<Button
onClick={closeAlert}
css={css`
width: 90%;
padding: 5px 0;
background-color: ${colors.grey200};
font-size: 20px;
`}
>
닫기
</Button>
</div>
</Backdrop>
)}
</AlertModalContext.Provider>
);
};
context/ModalContext.tsx
export const ModalContext = createContext<TestModals[]>([]);
export const ModalDispatchContext = createContext({
open: ({ type, props }: TestModals) => {},
close: () => {},
});
export const useModalContext = () => useContext(ModalContext);
export const useModalDispatchContext = () => useContext(ModalDispatchContext);
function ModalProvider({ children }: { children: ReactNode }) {
const [openedModals, setOpenedModals] = useState<TestModals[]>([]);
const open = ({ type, props }: TestModals) => {
const KEY = Math.random().toString(36).substring(2);
setOpenedModals(modals => {
return [
...modals,
{
type,
props,
id: `${type}${KEY}`,
},
];
});
};
const close = () => {
setOpenedModals(modals => {
return modals.slice(0, modals.length - 1);
});
};
const dispatch = useMemo(() => ({ open, close }), []);
return (
<ModalDispatchContext.Provider value={dispatch}>
<ModalContext.Provider value={openedModals}>
{children}
<CreatePortal />
</ModalContext.Provider>
</ModalDispatchContext.Provider>
);
}
export default ModalProvider;
function RootErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
const { openModal } = useModal();
...
if (error.code === 'ERR_NETWORK') {
openModal({
type: 'alert', // 모달종류
// 표현할 컨텐츠 전달
props: { title: '앗...😰', message: ERROR_MESSAGE[998], btnText: '닫기' },
});
resetErrorBoundary();
return;
}
return (...)
}
모달 종류는 두가지 있다.
const MODAL_COMPONENTS = {
alert: AlertModal,
confirm: ConfirmModal,
};
function AlertModal({ onClose, title, message, btnText = '닫기' }: ModalProps) {
return (
<BackDrop whiteBoard>
<div css={alert.alignColumn} role="dialog" tabIndex={0}>
<h3 css={alert.title}>{title}</h3>
<p css={alert.message}>{message}</p>
<Button onClick={onClose} css={alert.button} aria-label={btnText}>
{btnText}
</Button>
</div>
</BackDrop>
);
}
function ConfirmModal({ onClose, onSubmit, title, message, btnText,: ModalProps) {
return (
<BackDrop whiteBoard>
<div css={alert.alignColumn}>
<h3 css={alert.title}>{title}</h3>
<p css={alert.message}>{message}</p>
<div css={confirm.btnWrapper}>
<Button onClick={onSubmit} css={confirm.button}>
{btnText}
</Button>
<Button onClick={onClose} css={confirm.button}>
닫기
</Button>
</div>
</div>
</BackDrop>
);
}
createPortal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식 컴포넌트를 렌더링하는 방법이다. 모달창이 때때로 부모 컴포넌트의 스타일링에 영향받는 경우가 있는데, 이때 모달 컴포넌트가 부모 컴포넌트로부터 독립적인 위치에서 렌더링을 할 수 있게 해준다.
// components/@helper/Modal/CreatePortal.tsx
// 사용가능한 모달종류
const MODAL_COMPONENTS = {
alert: AlertModal,
confirm: ConfirmModal,
};
function CreatePortal() {
const openedModals = useModalContext();
const { hideModal } = useModal();
let modalElement;
// openedModals state를 map으로 순회하면서
// 렌더링할 모달컴포넌트를 리턴하고, createPortal에 전달한다.
const renderModal = openedModals.map(({ type, props, id }: Modals) => {
const ModalComponent = type && MODAL_COMPONENTS[type];
const { onSubmit, onClose, ...restProps } = props;
modalElement = document.getElementById(`${type}-modal`);
const handleSubmit = async () => {
if (typeof onSubmit === 'function') await onSubmit();
};
const handleClick = () => {
onClose && onClose();
hideModal();
};
return (
<ModalComponent
key={id}
{...restProps}
onClose={handleClick}
onSubmit={handleSubmit}
/>
);
});
return modalElement && createPortal(renderModal, modalElement);
}
export default CreatePortal;

const { showModal } = useModal();
const confirm = () => {
showModal({
type: 'confirm',
props: {
title: ' confirm 모달',
message: '컨펌하시겠습니까?',
btnText: 'yes',
onSubmit: () => window.alert('submit'),
},
});
};
const alert = () => {
showModal({
type: 'alert',
props: {
title: ' alert 모달',
message: '알림입니다.',
btnText: 'yes',
},
});
};
return (
<>
<div onClick={confirm}>confirm</div>
<div onClick={alert}>alert</div>
</>
);
참고
낙타 | 효율적인 modal 관리 with React(1)
낙타 | 효율적인 modal 관리 with React(2)
leego | [React] 효율적으로-모달-관리하기 -1
leego | [React] Portal을 사용한 모달창 만들기 -2
[React] 모달 깊게 파헤치기 : 효율적인 모달 관리에 대하여 (useModal custom hooks)
React | Portal을 이용한 Modal 구현
[React] Portal (포탈), Modal 구현하기
https://velog.io/@velopert/react-context-tutorial#context-란
https://mygumi.tistory.com/406
portal, modal 기본적인 사용법
https://abangpa1ace.tistory.com/entry/React-Portal-포탈 - 포탈 설명 굿
https://www.jeong-min.com/33-modal/ - 모달에 추가할만한 기능들