VIVITRIP 프로젝트 회고_modal

Kingdwan·2025년 4월 11일
post-thumbnail

모달 컴포넌트 제작

3차례 프로젝트를 진행 하면서 다른 팀원이 작성한 모달을 외부에서 사용하였는데 이번 프로젝트를 진행 하면서 좀더 모달을 이해하고 코드를 분석 해보고 싶은 마음이 생겼다.

모달 구현 목표

1. 포털을 사용한 모달

2. 페이지마다 모달 상태, 포지션, 이벤트 등을 따로 관리하다 보니 중앙 집중식 모달 관리의 필요성

크게 2가지 이다.



[기존 프로젝트에서 사용된 모달 방식]

기존 코드 모달 방식의 단점

  1. 다수의 모달 관리에는 불리
    (프로젝트가 커지고 여러 페이지에서 모달이 등장할 경우, 각각 상태/구현을 따로 관리해야 해서 중앙 집중형 아키텍처가 필요해진다.)

  2. 포탈 구조 미사용으로 인한 레이아웃 간섭 가능성

이를 해결 하고자 찾았던 방법이 포탈 이었다.

Portal 이란 무엇인가?

"포털(Portal)"은 리액트에서 모달, 드롭다운, 토스트 같은 UI를 만들 때 "DOM 트리 바깥"에 컴포넌트를 렌더링할 수 있게 해주는 기능

사용방법

import { createPortal } from "react-dom";

const MyModal = () => {
  return createPortal(
    <div className="modal">모달 내용</div>,
    document.getElementById("modal-root") as HTMLElement
  );
};


//여기서 modal-root는 보통 index.html의 <body> 안에 따로 선언
<body>
  <div id="root"></div>
  <div id="modal-root"></div> <!-- 포탈용 루트 -->
</body>

앞서 기존 모달 방식의 단점인 레이아웃 간섭이 포탈을 사용하면
모달이 DOM 최상단에 렌더링되므로 z-index, 스크롤, 포지셔닝 문제 해결된다.

모달 상태관리

기존에는 각 컴포넌트나 페이지에서 모달 상태를 로컬하게 관리했기 때문에, 모달 상태가 꼬이거나, 모달 닫힘 처리를 공통화하지 못하는 문제가 있었다.이번 프로젝트는 그것을 방지하기 위해 Zustand를 이용한 전역 모달 상태 관리 구조를 설계했다.
단일 모달만 띄우되, 내부 콘텐츠는 컴포넌트를 동적으로 주입하는 구조로 변경하면서,앱 어디서든 모달을 자유롭게 띄우고 닫을 수 있게 되었다..

코드

const useModalStore = create<ModalStoreState>((set) => ({
  // 초기값 설정
  isModalOpen: false,         // 기본적으로 모달은 닫혀 있음
  modal: null,                // 모달에 표시할 내용 없음
  modalID: null,              // 유니크 ID 없음
  modalOptions: null,         // 커스텀 옵션 없음

  // 모달 열기 함수
  setModalOpen: (modal, options) =>
    set({
      isModalOpen: true,             // 모달 열림 상태로 전환
      modalID: uuidv4(),             // 새로운 UUID 생성 → 모달 인스턴스 구분
      modal,                         // 표시할 컴포넌트 저장
      modalOptions: options || null, // 전달된 옵션 저장 (없으면 null)
    }),

  // 모달 닫기 함수
  setModalClose: () =>
    set({
      isModalOpen: false,   // 모달 닫힘
      modalID: null,        // ID 초기화
      modal: null,          // 콘텐츠 제거
      modalOptions: null,   // 옵션 제거
    }),
}));

export default useModalStore;


// 외부에서 사용 된 모습
const { setModalOpen } = useModalStore();

const openModal = () => {
  setModalOpen(<MyModalContent />, {
    customClass: "w-[600px]",
  });
};

모달 트러블 슈팅

1. 페이지 이동 시 모달이 닫히지 않는 문제

모달을 띄운 상태에서 next/router로 페이지 이동을 하면, 모달이 여전히 열린 채로 다음 페이지에도 렌더링되어 남아있는 현상이 발생했다.

  • 원인 분석

    현재 모달은 Zustand를 통해 전역 상태로 관리되고 있음.
    ModalPortal이 조건부 렌더링 되지 않거나, 렌더링 시점을 다시 잡지 않으면 라우팅 이후에도 모달이 열린 상태로 남아 있음.

  • 해결 방법

 useEffect(() => {
  const handleRouteChange = () => {
    if (isModalOpen) {
      setModalClose(); // 모달 상태 초기화
    }
  };

  router.events.on("routeChangeStart", handleRouteChange);

  return () => {
    router.events.off("routeChangeStart", handleRouteChange);
  };
}, [isModalOpen, setModalClose, router]);
  • 회고

전역 모달 상태를 사용하다 보니, 페이지 전환 시 모달이 닫히지 않고 그대로 유지되는 문제가 발생했다.이는 사용자 경험에 혼란을 줄 수 있기 때문에, Next.js의 router.events를 통해 routeChangeStart 시점에 setModalClose()를 호출하여 강제로 닫는 방식으로 문제를 해결했다. 이로써 페이지 전환 시 모달 상태가 초기화되어 UX의 일관성이 향상되었다.

2. ESC 키를 눌러도 모달이 닫히지 않음

사용자입장에서는 모달이 열린 상태에서 ESC 키를 눌러 모달을 닫는 경우도 많을 것이다. 제작자인 본인은 당연히 x 버튼을 눌러 모달을 닫았기 때문에 그만 문제를 놓쳐 버렸다.

  • 원인 분석

    리액트에서 브라우저 전역 키보드 이벤트를 감지하려면 window.addEventListener('keydown', ...) 형태로 수동 처리해야 함.
    모달에 keydown 이벤트를 리스닝하고 있지 않음.

  • 해결 방법

import { useEffect } from "react";

const useEscapeClose = (onClose: () => void) => {
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        onClose();
      }
    };
    document.body.addEventListener("keydown", handleKeyDown);

    return () => {
      document.body.removeEventListener("keydown", handleKeyDown);
    };
  }, [onClose]);
};

export default useEscapeClose;

useEscapeClose 훅을 만들어 모달이 열릴 때 ESC 키 입력을 감지하고, 눌렀을 때 setModalClose()를 실행하도록 설정

  • 회고

    대부분의 서비스에서 ESC 키를 눌렀을 때 모달이 닫히는 UX가 일반화되어 있음에도, 초기에 이를 고려하지 않아 접근성과 사용성에서 불편이 발생했다. 이를 해결하기 위해 window의 keydown 이벤트를 감지하는 커스텀 훅을 만들고, ESC 키 입력 시 setModalClose()를 실행해 자연스러운 UX를 구현했다. 추가적으로 작업 하다가 여러 상황을 전제 조건으로 테스트 해야 하는 필요성을 느끼게 되었다.

모달 리팩토링하면서 느낀 점

이번 모달 리팩토링에서는 포털을 도입하면서 모달과 페이지의 구조적 분리를 명확하게 가져갈 수 있었고, 그 덕분에 스크롤 제어나 z-index 문제를 깔끔하게 해결할 수 있었다. 또한 body 스크롤을 차단하고 모달 내부에서만 스크롤이 되도록 한 점, 그리고 오버레이의 시각적 개선을 통해 사용자 초점을 더 자연스럽게 유도한 점이 전체적인 사용자 경험을 한 단계 끌어올려 준 포인트였다.

[모달이 열리면 오버레이색상이 변경됨에 따라 시선은 자연스럽게 모달로 향하게 된다. ]

0개의 댓글