모달, 확장성과 재사용성을 고려한 설계

seonik kim·2023년 8월 17일

목표 : 홈페이지에서 다양하게 사용되는 모달을재사용성과, 확장성이 높은 모달을 만들어보고자 함.

설계방법

  1. root 내부가 아닌 아래 처럼 코드를 작성하면 예기치 못한 z-index 오류 등을 예방할 수 있다.
    또한 기능에 따라 컴포넌트 분류를 했기 때문에 재사용성과 확장성이 좋다.
public / -index.tsx

<body>
  <div id="root"></div>
  <div id="modal-root"></div>
</body>

  1. ModalPortal.ts 파일을 만들어서 createPortal을 사용한다.

부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 방법
: createPortal을 사용하여 해당 컴포넌트의 HTML 내용을 다른 곳으로 포털, 즉 이동시킬 수 있다!

components/modal/ModalPortal.ts

import ReactDom from 'react-dom';
import { ReactNode } from 'react';

type ModalPortalProps = {
  children: ReactNode;
};

const ModalPortal = ({ children }: ModalPortalProps) => {
  const modalRoot = document.getElementById('modal-root') as HTMLElement;
  return ReactDom.createPortal(children, modalRoot);
};

export default ModalPortal;

  1. Modal 파일을 만들어준다.
    : handleClose 함수에서 이벤트 객체의 타입을 지정해주는게 좋다. handleClose함수는 onClick 이벤트의 콜백 함수로 전달되고 React.MouseEvent타입의 객체를 인자로 받게 된다.
import * as S from './Modal.styled';
import React from 'react';
import ModalPortal from './ModalPortal';

export type ModalProps = {
  children: React.ReactNode;
  isOpen: boolean;
  closeModal: () => void;
};

const Modal = ({ children, isOpen, closeModal }: ModalProps) => {
  const handleClose = (e: React.MouseEvent<HTMLElement>) => {
    e.stopPropagation();
    closeModal();
  };
  return <ModalPortal>{isOpen ? <S.Overlay onClick={handleClose}>{children}</S.Overlay> : null}</ModalPortal>;
};

export default Modal;
  1. useModal 훅을 만든다.
    : 위에서만든 Modal을 열림/닫힘 상태관리(isOpen), 열림버튼(openModal)과 닫힘버튼(closeModal)을 따로 사용하기위해 각각 return 한다.
import { useState } from 'react';
import { Modal, ModalProps } from '../components';

type UseModalReutnType = {
  Modal: React.FC<ModalProps>;
  isOpen: boolean;
  openModal: () => void;
  closeModal: () => void;
};

const useModal = (): UseModalReutnType => {
  const [isOpen, setIsOpen] = useState(false);
  const openModal = () => setIsOpen(true);
  const closeModal = () => setIsOpen(false);

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

export default useModal;

  1. Dialog 파일을 만든다.
    : closeModal을 매개변수로 받아 버튼을 클릭했을 때 Dialog가 닫힐 수 있도록 한다.
import * as S from './Dialog.styled';

type DialogProps = {
  closeModal: () => void;
};

const Dialog = ({ closeModal }: DialogProps) => {
  return (
    <S.Container>
      <S.Inner
        onClick={(e: React.MouseEvent<HTMLElement>) => {
          e.stopPropagation();
        }}
      >
        <button onClick={closeModal}>닫기</button>
      </S.Inner>
    </S.Container>
  );
};

export default Dialog;

  1. 최종 사용
import React from 'react';
import { useModal } from '../hooks';
import { Dialog } from '../components';

const Main = () => {
  const { Modal, isOpen, openModal, closeModal } = useModal();

  return (
    <>
      <Modal isOpen={isOpen} closeModal={closeModal}>
        <Dialog closeModal={closeModal} />
      </Modal>
      <button onClick={openModal}>열기</button>
    </>
  );
};

export default Main;

생각이 필요한 부분 :
1. Dialog의 디자인에 맞춰서 어떻게 children 값을 보여주면 좋을지 고민해보기
2. 타입을 잘 지정해주고 있는지 확인해보기


참조 : 확장성 및 재사용성이 높은 커스텀 모달 hook 만들기

profile
김선익

1개의 댓글

comment-user-thumbnail
2023년 9월 4일

최고에요

답글 달기