useModal 모달 애니메이션 주기

Song-Minhyung·2022년 8월 4일
0

React

목록 보기
4/10

시리즈

  1. React 모달 여러개 띄우기
  2. useModal훅 만들기, 애니메이션 주기
  3. useModal 애니메이션 개선

개선이 필요하다!

이전글에서 useModal훅을 만들어서 모달을 여러개 띄웠었다.

const App = () => {
  const { Modal, closeModal, isOpen, openModal } = useModal();
  return (
    <>
      <button onClick={openModal}></button>
      <Modal
        isOpen={isOpen}
        onClose={closeModal}
      >
        <CategoryModal />
      </Modal>
    </>
  );
};
export default App;

이런식으로 띄웠었는데 쓸때마다 isOpenonClose를 넣는게 정말 번거로웠다.
그래서 useModal훅을 조금 개선하기로 했다.
그리고 열릴때와 닫힐때 애니메이션도 추가하기로 했다.

시작

현재 스타일링을 위해 styled-components를 사용하고있다.
타입스크립트에서 사용하려면 npm으로 설치를 한다면 아래 두개를 설치한다.

▶︎ npm i styled-components
▶︎ npm i -D @types/styled-components

그리고서 우선 useModal 훅부터 수정을 시작한다.

import ModalFrame from 'components/Common/Modal';
import React, { useState } from 'react';

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

  const closeModal = () => {

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

  const Modal = ({ children }: { children: React.ReactNode }) => (
    <ModalFrame
      isOpen={isOpen}
      onClose={closeModal}
    >
      {children}
    </ModalFrame>
  );

  return { Modal, showModal, closeModal };
};

export default useModal;

전에 바깥으로 isOpen, onClose를 받았었는데 다시 생각해보니 그럴 필요가 없었다.
useModal 훅에서 전부 처리해주면 되는데 귀찮은짓을 했었다.

이렇게만 사용해도 충분하지만 모달이 열릴때, 닫힐때 애니메이션을 넣고싶었다.
그래서 수많은 뻘짓 끝에 맘에드는 코드를 작성했다.

애니메이션 KeyFrames 선언

우선 styled-components를 사용해 css에서 사용할 keyframes를 외부에 작성해줄것이다.

// animations.ts
import { css } from 'styled-components';

export const Up100 = keyframes`
  0% {bottom: -100%; }
  100% {bottom: 0; }
`;

export const Down100 = keyframes`
  0% {bottom: 0%; }
  100% {bottom: -100%; }
`;

이렇게 선언한 애니메이션은 Modal에서 사용할것이다.
그리고 이제 Modal 컴포넌트는 열릴때, 닫힐때 애니메이션과 unuount를 알리는 prop을 추가로 받는다.

useModal() 수정

unmount가 됐는지 알 필요가 있는 이유는
컴포넌트가 unmount되기전 닫히는 애니메이션을 실행 후 unmount시키기 위해서다.
아래 코드를 보자.

import ModalFrame from 'components/Common/Modal';
import React, { useState } from 'react';
import { Keyframes } from 'styled-components';

const useModal = (
  openAnime?: Keyframes,	// 모달 열릴시 애니메이션
  closeAnime?: Keyframes,	// 모달 닫힐시 애니메이션
  animeTimeMs = 300,		// 애니메이션 실행시간
) => {
  const [isOpen, setIsOpen] = useState(false);
  const [isUnmount, setIsUnmount] = useState(false);

  const closeModal = () => {
    setIsUnmount(true);

    setTimeout(() => {
      setIsOpen(false);	// animeTimeMs의 시간후 모달 닫음
    }, animeTimeMs);
  };

  const showModal = () => {
    setIsUnmount(false)
    setIsOpen(true);
  };

  const Modal = ({ children }: { children: React.ReactNode }) => (
    <ModalFrame
      isOpen={isOpen}
      onClose={closeModal}
      isUnmount={isUnmount}
      openAnimation={openAnime}
      closeAnimation={closeAnime}
    >
      {children}
    </ModalFrame>
  );

  return { Modal, showModal, closeModal };
};

export default useModal;

위 코드는 매개변수로 애니메이션과 애니메이션 시간을 받는다.
그러고서 Modal, showModal, closeModal을 리턴해준다.
이제 Modal을 사용하고싶으면 그냥 아래처럼 쓰면 된다

const {Modal, showModal, closeModal}
<button onClick={showModal}> open Modal </button>
<Modal>{넣고싶은 모달}</Modal>

훨씬 간단해졌다.
애니메이션도 들어가므로 Modal 컴포넌트도 수정이 필요하다.

import React, { MouseEvent } from 'react';
import { ModalInner, ModalWrapper } from './Modal.style';
import ModalPortal from './ModalPortal';
import { Keyframes } from 'styled-components';

interface ModalProps {
  openAnimation?: Keyframes;
  closeAnimation?: Keyframes;
  isUnmount: boolean;
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
}

const Modal = ({
  openAnimation,
  closeAnimation,
  isUnmount,
  isOpen,
  onClose,
  children,
}: ModalProps) => {
  const handleClickInnerModal = (e: MouseEvent<HTMLDivElement>) => {
    // ModalWrapper로 이벤트 전파 방지
    e.stopPropagation();
  };
  return (
    <>
      {isOpen && (
        <ModalPortal>
          <ModalWrapper onClick={onClose}>
            <ModalInner
              onClick={onClose}
              openAnimation={openAnimation}
              closeAnimation={closeAnimation}
              isUnmount={isUnmount}
            >
              <div onClick={handleClickInnerModal}>{children}</div>
            </ModalInner>
          </ModalWrapper>
        </ModalPortal>
      )}
    </>
  );
};

export default Modal;

그리고 애니메이션도 입력받아야 하므로 styled-components로 선언한 ModalInner도 수정한다.

import styled, { Keyframes } from 'styled-components';

interface TModalInner {
  openAnimation?: Keyframes;
  closeAnimation?: Keyframes;
  isUnmount: boolean;
}

export const ModalWrapper = styled.div`
  position: fixed;
  left: 0;
  top: 0;

  z-index: 1050;
  width: 100%;
  height: 100%;

  background-color: #0b0b0bb8;
`;

export const ModalInner = styled.div<TModalInner>`
  position: relative;
  width: 100%;
  height: 100%;

  max-width: ${({ theme }) => theme.deviceSizes.mobile};
  margin: 0 auto;

  animation: ${({ isUnmount, openAnimation, closeAnimation }) =>
      isUnmount ? closeAnimation : openAnimation}
    0.3s ease-in-out;
`;

이렇게해서 이제 훨씬 간편하게 애니메이션과 함께 쓸 수 있게됐다.

완성

사용은 이렇게 할 수 있다.

import useModal from 'hooks/useModal';
import { Down100, Up100 } from 'styles/animations';

interface CalanderPageProps {}

const CalanderPage = ({}: CalanderPageProps) => {
  const { Modal, showModal, closeModal } = useModal(Up100, Down100, 300);

  return (
    <>
      <button onClick={showModal}>click!!</button>
      <Modal>
        <SelectMonthModal/>
      </Modal>
    </>
  );
};

export default CalanderPage;

SelectMonthModal은 별도로 만든 컴포넌트다.
이렇게 시작, 종료 애니메이션까지 넣고 실행하면 아래처럼 올라갔다 내려갔다 애니메이션이 잘 보이게 된다.
물론 SelectMonthModal에서 또 모달을 부른다면 중첩이 된다.

profile
기록하는 블로그

0개의 댓글