본문 바로가기

공부/Next

Next 커스텀 훅을 사용한 모달창 기능 구현

반응형

 

 

모달창을 구현하는 과정에서 효율성을 높이고 재사용할 수 있게 커스텀 훅을 활용해 모달 기능을 구현했습니다.

 

왜 커스텀 훅으로 모달을 구현했는지? 📖

모달창을 구현하다 보니 다양한 컴포넌트에서 동일한 모달 로직을 반복적으로 사용하는 문제가 있었습니다.

 

그래서 효율성과 재사용성을 높일 수 있게 커스텀 훅으로 분리했습니다!

 

useModal 커스텀 훅 📖

`useModal` 훅에서는 모달의 상태, 열기, 닫기, 포탈 렌더링 로직을 포함하고 있고 외부로 반환하고 있습니다.

import { useCallback, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';

const useModal = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);

  useEffect(() => {
    if (typeof window !== 'undefined') {
      setPortalElement(document.getElementById('overlays'));
    }
  }, [isOpen]);

  const openModal = useCallback(() => setIsOpen(true), []);
  const closeModal = useCallback(() => setIsOpen(false), []);

  const Modal = ({ children }: { children: React.ReactNode }) => {
    if (!isOpen || !portalElement) return null;

    return createPortal(
      <div
        onClick={closeModal}
        style={{ zIndex: 999, backgroundColor: 'rgba(53, 53, 53, 0.6)' }}
        className="fixed inset-0"
      >
        {children}
      </div>,
      portalElement
    );
  };

  return { isOpen, openModal, closeModal, Modal };
};

export default useModal;

 

 

 

모달을 열고 닫는 기능을 `useCallback`을 통해 메모이제이션하여 불필요한 렌더링을 방지했습니다.

const openModal = useCallback(() => setIsOpen(true), []);
const closeModal = useCallback(() => setIsOpen(false), []);

 

 

모달창이 렌더링 될 DOM 요소입니다. 최상단에 `overlays` ID를 가진 DOM 노드를 찾아 상태로 저장합니다.

const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);

  useEffect(() => {
    if (typeof window !== 'undefined') {
      setPortalElement(document.getElementById('overlays'));
    }
  }, [isOpen]);

 

 

`createPortal`을 사용해 모달을 렌더링하고 `isOpen`과 `portalElement`가 존재할 때만 렌더링 할 수 있도록 조건문을 설정했습니다.

const Modal = ({ children }: { children: React.ReactNode }) => {
    if (!isOpen || !portalElement) return null;

    return createPortal(
      <div
        onClick={closeModal}
        style={{ zIndex: 999, backgroundColor: 'rgba(53, 53, 53, 0.6)' }}
        className="fixed inset-0"
      >
        {children}
      </div>,
      portalElement
    );
  };

 

 

useModal 모달창 구현 📖

KakaoMap.tsx

`KakaoMap`컴포넌트에서 `useModal`훅을 호출해 마커를 클릭할 때 모달을 열고 `StampModal` 컴포넌트에서 `Modal`컴포넌트를 렌더링 합니다.

// KakaoMap.tsx
const KakaoMap = () => {
  const { openModal, Modal } = useModal();

  return (
    <>
      <KakaoMapMarker openModal={openModal} />
      <StampModal Modal={Modal} />
    </>
  );
};

export default KakaoMap;

 

StampModal.tsx

`Modal` 컴포넌트를 감싸 모달에 들어갈 내용을 작성했습니다.

// StampModal.tsx
import Image from 'next/image';
import Link from 'next/link';
import Icon from '../Icons/Icon';

interface StampModalPropsType {
  Modal: ({ children }: { children: React.ReactNode }) => React.ReactPortal | null;
}

const StampModal = ({ Modal }: StampModalPropsType) => {
  return (
    <Modal>
      <div
        onClick={(e) => e.stopPropagation()}
        className="absolute bottom-0 left-0 z-[1000] flex h-[342px] w-full animate-slideDownModal flex-col items-center justify-center rounded-tl-[32px] rounded-tr-[32px] bg-white p-6 shadow-overlayShadow"
      >
        {...}
      </div>
    </Modal>
  );
};

export default StampModal;

 

 

결과 화면 📖

회고 🧐

커스텀 훅을 이용한 모달 상태 관리와 포탈 렌더링을 분리해 사용해서 재사용성가독성을 높일 수 있었습니다.

덕분에 프로젝트 복잡성을 줄이고 일관성을 유지할 수 있었습니다!

 

 

반응형