일상이 개발
Next.js 모달 & 다이얼로그 완전정복 – 전역 상태 관리부터 UX 접근성까지 실전 가이드
아빠고미
2025. 5. 18. 12:59
반응형
Next.js 앱에서 모달과 다이얼로그 관리 전략 – 전역 모달 스토어부터 겹침 순서 UX까지 실전 가이드
모달(modal)은 UI에서 가장 많이 사용되는 인터랙션 중 하나입니다. 로그인, 알림, 설정, 폼 입력, 확인창 등 다양한 상황에서 사용자의 흐름을 잠시 멈추고 집중하게 만드는 역할을 하죠.
하지만 프로젝트 규모가 커질수록 모달의 개수는 많아지고, 모달을 제어하는 로직은 점점 복잡해집니다.
이번 글에서는 Next.js 앱에서 모달과 다이얼로그를 체계적으로 설계하고 효율적으로 관리하는 방법을 다음과 같은 구조로 정리합니다:
- ✅ 모달 컴포넌트의 역할과 UX 정의
- ✅ 전역 모달 스토어 설계 (Zustand, Context API)
- ✅ 중첩 모달, 겹침 순서(Z-index) 처리
- ✅ ESC 닫기, 바깥 영역 클릭 처리
- ✅ SSR 환경에서의 모달 구조
- ✅ 접근성과 키보드 탐색 고려
1. 🧠 모달의 UX 패턴 이해하기
✅ 모달과 다이얼로그의 차이
- Modal: 전체 UI 흐름을 잠시 멈추는 오버레이 인터페이스
- Dialog: 확인, 경고, 선택 등의 짧은 응답 목적
📌 좋은 모달의 조건
- 배경을 흐리게 처리하여 사용자 집중 유도
- 닫는 동작이 명확하고 빠르다 (ESC, X 버튼, 배경 클릭)
- 키보드 탐색이 가능하고 초점이 내부에 고정된다 (accessibility)
- 중첩 시 계층 정리(Z-index), 오버레이 관리가 철저하다
✅ 모달 종류 분류
- 📝 폼 모달 (회원가입, 정보 입력)
- ⚠️ 확인/경고 모달 (삭제 확인 등)
- 🔍 상세 모달 (상세보기, 미리보기)
- 📦 장바구니/옵션 선택 모달
2. 📦 모달 컴포넌트 기본 구조 만들기
✅ 예시: 기본 Modal 컴포넌트
// components/Modal.tsx
import React from 'react';
export default function Modal({ children, isOpen, onClose }) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<button className="close-btn" onClick={onClose}>X</button>
{children}
</div>
</div>
);
}
✅ 스타일 기본 예시
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex; justify-content: center; align-items: center;
z-index: 1000;
}
.modal-content {
background: #fff;
padding: 2rem;
border-radius: 8px;
min-width: 300px;
position: relative;
z-index: 1001;
}
📌 UX 포인트
- onClick 이벤트 전파 방지로 배경 클릭만 닫히게 함
- 닫기 버튼은 상단 우측에 배치
- ESC 키, focus trap은 다음 섹션에서 구현
3. 🧭 전역 모달 상태 관리 – Zustand or Context로 관리하자
✅ 왜 전역으로 관리할까?
- 모달이 페이지 어디서든 열릴 수 있음
- 여러 모달이 중첩될 수 있음
- 라우팅과 별개로 모달 상태 유지가 필요함
📦 Zustand 기반 전역 모달 스토어 예시
// store/modalStore.ts
import { create } from 'zustand';
type ModalState = {
modal: string | null;
openModal: (name: string) => void;
closeModal: () => void;
};
export const useModalStore = create<ModalState>((set) => ({
modal: null,
openModal: (name) => set({ modal: name }),
closeModal: () => set({ modal: null })
}));
📦 App 레벨에서 렌더링
// components/GlobalModal.tsx
import LoginModal from './modals/LoginModal';
import { useModalStore } from '@/store/modalStore';
export default function GlobalModal() {
const { modal, closeModal } = useModalStore();
if (modal === 'login') return <LoginModal isOpen={true} onClose={closeModal} />;
return null;
}
➡️ <GlobalModal />
컴포넌트를 layout.tsx
또는 _app.tsx
에 배치하여 전역에서 모달 상태를 감지해 렌더링할 수 있습니다.
4. 📍 모달 포탈 처리 – body 외부로 분리 렌더링
✅ 왜 포탈이 필요한가?
- 모달이
z-index
나 스크롤에 방해받지 않도록 root 외부에 출력 - 중첩 구조 방지
- 렌더링 위치 고정
📦 Portal 컴포넌트 예시
// components/Portal.tsx
import { createPortal } from 'react-dom';
import { useEffect, useState } from 'react';
export default function Portal({ children }) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return mounted ? createPortal(children, document.body) : null;
}
📌 모달에 적용
<Portal>
<div className="modal-overlay">...</div>
</Portal>
➡️ 모달은 항상 body
최상단에 렌더링되어야 레이아웃과 스타일 충돌 없이 동작할 수 있습니다.
5. 🧩 다중 모달, 중첩 모달 UX 설계
✅ 대표 상황
- 로그인 모달 안에서 "비밀번호 재설정" 모달이 다시 열림
- 상품 상세 모달 → 옵션 선택 모달 → 결제 모달
📦 중첩 모달 스택 관리 예시
type ModalState = {
stack: string[];
openModal: (name: string) => void;
closeModal: () => void;
};
export const useModalStore = create<ModalState>((set) => ({
stack: [],
openModal: (name) => set((state) => ({ stack: [...state.stack, name] })),
closeModal: () => set((state) => ({ stack: state.stack.slice(0, -1) }))
}));
➡️ 모달을 스택 구조로 쌓고, 가장 위에 있는 모달만 렌더링하면 다중 모달 상황에서도 UX를 해치지 않고 관리할 수 있습니다.
6. ♿ 접근성(A11y)을 고려한 모달 UX 설계
✅ 모달 접근성 체크리스트
- 포커스 트랩: 모달 내부 요소에만 탭 이동 제한
- ESC 키로 닫기: 키보드만으로 모달 닫기 가능
- ARIA 속성: 스크린 리더가 인식할 수 있도록 구조화
- 모달 외부는 스크린 리더 숨김 처리
📦 ESC 닫기 이벤트 추가
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, []);
📦 포커스 트랩 구현
import FocusTrap from 'focus-trap-react';
<FocusTrap active={isOpen}>
<div className="modal-content">...</div>
</FocusTrap>
focus-trap-react 라이브러리를 사용하면 손쉽게 모달 내부 포커스를 제한할 수 있습니다.
📌 ARIA 속성 적용
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-desc"
>
<h2 id="modal-title">회원가입</h2>
<p id="modal-desc">가입을 위해 정보를 입력해주세요</p>
</div>
➡️ 접근성을 고려한 모달은 법적 요건을 충족하고, 모든 사용자가 사용할 수 있는 UI를 완성합니다.
7. ✅ UX 중심 모달 설계 체크리스트
📌 UI 흐름 체크
- ✔️ 모달 열릴 때 포커스가 내부로 이동하는가?
- ✔️ ESC 키나 바깥 클릭으로 닫을 수 있는가?
- ✔️ 전역 상태 관리가 잘 분리되어 있는가?
- ✔️ 중첩 모달일 경우 계층 및 오버레이가 적절한가?
- ✔️ 모바일에서도 스크롤 고정과 반응형이 정상 작동하는가?
🧩 프로젝트 구조 예시
/components
└── modals/
├── LoginModal.tsx
├── ConfirmModal.tsx
└── Portal.tsx
/store
└── modalStore.ts
/_app.tsx
└── <GlobalModal /> 포함
utils/
└── focusTrap.ts, zIndexManager.ts
🎯 모달은 “가볍지만 강력하게”
복잡한 로직이 아닌, 사용자의 흐름을 부드럽게 돕는 구조로 설계할수록 더 효과적인 사용자 경험을 제공합니다.
긴 글 읽어주셔서 감사합니다! 공감, 댓글, 공유는 다음 글 제작에 큰 힘이 됩니다 🙌
반응형