React 컴포넌트로 Modal 창 구현하기(with hooks)

beeen_devlog·2022년 12월 24일
0

React Portal

React Portal은 부모 컴포넌트의 DOM 계층 구조 밖에 컴포넌트를 렌더링할수 있게 해주는 역할을 한다.
사용법은 다음과 같이 매우 간결하다.

// 컴포넌트를 렌더링하고자 하는 위치의 부모 컴포넌트의 element
const rootEl = document.getElementById("root");
// 렌더링할 컴포넌트
const PortalComponent = () => <div>portal</div>

ReactDOM.createPortal(PortalComponent, rootEl)

기본 구현

일반적으로 Modal창은 한 프로젝트에서 여러개를 사용하므로, 모듈화시키는 것이 좋다.
필자는 다음고 같이 구현하였다.
우선 Portal을 구현한 컴포넌트를 추가하여 children을 렌더링하게 한다.

const rootEl = document.getElementById("root");

const Portal = ({ children }) => {
  return reactDom.createPortal(children, rootEl);
};

그 다음, 뒷배경을 구현하기 위해 그 안에 ModalBackground를 추가한 ModalWrapper 컴포넌트를 만든다.

const ModalWrapper = ({ isOpen, closeModal, children }) => {
  const onClickModal = useCallback((e) => {
    e.stopPropagation();
  }, []);
  if (!isOpen) {
    return null;
  } else {
    return (
      <Portal>
        <ModalBackground onClick={closeModal}>
          <div onClick={onClickModal}>{children}</div>
        </ModalBackground>
      </Portal>
    );
  }
};

const ModalBackground = styled.div`
  width: 100%;
  height: 100%;
  position: absolute;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 500;
`;

다음과 같이 구현하면, ModalWrapper로 감싸주게 되면 어떤 컴포넌트라도 Modal로써 사용할수 있게 된다.

const MyModalMaker = () => {
  const [isOpen, setIsOpen] = useState<boolean>(false);

  const onOpen = () => {
	setIsOpen(true)
  }
  
  const closeModal = () => {
	setIsOpen(false)
  }

  return (
    <>
	  // 클릭시 Modal open
	  <button onClick={onOpen}>open</button>
      <ModalWrapper isOpen={isOpen} closeModal={closeModal}>
        <AlertModal closeModal={closeModal} />
      </ModalWrapper>
    </>
  );
};

다음과 같이 ModalWrapper를 사용하여 Modal관련 로직과 Modal이 될 컴포넌트의 로직을 성공적으로 분리할수 있게 되었다.

하지만 여기서 한가지 번거로운 점이 있는데, 여러 모달창들을 추가할때마다 모달창에 대한 상태를 계속 추가해주어야 한다는 문제점이 생긴다.

// 한 컴포넌트 내에 모달창 3개를 구현할 상황에서..
const [isOpen1, setIsOpen1] = useState<boolean>(false);
const [isOpen2, setIsOpen2] = useState<boolean>(false);
const [isOpen3, setIsOpen3] = useState<boolean>(false);

// 다음과 같이 계속 추가해주어야 한다.
const onOpen1 = () => {...}
const closeModal1 = () => {...}

const onOpen2 = () => {...}
const closeModal2 = () => {...}

const onOpen3 = () => {...}
const closeModal3 = () => {...}

또한 지금은 open, close 기능밖에 없지만 추후 다양한 기능이 추가될 경우, 같이 구현해주어야 할 상태들이 많이 있을수도 있다.

따라서 필자는 해당 기능과 컴포넌트를 커스텀 훅을 사용하여 다음과 같이 추상화하였다.

const useModal = () => {
  const [isOpen, setIsOpen] = useState(false);

  const openModal = () => {
    setIsOpen(true);
  }

  const closeModal = () => {
    setIsOpen(false);
  }
  // 다음과 같이 부가기능도 마음껏 추가할수 있다.
  const toggleModal = () => {
    setIsOpen(!isOpen);
  }

  // 다음과 같이 isOpen, closeModal이 미리 추가된 ModalWrapper을 리턴
  const ModalWrapper = ({children}) => {
    return <ModalWrapper isOpen={isOpen} closeModal={closeModal}>{children}</ModalWrapper>  
  }
  
  // 다른 형태의 Modal도 선택적으로 추가할수 있다.
  const NewModalWrapper = ({children}) => {
    return <NewModalWrapper isOpen={isOpen} closeModal={closeModal}>{children}</NewModalWrapper>  
  }

  return {isOpen, openModal, closeModal, toggleModal, ModalWrapper};
}

export default useModal;

다 만들어놓은 useModal을 사용하면 단지 다음과 같이 하면 된다!

const MyModalMaker = () => {
  const {openModal, closeModal, ModalWrapper} = useModal();

  return (
    <>
	  // 클릭시 Modal open
	  <button onClick={openModal}>open</button>
      <ModalWrapper>
        <AlertModal closeModal={closeModal} />
      </ModalWrapper>
    </>
  );
};
profile
꾸준함을 목표로 하는 프론트엔드 개발자

0개의 댓글