일상이 개발

Next.js 모달 & 다이얼로그 완전정복 – 전역 상태 관리부터 UX 접근성까지 실전 가이드

아빠고미 2025. 5. 18. 12:59
반응형

Next.js 앱에서 모달과 다이얼로그 관리 전략 – 전역 모달 스토어부터 겹침 순서 UX까지 실전 가이드

모달(modal)은 UI에서 가장 많이 사용되는 인터랙션 중 하나입니다. 로그인, 알림, 설정, 폼 입력, 확인창 등 다양한 상황에서 사용자의 흐름을 잠시 멈추고 집중하게 만드는 역할을 하죠.

하지만 프로젝트 규모가 커질수록 모달의 개수는 많아지고, 모달을 제어하는 로직은 점점 복잡해집니다.

Next.js 모달 & 다이얼로그 완전정복 – 전역 상태 관리부터 UX 접근성까지 실전 가이드

이번 글에서는 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

🎯 모달은 “가볍지만 강력하게”

복잡한 로직이 아닌, 사용자의 흐름을 부드럽게 돕는 구조로 설계할수록 더 효과적인 사용자 경험을 제공합니다.


긴 글 읽어주셔서 감사합니다! 공감, 댓글, 공유는 다음 글 제작에 큰 힘이 됩니다 🙌

반응형