일상이 개발

React 모달/다이얼로그 시스템 설계 가이드 – Context 상태 관리, 포탈, 접근성까지 완벽 정리

아빠고미 2025. 5. 3. 03:46
반응형

React에서 모달/다이얼로그 시스템 설계 및 Context 기반 상태 관리 전략 – 중첩 모달, 포탈, 접근성까지 완벽 가이드

React 앱을 만들다 보면 가장 자주 등장하면서도 가장 설계가 까다로운 UI 중 하나가 바로 모달(Modal)입니다.

React 모달/다이얼로그 시스템 설계 가이드 – Context 상태 관리, 포탈, 접근성까지 완벽 정리

처음에는 간단한 팝업 하나였던 것이, 나중에는 중첩 모달, 다중 모달 상태, 포커스 트랩, 외부 클릭 감지, 키보드 접근성 등으로 점점 복잡해지죠.

이번 글에서는 다음과 같은 문제를 한 번에 해결하는 모달 시스템 설계 전략을 정리합니다.

  • ✅ 모달을 어디에서 상태로 관리할 것인가?
  • ✅ 여러 모달이 동시에 뜰 때 어떻게 제어할 것인가?
  • ✅ 재사용 가능한 구조를 어떻게 만들 것인가?
  • ✅ 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로 모달 상태를 전역에서 안정적으로 관리
  • 중첩 모달은 스택 기반으로 제어하자
  • 접근성과 포커스 제어는 반드시 포함

이제 여러분의 프로젝트에서도 “튼튼하고 일관된 모달 시스템”을 직접 설계해보세요. 유저는 물론, 개발자 모두가 편안해질 거예요!


도움이 되셨다면 댓글과 공감 부탁드립니다! 🙌

반응형