React 모달/다이얼로그 시스템 설계 가이드 – Context 상태 관리, 포탈, 접근성까지 완벽 정리
React에서 모달/다이얼로그 시스템 설계 및 Context 기반 상태 관리 전략 – 중첩 모달, 포탈, 접근성까지 완벽 가이드
React 앱을 만들다 보면 가장 자주 등장하면서도 가장 설계가 까다로운 UI 중 하나가 바로 모달(Modal)입니다.
처음에는 간단한 팝업 하나였던 것이, 나중에는 중첩 모달, 다중 모달 상태, 포커스 트랩, 외부 클릭 감지, 키보드 접근성 등으로 점점 복잡해지죠.
이번 글에서는 다음과 같은 문제를 한 번에 해결하는 모달 시스템 설계 전략을 정리합니다.
- ✅ 모달을 어디에서 상태로 관리할 것인가?
- ✅ 여러 모달이 동시에 뜰 때 어떻게 제어할 것인가?
- ✅ 재사용 가능한 구조를 어떻게 만들 것인가?
- ✅ Portal과 접근성을 어떻게 고려할 것인가?
1. 📌 모달은 왜 복잡한가?
❗ 단순한 UI 같지만 실제로는?
- 상태 관리 범위가 전역 ↔ 지역 사이에서 애매함
- 포커스 관리, 스크롤 제어, 백그라운드 클릭 이벤트 등 고려 요소 다수
- UI 라이브러리와 충돌 가능성 (Tailwind, MUI 등)
👉 따라서 모달은 ‘단순한 컴포넌트’가 아니라, 시스템으로 설계해야 합니다.
2. 🧱 기본적인 모달 컴포넌트 설계
✅ 모달 컴포넌트의 필수 구성 요소
- isOpen: 열림/닫힘 상태
- onClose: 닫기 함수 (ESC, 클릭 등)
- children: 내부 콘텐츠
📦 기본 예시
// Modal.jsx
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
}
🧠 개선 포인트
- 포커스가 백그라운드로 빠지는 문제
- 포탈 사용하지 않아 DOM 계층이 어색해짐
👉 다음 단계에서는 Portal 기반 설계로 넘어가 보겠습니다.
3. 🚪 React Portal을 활용한 모달 렌더링
기본적인 모달 컴포넌트는 DOM 트리 안에 그대로 위치해 있어 z-index 충돌이나 스크롤 제한, 레이아웃 간섭 문제가 발생할 수 있어요.
이를 해결하려면 모달을 DOM 트리 밖에 렌더링할 수 있어야 하고, React에서는 그 기능을 Portal
이 제공합니다.
✅ Portal 설정 예시
// index.html
<body>
<div id="root"></div>
<div id="modal-root"></div>
</body>
// Modal.jsx
import ReactDOM from 'react-dom';
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.getElementById('modal-root')
);
}
🎯 장점
- 레이아웃 충돌 없이 모달을 최상단에 렌더링
- 스크롤, 포커스 제어가 쉬워짐
4. 🌐 Context 기반 모달 상태 전역 관리
앱이 커지면 모달 상태를 useState로만 지역 관리하기에는 한계가 있습니다.
특히 로그인, 알림, 공지사항 등 다양한 모달이 생기면 중앙 집중형 관리 구조가 필요해요.
✅ Modal Context 설계 예시
// ModalContext.js
const ModalContext = createContext();
export function ModalProvider({ children }) {
const [modal, setModal] = useState(null);
const openModal = (name, props) => setModal({ name, props });
const closeModal = () => setModal(null);
return (
<ModalContext.Provider value={{ modal, openModal, closeModal }}>
{children}
<ModalContainer modal={modal} onClose={closeModal} />
</ModalContext.Provider>
);
}
// useModal.js
export function useModal() {
return useContext(ModalContext);
}
💡 사용 예시
const { openModal } = useModal();
openModal("login", { redirectTo: "/mypage" });
👉 이 방식은 어디서든 useModal()
훅을 통해 모달을 열고 닫을 수 있어요.
5. 🔄 중첩 모달 대응 전략
실제 서비스에서는 모달 안에서 또 다른 모달이 뜨는 상황이 종종 발생합니다. 예: 장바구니 모달 안에서 결제 정책 모달이 뜨는 구조 등.
📦 대응 구조
- 모달 상태를
배열
로 구성하여 스택처럼 관리 - 가장 상단 모달만 포커스를 갖고 이벤트 수신
- ESC 등 키보드 이벤트는 마지막 모달만 반응
✅ 구조 예시
const [modalStack, setModalStack] = useState([]); // [{ name: 'Login', props: {...} }, ...]
const openModal = (name, props) => {
setModalStack((prev) => [...prev, { name, props }]);
};
const closeModal = () => {
setModalStack((prev) => prev.slice(0, -1));
};
👉 중첩 모달을 쌓고, 마지막 모달만 렌더링하게 처리하면 안정적으로 다중 레이어 UI를 제어할 수 있습니다.
6. ♿ 모달의 접근성(A11y) 고려사항
모달은 시각적 요소뿐 아니라, 키보드와 스크린리더 사용자에게도 정확한 정보와 제어를 제공해야 해요.
✅ 필수 접근성 요소
role="dialog"
또는alertdialog
설정- aria-labelledby / aria-describedby로 타이틀 및 설명 연결
- ESC 키를 통한 닫기 기능
- Tab 키로 포커스 이동이 모달 안에서만 이루어지도록 제한
📦 기본 구조 예시
<div
role="dialog"
aria-labelledby="modal-title"
aria-describedby="modal-description"
aria-modal="true"
>
<h2 id="modal-title">로그인 안내</h2>
<p id="modal-description">서비스 이용을 위해 로그인이 필요합니다.</p>
</div>
7. 🔐 포커스 트랩과 ESC 닫기 기능
✅ 포커스 트랩
모달이 열리면 키보드 사용자는 Tab 키로 포커스 이동이 가능해야 하며, 포커스가 모달 안을 벗어나면 안 됩니다.
import { FocusTrap } from '@chakra-ui/focus-trap';
<FocusTrap>
<Modal />
</FocusTrap>
✅ ESC 키 닫기
useEffect(() => {
const handleEsc = (e) => {
if (e.key === 'Escape') {
closeModal();
}
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, []);
이러한 기능을 통해 키보드 접근성과 UX를 동시에 만족시킬 수 있어요.
8. 🧠 마무리 – 모달은 하나의 UI 시스템이다
모달은 단지 “띄우고 닫는” UI가 아닙니다. **상태 관리, DOM 구조, 접근성, UX, 포커스 제어**까지 모두 고려한 UI 시스템으로 설계되어야 합니다.
✨ 핵심 요약
- 모달은 재사용 가능한 구조로 설계하자
- React Portal로 구조를 분리하고 레이아웃 충돌 방지
- Context로 모달 상태를 전역에서 안정적으로 관리
- 중첩 모달은 스택 기반으로 제어하자
- 접근성과 포커스 제어는 반드시 포함
이제 여러분의 프로젝트에서도 “튼튼하고 일관된 모달 시스템”을 직접 설계해보세요. 유저는 물론, 개발자 모두가 편안해질 거예요!
도움이 되셨다면 댓글과 공감 부탁드립니다! 🙌